diff --git a/index.html b/index.html index 064d1931..cf67fbc7 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/package-lock.json b/package-lock.json index 2b3f05a1..eb2f9d01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,8 @@ "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", "@react-oauth/google": "^0.12.1", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.18.0", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "aos": "^2.3.4", @@ -27,6 +29,7 @@ "date-fns": "^3.6.0", "dompurify": "^3.3.1", "framer-motion": "^11.2.10", + "gsap": "^3.14.2", "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "js-confetti": "^0.12.0", @@ -47,7 +50,7 @@ "react-dom": "^18.2.0", "react-headroom": "^3.2.1", "react-hot-toast": "^2.4.1", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-image-crop": "^11.0.6", "react-intersection-observer": "^9.10.2", "react-loader-spinner": "^6.1.6", @@ -68,6 +71,7 @@ "react-toastify": "^10.0.5", "sass": "^1.76.0", "sister": "^3.0.2", + "three": "^0.182.0", "uuid": "^10.0.0", "xlsx": "^0.18.5" }, @@ -121,7 +125,6 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.6", @@ -623,6 +626,12 @@ "node": ">=0.1.90" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -678,7 +687,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "peer": true, "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -692,7 +700,6 @@ "version": "11.11.4", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -733,7 +740,6 @@ "version": "11.11.5", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -1276,7 +1282,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", "hasInstallScript": true, - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.5.2" }, @@ -2051,6 +2056,24 @@ "node": ">=10" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -2120,7 +2143,6 @@ "version": "5.15.20", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz", "integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/base": "5.0.0-beta.40", @@ -2606,6 +2628,196 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/three": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", + "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-three/drei": { + "version": "9.122.0", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz", + "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@react-spring/three": "~9.7.5", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^2.9.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "react-composer": "^5.0.3", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.8", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.0", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^8", + "react": "^18", + "react-dom": "^18", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", + "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=18 <19", + "react-dom": ">=18 <19", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@react-three/fiber/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -3032,6 +3244,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3078,6 +3296,12 @@ "@types/ms": "*" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3159,6 +3383,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -3187,7 +3417,6 @@ "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3202,6 +3431,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -3220,11 +3458,33 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, "node_modules/@types/stylis": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, + "node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -3238,6 +3498,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -3261,6 +3527,24 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vercel/analytics": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.3.1.tgz", @@ -3334,6 +3618,12 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "license": "BSD-3-Clause" + }, "node_modules/@wojtekmaj/date-utils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz", @@ -3374,7 +3664,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3954,6 +4243,26 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -3973,6 +4282,15 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3987,8 +4305,7 @@ "node_modules/blurhash": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", - "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", - "peer": true + "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==" }, "node_modules/boxen": { "version": "7.1.1", @@ -4129,7 +4446,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -4151,6 +4467,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4345,6 +4685,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camera-controls": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz", + "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001621", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", @@ -4762,6 +5111,24 @@ "node": ">=0.8" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4839,8 +5206,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "peer": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/data-view-buffer": { "version": "1.0.1", @@ -5082,6 +5448,15 @@ "url": "https://github.com/wojtekmaj/detect-element-overflow?sponsor=1" } }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -5181,6 +5556,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5480,7 +5861,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5900,6 +6280,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6458,6 +6844,12 @@ "node": ">=0.10.0" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/goober": { "version": "2.1.14", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", @@ -6513,6 +6905,12 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gsap": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", + "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -6638,6 +7036,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -6773,6 +7177,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -6815,6 +7239,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", @@ -7308,6 +7738,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -7596,6 +8032,27 @@ "set-function-name": "^2.0.1" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", @@ -8928,6 +9385,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9022,6 +9488,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -9291,6 +9767,21 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "license": "MIT" + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -11492,6 +11983,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11564,6 +12061,16 @@ "node": ">=10" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11901,7 +12408,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11956,6 +12462,18 @@ } } }, + "node_modules/react-composer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", + "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-date-picker": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-11.0.0.tgz", @@ -12003,7 +12521,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12075,9 +12592,10 @@ } }, "node_modules/react-icons": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", - "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -12224,6 +12742,31 @@ "react-dom": "^16 || ^17 || ^18" } }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -12251,7 +12794,6 @@ "version": "6.23.1", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", - "peer": true, "dependencies": { "@remix-run/router": "1.16.1", "react-router": "6.23.1" @@ -12420,6 +12962,21 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-webcam": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz", @@ -12919,7 +13476,6 @@ "version": "1.77.2", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz", "integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -13278,6 +13834,32 @@ "node": ">=8" } }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13572,6 +14154,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -13640,6 +14231,46 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/three": { + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz", + "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==", + "license": "MIT", + "peer": true + }, + "node_modules/three-mesh-bvh": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", + "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", + "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -13693,6 +14324,36 @@ "node": ">=0.10.0" } }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -13730,6 +14391,43 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14122,11 +14820,30 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -14211,7 +14928,6 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14330,6 +15046,17 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -14762,6 +15489,35 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index e5e79dc7..1d9482bf 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", "@react-oauth/google": "^0.12.1", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.18.0", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "aos": "^2.3.4", @@ -29,6 +31,7 @@ "date-fns": "^3.6.0", "dompurify": "^3.3.1", "framer-motion": "^11.2.10", + "gsap": "^3.14.2", "html2canvas": "^1.4.1", "html5-qrcode": "^2.3.8", "js-confetti": "^0.12.0", @@ -49,7 +52,7 @@ "react-dom": "^18.2.0", "react-headroom": "^3.2.1", "react-hot-toast": "^2.4.1", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-image-crop": "^11.0.6", "react-intersection-observer": "^9.10.2", "react-loader-spinner": "^6.1.6", @@ -70,6 +73,7 @@ "react-toastify": "^10.0.5", "sass": "^1.76.0", "sister": "^3.0.2", + "three": "^0.182.0", "uuid": "^10.0.0", "xlsx": "^0.18.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4a9830e..a8820a7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,12 @@ importers: '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-three/drei': + specifier: ^9.122.0 + version: 9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0))(@types/react@18.3.3)(@types/three@0.182.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0)(use-sync-external-store@1.6.0(react@18.3.1)) + '@react-three/fiber': + specifier: ^8.18.0 + version: 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0) '@vercel/analytics': specifier: ^1.3.1 version: 1.3.1(react@18.3.1) @@ -65,6 +71,9 @@ importers: framer-motion: specifier: ^11.2.10 version: 11.2.11(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + gsap: + specifier: ^3.14.2 + version: 3.14.2 html2canvas: specifier: ^1.4.1 version: 1.4.1 @@ -188,6 +197,9 @@ importers: sister: specifier: ^3.0.2 version: 3.0.2 + three: + specifier: ^0.182.0 + version: 0.182.0 uuid: specifier: ^10.0.0 version: 10.0.0 @@ -402,6 +414,10 @@ packages: resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + '@babel/template@7.24.7': resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} engines: {node: '>=6.9.0'} @@ -421,6 +437,9 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@emotion/babel-plugin@11.11.0': resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} @@ -803,6 +822,14 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + '@mui/base@5.0.0-beta.40': resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} @@ -983,6 +1010,70 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@react-spring/animated@9.7.5': + resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/core@9.7.5': + resolution: {integrity: sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/rafz@9.7.5': + resolution: {integrity: sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==} + + '@react-spring/shared@9.7.5': + resolution: {integrity: sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@react-spring/three@9.7.5': + resolution: {integrity: sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==} + peerDependencies: + '@react-three/fiber': '>=6.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + three: '>=0.126' + + '@react-spring/types@9.7.5': + resolution: {integrity: sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==} + + '@react-three/drei@9.122.0': + resolution: {integrity: sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==} + peerDependencies: + '@react-three/fiber': ^8 + react: ^18 + react-dom: ^18 + three: '>=0.137' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@8.18.0': + resolution: {integrity: sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=18 <19' + react-dom: '>=18 <19' + react-native: '>=0.64' + three: '>=0.133' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@remix-run/router@1.16.1': resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==} engines: {node: '>=14.0.0'} @@ -1112,6 +1203,9 @@ packages: resolution: {integrity: sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1127,6 +1221,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1160,6 +1257,9 @@ packages: '@types/node@20.14.6': resolution: {integrity: sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1175,6 +1275,14 @@ packages: '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-reconciler@0.26.7': + resolution: {integrity: sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==} + + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react-transition-group@4.4.10': resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} @@ -1187,9 +1295,15 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + '@types/stylis@4.2.5': resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} + '@types/three@0.182.0': + resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1199,6 +1313,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1211,6 +1328,14 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vercel/analytics@1.3.1': resolution: {integrity: sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==} peerDependencies: @@ -1251,6 +1376,9 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + '@wojtekmaj/date-utils@1.5.1': resolution: {integrity: sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==} @@ -1460,6 +1588,9 @@ packages: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcrypt@5.1.1: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} @@ -1467,6 +1598,9 @@ packages: bcryptjs@2.4.3: resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1499,6 +1633,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + cacache@16.1.3: resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -1546,6 +1683,11 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + camera-controls@2.10.1: + resolution: {integrity: sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001636: resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} @@ -1709,6 +1851,11 @@ packages: engines: {node: '>=0.8'} hasBin: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1820,6 +1967,9 @@ packages: detect-element-overflow@1.4.2: resolution: {integrity: sha512-4m6cVOtvm/GJLjo7WFkPfwXoEIIbM7GQwIh4WEa4g7IsNi1YzwUsGL5ApNLrrHL29bHeNeQ+/iZhw+YHqgE2Fw==} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -1860,6 +2010,9 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2068,6 +2221,12 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2296,6 +2455,9 @@ packages: resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==} engines: {node: '>=0.10.0'} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + goober@2.1.14: resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} peerDependencies: @@ -2321,6 +2483,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gsap@3.14.2: + resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2368,6 +2533,9 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -2421,6 +2589,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore-walk@6.0.5: resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2429,6 +2600,9 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immutable@4.3.6: resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==} @@ -2607,6 +2781,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -2682,6 +2859,11 @@ packages: iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + its-fine@1.2.5: + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} + peerDependencies: + react: '>=18.0' + jackspeak@3.4.0: resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} engines: {node: '>=14'} @@ -2928,6 +3110,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2984,6 +3169,12 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -3052,6 +3243,14 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@0.22.0: + resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3522,6 +3721,9 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3550,6 +3752,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + prompts-ncu@3.0.0: resolution: {integrity: sha512-qyz9UxZ5MlPKWVhWrCmSZ1ahm2GVYdjLb8og2sg0IPth1KRuhcggHGuijz0e41dkx35p1t1q3GRISGH7QGALFA==} engines: {node: '>= 14'} @@ -3634,6 +3839,11 @@ packages: '@types/react': optional: true + react-composer@5.0.3: + resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-date-picker@11.0.0: resolution: {integrity: sha512-l+siu5HSZ/ciGL1293KCAHl4o9aD5rw16V4tB0C43h7QbMv2dWGgj7Dxgt8iztLaPVtEfOt/+sxNiTYw4WVq6A==} peerDependencies: @@ -3756,6 +3966,12 @@ packages: react: ^16 || ^17 || ^18 react-dom: ^16 || ^17 || ^18 + react-reconciler@0.27.0: + resolution: {integrity: sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.0.0 + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -3838,6 +4054,15 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react-webcam@7.2.0: resolution: {integrity: sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==} peerDependencies: @@ -3987,6 +4212,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + scheduler@0.21.0: + resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -4131,6 +4359,15 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -4235,6 +4472,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -4256,6 +4498,20 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + three-mesh-bvh@0.7.8: + resolution: {integrity: sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==} + deprecated: Deprecated due to three.js version incompatibility. Please use v0.8.0, instead. + peerDependencies: + three: '>= 0.151.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.182.0: + resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==} + tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} @@ -4283,6 +4539,19 @@ packages: resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} engines: {node: '>=0.10.0'} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -4300,6 +4569,9 @@ packages: resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4425,9 +4697,18 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + utrie@1.0.2: resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} @@ -4486,6 +4767,12 @@ packages: warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4610,6 +4897,48 @@ packages: youtube-player@5.5.2: resolution: {integrity: sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==} + zustand@3.7.2: + resolution: {integrity: sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==} + engines: {node: '>=12.7.0'} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.10: + resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4830,6 +5159,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.6': {} + '@babel/template@7.24.7': dependencies: '@babel/code-frame': 7.24.7 @@ -4862,6 +5193,8 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@dimforge/rapier3d-compat@0.12.0': {} + '@emotion/babel-plugin@11.11.0': dependencies: '@babel/helper-module-imports': 7.24.7 @@ -5321,6 +5654,13 @@ snapshots: - encoding - supports-color + '@mediapipe/tasks-vision@0.10.17': {} + + '@monogrid/gainmap-js@3.4.0(three@0.182.0)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.182.0 + '@mui/base@5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.7 @@ -5522,6 +5862,94 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@react-spring/animated@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/core@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/rafz@9.7.5': {} + + '@react-spring/shared@9.7.5(react@18.3.1)': + dependencies: + '@react-spring/rafz': 9.7.5 + '@react-spring/types': 9.7.5 + react: 18.3.1 + + '@react-spring/three@9.7.5(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0))(react@18.3.1)(three@0.182.0)': + dependencies: + '@react-spring/animated': 9.7.5(react@18.3.1) + '@react-spring/core': 9.7.5(react@18.3.1) + '@react-spring/shared': 9.7.5(react@18.3.1) + '@react-spring/types': 9.7.5 + '@react-three/fiber': 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0) + react: 18.3.1 + three: 0.182.0 + + '@react-spring/types@9.7.5': {} + + '@react-three/drei@9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0))(@types/react@18.3.3)(@types/three@0.182.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0)(use-sync-external-store@1.6.0(react@18.3.1))': + dependencies: + '@babel/runtime': 7.28.6 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.182.0) + '@react-spring/three': 9.7.5(@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0))(react@18.3.1)(three@0.182.0) + '@react-three/fiber': 8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0) + '@use-gesture/react': 10.3.1(react@18.3.1) + camera-controls: 2.10.1(three@0.182.0) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.15 + maath: 0.10.8(@types/three@0.182.0)(three@0.182.0) + meshline: 3.3.1(three@0.182.0) + react: 18.3.1 + react-composer: 5.0.3(react@18.3.1) + stats-gl: 2.4.2(@types/three@0.182.0)(three@0.182.0) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.182.0 + three-mesh-bvh: 0.7.8(three@0.182.0) + three-stdlib: 2.36.1(three@0.182.0) + troika-three-text: 0.52.4(three@0.182.0) + tunnel-rat: 0.1.2(@types/react@18.3.3)(react@18.3.1) + utility-types: 3.11.0 + zustand: 5.0.10(@types/react@18.3.3)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + - use-sync-external-store + + '@react-three/fiber@8.18.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.182.0)': + dependencies: + '@babel/runtime': 7.28.6 + '@types/react-reconciler': 0.26.7 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 1.2.5(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-reconciler: 0.27.0(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + scheduler: 0.21.0 + suspend-react: 0.1.3(react@18.3.1) + three: 0.182.0 + zustand: 3.7.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + '@remix-run/router@1.16.1': {} '@rollup/rollup-android-arm-eabi@4.18.0': @@ -5618,6 +6046,8 @@ snapshots: '@tufjs/canonical-json': 1.0.0 minimatch: 9.0.4 + '@tweenjs/tween.js@23.1.3': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.24.7 @@ -5643,6 +6073,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/draco3d@1.4.10': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.5 @@ -5679,6 +6111,8 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/offscreencanvas@2019.7.3': {} + '@types/parse-json@4.0.2': {} '@types/prettier@2.7.3': {} @@ -5693,6 +6127,14 @@ snapshots: dependencies: '@types/react': 18.3.3 + '@types/react-reconciler@0.26.7': + dependencies: + '@types/react': 18.3.3 + + '@types/react-reconciler@0.28.9(@types/react@18.3.3)': + dependencies: + '@types/react': 18.3.3 + '@types/react-transition-group@4.4.10': dependencies: '@types/react': 18.3.3 @@ -5706,8 +6148,20 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/stats.js@0.17.4': {} + '@types/stylis@4.2.5': {} + '@types/three@0.182.0': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.69 + fflate: 0.8.2 + meshoptimizer: 0.22.0 + '@types/trusted-types@2.0.7': optional: true @@ -5715,6 +6169,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/webxr@0.5.24': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.32': @@ -5725,6 +6181,13 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@18.3.1)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 18.3.1 + '@vercel/analytics@1.3.1(react@18.3.1)': dependencies: server-only: 0.0.1 @@ -5746,6 +6209,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@webgpu/types@0.1.69': {} + '@wojtekmaj/date-utils@1.5.1': {} '@zxing/library@0.21.3': @@ -6017,6 +6482,8 @@ snapshots: base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} + bcrypt@5.1.1(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) @@ -6027,6 +6494,10 @@ snapshots: bcryptjs@2.4.3: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} blurhash@2.0.5: {} @@ -6068,6 +6539,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + cacache@16.1.3: dependencies: '@npmcli/fs': 2.1.2 @@ -6146,6 +6622,10 @@ snapshots: camelize@1.0.1: {} + camera-controls@2.10.1(three@0.182.0): + dependencies: + three: 0.182.0 + caniuse-lite@1.0.30001636: {} ccount@2.0.1: {} @@ -6293,6 +6773,10 @@ snapshots: crc-32@1.2.2: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -6399,6 +6883,10 @@ snapshots: detect-element-overflow@1.4.2: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.0.3: {} detect-newline@3.1.0: {} @@ -6436,6 +6924,8 @@ snapshots: dependencies: is-obj: 2.0.0 + draco3d@1.5.7: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6764,6 +7254,10 @@ snapshots: dependencies: bser: 2.1.1 + fflate@0.6.10: {} + + fflate@0.8.2: {} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -7024,6 +7518,8 @@ snapshots: pify: 2.3.0 pinkie-promise: 2.0.1 + glsl-noise@0.0.0: {} + goober@2.1.14(csstype@3.1.3): dependencies: csstype: 3.1.3 @@ -7054,6 +7550,8 @@ snapshots: graphemer@1.4.0: {} + gsap@3.14.2: {} + has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -7106,6 +7604,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hls.js@1.6.15: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -7164,12 +7664,16 @@ snapshots: safer-buffer: 2.1.2 optional: true + ieee754@1.2.1: {} + ignore-walk@6.0.5: dependencies: minimatch: 9.0.4 ignore@5.3.1: {} + immediate@3.0.6: {} + immutable@4.3.6: {} import-fresh@3.3.0: @@ -7319,6 +7823,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@2.2.2: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -7402,6 +7908,13 @@ snapshots: reflect.getprototypeof: 1.0.6 set-function-name: 2.0.2 + its-fine@1.2.5(@types/react@18.3.3)(react@18.3.1): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@18.3.3) + react: 18.3.1 + transitivePeerDependencies: + - '@types/react' + jackspeak@3.4.0: dependencies: '@isaacs/cliui': 8.0.2 @@ -7835,6 +8348,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lines-and-columns@1.2.4: {} load-script@1.0.0: {} @@ -7877,6 +8394,11 @@ snapshots: dependencies: react: 18.3.1 + maath@0.10.8(@types/three@0.182.0)(three@0.182.0): + dependencies: + '@types/three': 0.182.0 + three: 0.182.0 + make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -8039,6 +8561,12 @@ snapshots: merge2@1.4.1: {} + meshline@3.3.1(three@0.182.0): + dependencies: + three: 0.182.0 + + meshoptimizer@0.22.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -8622,6 +9150,8 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + potpack@1.0.2: {} + prelude-ls@1.2.1: {} pretty-format@28.1.3: @@ -8642,6 +9172,11 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + prompts-ncu@3.0.0: dependencies: kleur: 4.1.5 @@ -8746,6 +9281,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-composer@5.0.3(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-date-picker@11.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@wojtekmaj/date-utils': 1.5.1 @@ -8883,6 +9423,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-reconciler@0.27.0(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.21.0 + react-refresh@0.14.2: {} react-router-dom@6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -9001,6 +9547,12 @@ snapshots: react-dom: 18.3.1(react@18.3.1) scriptjs: 2.5.9 + react-use-measure@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-webcam@7.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -9183,6 +9735,10 @@ snapshots: immutable: 4.3.6 source-map-js: 1.2.0 + scheduler@0.21.0: + dependencies: + loose-envify: 1.4.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -9325,6 +9881,13 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stats-gl@2.4.2(@types/three@0.182.0)(three@0.182.0): + dependencies: + '@types/three': 0.182.0 + three: 0.182.0 + + stats.js@0.17.0: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -9452,6 +10015,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + tabbable@6.2.0: {} tar@6.2.1: @@ -9480,6 +10047,22 @@ snapshots: text-table@0.2.0: {} + three-mesh-bvh@0.7.8(three@0.182.0): + dependencies: + three: 0.182.0 + + three-stdlib@2.36.1(three@0.182.0): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.182.0 + + three@0.182.0: {} + tiny-warning@1.0.3: {} tinycolor2@1.6.0: {} @@ -9500,6 +10083,20 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + troika-three-text@0.52.4(three@0.182.0): + dependencies: + bidi-js: 1.0.3 + three: 0.182.0 + troika-three-utils: 0.52.4(three@0.182.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.182.0): + dependencies: + three: 0.182.0 + + troika-worker-utils@0.52.0: {} + trough@2.2.0: {} ts-custom-error@3.3.1: {} @@ -9516,6 +10113,14 @@ snapshots: transitivePeerDependencies: - supports-color + tunnel-rat@0.1.2(@types/react@18.3.3)(react@18.3.1): + dependencies: + zustand: 4.5.7(@types/react@18.3.3)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -9669,8 +10274,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} + utility-types@3.11.0: {} + utrie@1.0.2: dependencies: base64-arraybuffer: 1.0.2 @@ -9718,6 +10329,10 @@ snapshots: dependencies: loose-envify: 1.4.0 + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -9882,4 +10497,21 @@ snapshots: transitivePeerDependencies: - supports-color + zustand@3.7.2(react@18.3.1): + optionalDependencies: + react: 18.3.1 + + zustand@4.5.7(@types/react@18.3.3)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + react: 18.3.1 + + zustand@5.0.10(@types/react@18.3.3)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.3 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + zwitch@2.0.4: {} diff --git a/public/models/fox.glb b/public/models/fox.glb new file mode 100644 index 00000000..e36eac43 Binary files /dev/null and b/public/models/fox.glb differ diff --git a/src/App.jsx b/src/App.jsx index 7fae46d7..e820e89e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,12 @@ -import { Suspense, lazy, useContext, useEffect } from "react"; +import { Suspense, lazy, useContext, useEffect, useRef } from "react"; import { Routes, Route, Outlet, Navigate, useLocation } from "react-router-dom"; +import styles from "./App.module.scss"; // layouts -import { Footer, Navbar, ProfileLayout } from "./layouts"; +import { Footer, ProfileTopbar } from "./layouts"; // microInteraction -import { Loading } from "./microInteraction"; +import { Loading, Alert } from "./microInteraction"; // modals import { EventModal } from "./features"; @@ -20,6 +21,12 @@ import EventStats from "./features/Modals/Event/EventStats/EventStats"; // Chatbot import Chatbot from "./components/Chatbot/Chatbot"; +// services +import { api } from "./services"; + +// Fox Mascot - DISABLED (react-three version incompatibility) +// import FoxMascot from "./components/FoxMascot"; + import { EventsView, NewForm, @@ -39,6 +46,8 @@ const Home = lazy(() => import("./pages/Home/Home")); const Event = lazy(() => import("./pages/Event/Event")); const PastEvent = lazy(() => import("./pages/Event/PastEvent")); const EventForm = lazy(() => import("./pages/Event/EventForm")); +const EventDetailPage = lazy(() => import("./pages/Event/EventDetailPage")); +const EventRegisterPage = lazy(() => import("./pages/Event/EventRegisterPage")); const Social = lazy(() => import("./pages/Social/Social")); const Team = lazy(() => import("./pages/Team/Team")); const Alumni = lazy(() => import("./pages/Alumni/Alumni")); @@ -64,10 +73,19 @@ const OTPInput = lazy(() => import("./authentication/Login/ForgotPassword/OTPInput") ); const AttendancePage = lazy(() => import('./pages/AttendancePage/AttendancePage')); +const TeamManagement = lazy(() => import('./pages/TeamManagement/TeamManagement')); const MainLayout = () => { const location = useLocation(); const isomegaPage = location.pathname === "/omega"; + const path = location.pathname.toLowerCase(); + const hideScrollbarOnRoute = + path === "/" || + path.startsWith("/events") || + path.startsWith("/social") || + path.startsWith("/team") || + path.startsWith("/blog") || + path.startsWith("/blogs"); useEffect(() => { if (isomegaPage) { @@ -76,18 +94,30 @@ const MainLayout = () => { document.body.style.backgroundColor = ""; } + if (hideScrollbarOnRoute) { + document.body.classList.add("route-scrollbar-hidden"); + document.documentElement.classList.add("route-scrollbar-hidden"); + } else { + document.body.classList.remove("route-scrollbar-hidden"); + document.documentElement.classList.remove("route-scrollbar-hidden"); + } + return () => { document.body.style.backgroundColor = ""; + document.body.classList.remove("route-scrollbar-hidden"); + document.documentElement.classList.remove("route-scrollbar-hidden"); }; - }, [isomegaPage]); + }, [isomegaPage, hideScrollbarOnRoute]); return ( -
- -
- +
+
+ +
+ +
+
-
); }; @@ -98,15 +128,101 @@ const AuthLayout = () => (
); +// [v2] Protected route wrapper — redirects to login with return URL +const ProtectedRoute = ({ children }) => { + const authCtx = useContext(AuthContext); + const location = useLocation(); + + if (!authCtx.isLoggedIn) { + // Store full URL (path + search params) so login can redirect back + sessionStorage.setItem("prevPage", location.pathname + location.search); + Alert({ + type: "info", + message: "Please log in first to access this page.", + position: "bottom-right", + duration: 3000, + }); + return ; + } + + return children; +}; + +// [v2] Redirect after login — uses prevPage from sessionStorage if available +const LoginRedirect = () => { + const redirectTo = sessionStorage.getItem("prevPage") || "/profile"; + sessionStorage.removeItem("prevPage"); + return ; +}; + function App() { const authCtx = useContext(AuthContext); console.log(authCtx.user.access); + // [v2] Check for unseen join request updates globally on login + const hasCheckedUpdates = useRef(false); + useEffect(() => { + if (!authCtx.isLoggedIn || hasCheckedUpdates.current) return; + hasCheckedUpdates.current = true; + + const checkGlobalUpdates = async () => { + try { + const response = await api.get("/api/form/allJoinRequestUpdates"); + const updates = response.data?.data?.updates; + if (!updates || updates.length === 0) return; + + // Small delay so the page renders first + setTimeout(() => { + for (const update of updates) { + const teamLabel = update.teamName ? `"${update.teamName}"` : "a team"; + switch (update.status) { + case "ACCEPTED": + Alert({ + type: "success", + message: `🎉 Your request to join ${teamLabel} was accepted!`, + position: "top-right", + duration: 5000, + }); + break; + case "REJECTED": + Alert({ + type: "error", + message: `Your request to join ${teamLabel} was declined by the team leader.`, + position: "top-right", + duration: 6000, + }); + break; + case "AUTO_EXPIRED": + case "EXPIRED": + Alert({ + type: "info", + message: `Your request to join ${teamLabel} has expired.`, + position: "top-right", + duration: 4000, + }); + break; + default: + break; + } + } + }, 1500); + } catch (err) { + // Silent — don't break app startup + console.error("Error checking global join request updates:", err); + } + }; + + checkGlobalUpdates(); + }, [authCtx.isLoggedIn]); + return (
{/* Global Chatbot Component */} + {/* Fox Mascot Animation - DISABLED */} + {/* */} + }> }> @@ -121,80 +237,6 @@ function App() { } /> } /> {/* } /> */} - {/* Route After Login */} - {authCtx.isLoggedIn && ( - }> - } - /> - {authCtx.user.access === "ADMIN" ? ( - } /> - ) : ( - <> - } /> - } /> - - )} - } /> - - {authCtx.user.access === "ADMIN" && ( - } /> - )} - - {/* blog access to this mail*/} - - {(authCtx.user.access === "ADMIN" || - authCtx.user.access === "SENIOR_EXECUTIVE_CREATIVE") && ( - } /> - )} - {/* Certificates Route */} - - {authCtx.user.access === "ADMIN" && ( - } /> - )} - - {authCtx.user.access === "ADMIN" && ( - } - /> - )} - - {authCtx.user.access === "ADMIN" && ( - } - /> - )} - - {authCtx.user.access === "ADMIN" && ( - } - /> - )} - - ]} - /> - {authCtx.user.access !== "USER" && ( - ]} - /> - )} - {authCtx.user.access === "USER" && - authCtx.user.email == "attendance@fedkiit.com" && ( - ]} - /> - )} - } /> - - )} , ]} @@ -213,7 +255,29 @@ function App() { , ]} + element={ + + + + + } + /> + + + + + } + /> + } + /> + } /> } /> @@ -224,6 +288,74 @@ function App() { } /> + {authCtx.isLoggedIn && ( + }> + } /> + {authCtx.user.access === "ADMIN" ? ( + } /> + ) : ( + <> + } /> + } /> + + )} + } /> + + {authCtx.user.access === "ADMIN" && ( + } /> + )} + + {(authCtx.user.access === "ADMIN" || + authCtx.user.access === "SENIOR_EXECUTIVE_CREATIVE") && ( + } /> + )} + + {authCtx.user.access === "ADMIN" && ( + } /> + )} + + {authCtx.user.access === "ADMIN" && ( + } + /> + )} + + {authCtx.user.access === "ADMIN" && ( + } + /> + )} + + {authCtx.user.access === "ADMIN" && ( + } + /> + )} + + ]} + /> + {authCtx.user.access !== "USER" && ( + ]} + /> + )} + {authCtx.user.access === "USER" && + authCtx.user.email == "attendance@fedkiit.com" && ( + ]} + /> + )} + } /> + + )} + {/* Routes for Authentication witout Navbar and footer */} }> {!authCtx.isLoggedIn && ( @@ -232,13 +364,13 @@ function App() { : + authCtx.isLoggedIn ? : } /> : + authCtx.isLoggedIn ? : } /> setCodeResponse(codeResponse), - onError: (error) => console.error("Login failed:", error), - }); - - useEffect(() => { - if (codeResponse) { - handleLoginSuccess(); - } - }, [codeResponse]); - - useEffect(() => { - if (alert) { - const { type, message, position, duration } = alert; - Alert({ type, message, position, duration }); - setAlert(null); - } - }, [alert]); - - useEffect(() => { - if (shouldNavigate) { - navigate(navigatePath); - setShouldNavigate(false); - } - }, [shouldNavigate, navigatePath, navigate]); - - const handleLoginSuccess = async () => { - setIsLoading(true); - try { - try { - const response = await api.post("/api/auth/googleAuth", { - access_token: codeResponse.access_token, - }); - - if (response.status === 200 || response.status === 201) { - // User exists in the backend - // console.log(response); - const user = response.data.user; - - setAlert({ - type: "success", - message: "Login successful", - position: "bottom-right", - duration: 2800, - }); - - setNavigatePath(sessionStorage.getItem("prevPage") || "/"); - - setTimeout(() => { - setShouldNavigate(true); - }); - - setTimeout(() => { - localStorage.setItem("token",response.data.token); - authCtx.login( - user.name, - user.email, - user.img, - user.rollNumber, - user.school, - user.college, - user.contactNo, - user.year, - user.extra?.github, - user.extra?.linkedin, - user.extra?.designation, - user.access, - user.editProfileCount, - user.regForm, - user.blurhash, - response.data.token, - 9600000 - ); - }, 800); - - sessionStorage.removeItem("prevPage"); // Clean up - } else { - - // console.log("Unexpected backend response status:", response.status); - // handleFallbackOrSignup(googleUserData); - } - } catch (error) { - setAlert({ - type: "error", - message: "There was an error logging in. Please try again.", - position: "bottom-right", - duration: 3000, - }); - - console.error("Backend API call failed:", error); - - } - } catch (error) { - console.error("Login error:", error); - setAlert({ - type: "error", - message: "There was an error logging in. Please try again.", - position: "bottom-right", - duration: 3000, - }); - } finally { - setIsLoading(false); - } - }; - - return ( - <> - - - - ); -} diff --git a/src/authentication/Login/Login.jsx b/src/authentication/Login/Login.jsx index c9228cc8..40c98f5c 100644 --- a/src/authentication/Login/Login.jsx +++ b/src/authentication/Login/Login.jsx @@ -1,28 +1,30 @@ /* eslint-disable react/no-unescaped-entities */ -/* eslint-disable no-unused-vars */ import React, { useState, useContext, useEffect } from "react"; import { useNavigate, Link } from "react-router-dom"; +import { useGoogleLogin } from "@react-oauth/google"; import style from "./styles/Login.module.scss"; import Input from "../../components/Core/Input"; import Button from "../../components/Core/Button"; import Text from "../../components/Core/Text"; -import users from "../../data/user.json"; import { api } from "../../services"; import AuthContext from "../../context/AuthContext"; import { RecoveryContext } from "../../context/RecoveryContext"; -import GoogleLogin from "./GoogleLogin"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import MailOutlineIcon from "@mui/icons-material/MailOutline"; +import GoogleIcon from "@mui/icons-material/Google"; import { Alert, MicroLoading } from "../../microInteraction"; +import heroBgImage from "../../assets/images/herobgimage.png"; const Login = () => { const navigate = useNavigate(); const { setEmail } = useContext(RecoveryContext); const authCtx = useContext(AuthContext); + + const [mode, setMode] = useState("select"); const [alert, setAlert] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [shouldNavigate, setShouldNavigate] = useState(false); - const [navigatePath, setNavigatePath] = useState("/"); - const [email, setemail] = useState(""); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [email, setEmailState] = useState(""); const [password, setPassword] = useState(""); useEffect(() => { @@ -30,102 +32,144 @@ const Login = () => { }, []); useEffect(() => { - if (alert) { - const { type, message, position, duration } = alert; - Alert({ type, message, position, duration }); - } + if (alert) Alert(alert); }, [alert]); - useEffect(() => { - if (shouldNavigate) { - navigate(navigatePath); - setShouldNavigate(false); + /* ---------------- GOOGLE LOGIN ---------------- */ + + const handleGoogleLogin = async (tokenResponse) => { + setIsGoogleLoading(true); + + try { + const response = await api.post("/api/auth/googleAuth", { + access_token: tokenResponse.access_token, + }); + + const user = response.data.user; + + localStorage.setItem("token", response.data.token); + + authCtx.login( + user.name, + user.email, + user.img, + user.rollNumber, + user.school, + user.college, + user.contactNo, + user.year, + user.extra?.github, + user.extra?.linkedin, + user.extra?.designation, + user.access, + user.editProfileCount, + user.regForm, + user.blurhash, + response.data.token, + 9600000 + ); + + setAlert({ + type: "success", + message: "Login successful", + position: "bottom-right", + duration: 2400, + }); + + const nextPath = sessionStorage.getItem("prevPage") || "/"; + sessionStorage.removeItem("prevPage"); + + setTimeout(() => navigate(nextPath), 600); + } catch (error) { + setAlert({ + type: "error", + message: "There was an error logging in. Please try again.", + position: "bottom-right", + duration: 2500, + }); + } finally { + setIsGoogleLoading(false); } - }, [shouldNavigate, navigatePath, navigate]); + }; - const handleLogin = async (event) => { - event.preventDefault(); - setIsLoading(true); + const loginWithGoogle = useGoogleLogin({ + onSuccess: handleGoogleLogin, + onError: () => { + setAlert({ + type: "error", + message: "Google login failed. Please try again.", + position: "bottom-right", + duration: 2500, + }); + }, + }); - if (email === "" || password === "") { + /* ---------------- EMAIL LOGIN ---------------- */ + + const handleLogin = async (e) => { + e.preventDefault(); + + if (!email || !password) { setAlert({ type: "error", - message: "Please fill all the fields", + message: "Fill all fields", position: "bottom-right", - duration: 3000, + duration: 2500, }); - setIsLoading(false); return; } + setIsLoading(true); + try { - const response = await api.post("/api/auth/login", { + const res = await api.post("/api/auth/login", { email: email.toLowerCase(), password, }); - // console.log("incoming response", response.data); - - if (response.status === 200 || response.status === 201) { - const user = response.data.user; - // console.log("user is ", user); - - setAlert({ - type: "success", - message: "Login successful", - position: "bottom-right", - duration: 2800, - }); - - setNavigatePath(sessionStorage.getItem("prevPage") || "/"); - - setTimeout(() => { - setShouldNavigate(true); - }, 750); - - setTimeout(() => { - localStorage.setItem("token", response.data.token); - authCtx.login( - user.name, - user.email, - user.img, - user.rollNumber, - user.school, - user.college, - user.contactNo, - user.year, - user.extra?.github, - user.extra?.linkedin, - user.extra?.designation, - user.access, - user.editProfileCount, - user.regForm, - user.blurhash, - response.data.token, - 9600000 - ); - }, 800); - // console.log(authCtx); - - sessionStorage.removeItem("prevPage"); - } else { - setAlert({ - type: "error", - message: response.data.message || "Invalid email or password", - position: "bottom-right", - duration: 3000, - }); - } - } catch (error) { + const user = res.data.user; + + localStorage.setItem("token", res.data.token); + + authCtx.login( + user.name, + user.email, + user.img, + user.rollNumber, + user.school, + user.college, + user.contactNo, + user.year, + user.extra?.github, + user.extra?.linkedin, + user.extra?.designation, + user.access, + user.editProfileCount, + user.regForm, + user.blurhash, + res.data.token, + 9600000 + ); + + setAlert({ + type: "success", + message: "Login successful", + position: "bottom-right", + duration: 2000, + }); + + const nextPath = sessionStorage.getItem("prevPage") || "/"; + sessionStorage.removeItem("prevPage"); + + setTimeout(() => navigate(nextPath), 600); + } catch (err) { setAlert({ type: "error", message: - error?.response?.data?.message || - "There was an error logging in. Please try again.", + err?.response?.data?.message || "Invalid email or password", position: "bottom-right", - duration: 3000, + duration: 2500, }); - console.error("Error logging in:", error?.response?.data?.message); } finally { setIsLoading(false); } @@ -136,135 +180,116 @@ const Login = () => { navigate("/ForgotPassword"); }; + /* ---------------- UI ---------------- */ + return ( -
-
- -
- -
- -
-
-
-
-
-

- Login -

- -
-
-

- or -

-
-
-
- setemail(e.target.value)} - required - style={{ - width: "98%", - }} - /> - setPassword(e.target.value)} - required - style={{ - width: "98%", - }} - /> - - Forget Password? - - - + ) : ( + <> + + Continue with Google + + )} + + +
+ + OR + +
+ + + + + Don't have an account{" "} + Sign Up - -
+
+ )} + + {/* -------- EMAIL MODE -------- */} + {mode === "email" && ( +
+ + +
+ setEmailState(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + /> + +
+ + Forgot password? + +
+ + +
+
+ )}
+
); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/authentication/Login/styles/Login.module.scss b/src/authentication/Login/styles/Login.module.scss index 182b8156..82c2ab0f 100644 --- a/src/authentication/Login/styles/Login.module.scss +++ b/src/authentication/Login/styles/Login.module.scss @@ -1,195 +1,209 @@ -.container { - background-color: #2f2f2f90; - border: 1px solid #ffffff61; - border-radius: 10px; - background: linear-gradient(90deg, rgba(32, 32, 32, 0.16) -11.52%, rgba(37, 37, 37, 0.22) 106.29%); - padding: 15px; +.authShell { + min-height: 100vh; + display: flex; justify-content: center; align-items: center; - width: 30rem; - display: flex; - flex-direction: row; - margin-top: 20%; + padding: 64px 20px; + background: + var(--auth-bg-image), + } -.form{ - width: auto; - padding-left: 0; - padding-right: 0; + +.backBtn { + position: fixed; + top: 20px; + left: 20px; + color: #fff; + z-index: 10; } -.container:hover{ - border: 1px solid #ffffff70; - box-shadow: 2px 3px #94949470; +/* CARD */ +.card { + width: 100%; + max-width: 420px; + padding: 40px 32px; + border-radius: 22px; + background: linear-gradient(160deg, rgba(19, 24, 35, 0.96), rgba(12, 16, 24, 0.95)); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(18px); + box-shadow: 0 25px 60px rgba(0, 0, 0, 0.6); + text-align: center; } -.container h1 { - color: #ff6b00; - font-size: 1.3rem; - margin-top: -1rem; + +/* TYPOGRAPHY */ +.title { + font-size: 1.8rem; + font-weight: 700; + color: #fff; + margin-bottom: 6px; + letter-spacing: -0.4px; } -.login { - width: 100%; - height: 26rem; + +.subtitle { + font-size: 0.88rem; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 28px; +} + +/* OPTIONS */ +.options { display: flex; flex-direction: column; - justify-content: center; - // margin-right: 3%; + gap: 16px; } -.sideImage { + +.socialButton, +.emailOption { width: 100%; + height: 48px; + border-radius: 14px; + font-size: 0.9rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.18s ease; +} + +/* SOCIAL */ +.socialButton { + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.04); + color: #fff; + gap: 8px; +} + + - height: 32rem; - object-fit: cover; - border-radius: 10px; - background-color: #949494; - width: 28rem; - margin-left: auto; -} - -.circle { - height: 25rem; - width: 25rem; - background-color: rgba(255, 92, 0, 0.19); - filter: blur(100px); - position: absolute; - top: 20%; - left: -12%; - border-radius: 50%; - overflow: hidden; - z-index: 0; -} - -.circle1 { - height: 25rem; - width: 25rem; - background-color: rgba(255, 92, 0, 0.19); - position: absolute; - - top: 5%; - right: -5%; - border-radius: 50%; - filter: blur(100px); - // overflow: hidden; -} - -.ArrowBackIcon { - position: absolute; - top: 5%; - left: 30px; +/* EMAIL CTA */ +.emailOption { + gap: 8px; + border: none; + background: linear-gradient(135deg, #ff8a3d, #ff5f1f); color: #fff; + gap:8x; cursor: pointer; } -.divider{ - height: 2px; - width: 5rem; - border-radius: 15px 15px; - background-color: #7c7a7a90; + +.socialIcon, +.mailIcon { + font-size: 18px !important; } -@media (max-width: 800px) { +/* DIVIDER */ +.dividerRow { + display: flex; + align-items: center; + gap: 10px; + margin: 2px 0; +} - .sideImage { - display: none; - } - .login { - width: 100%; - } +.line { + flex: 1; + height: 1px; + background: rgba(255, 255, 255, 0.1); +} - .circle { - height: 18rem; - width: 18rem; - top: 30%; - left: -15%; - z-index: 0; - } +.or { + font-size: 0.68rem; + color: rgba(255, 255, 255, 0.45); + letter-spacing: 1px; +} - .circle1 { - height: 18rem; - width: 18rem; - top: 5%; - right: -15%; +/* FORM WRAPPER */ +.emailFormWrapper { + margin-top: 6px; + text-align: left; +} - // overflow: hidden; - } +/* SWITCH BACK */ +.switchBack { + display: inline-flex; + align-items: center; + gap: 6px; + background: none; + border: none; + color: #ffc48b; + font-weight: 600; + margin-bottom: 18px; + padding: 0; + cursor: pointer; + transition: opacity 0.18s ease; } -@media (max-width: 1100px) { - .container { - width: 28rem; - } - .login { - width: 100%; - } +.backIcon { + font-size: 16px !important; +} - .circle { - height: 22rem; - width: 22rem; - top: 30%; - left: -15%; - z-index: 0; - } +/* FORM */ +.form { + display: flex; + flex-direction: column; + gap: 20px; +} - .circle1 { - height: 20rem; - width: 20rem; - top: 5%; - right: -15%; +/* FOOTER */ +.formFooter { + display: flex; + justify-content: flex-end; + margin-top: -4px; +} - // overflow: hidden; - } +.forgot { + font-size: 0.75rem; + color: #ffc48b; + cursor: pointer; } -@media (max-width: 800px) { - .circle { - height: 15rem; - width: 8rem; - top: 30%; - left: 0%; - z-index: 0; - } +/* SUBMIT */ +.submitBtn { + width: 100%; + height: 48px; + border-radius: 999px; + background: linear-gradient(140deg, #ff8a3d, #ff5f1f); + color: #fff !important; + font-weight: 600; + margin-top: 6px; + transition: all 0.18s ease; +} - .circle1 { - height: 15rem; - width: 10rem; - top: 10%; - right: -10%; - // overflow: hidden; - } +/* BOTTOM TEXT */ +.bottomText { + margin-top: 18px; + font-size: 0.8rem; + text-align: center; + color: rgba(255, 255, 255, 0.6); +} + +.linkAccent { + color: #ffbd84; + font-weight: 600; + text-decoration: none; } -@media (max-width: 500px) { - .container { - flex-direction: column; - width: 90vw; - margin-top: 40%; - } - .sideImage { - display: none; - } - .login { - width: 100%; - } - .circle { - height: 15rem; - width: 8rem; - top: 30%; - left: 0%; - z-index: 0; +.linkAccent:hover { + text-decoration: underline; +} + +/* MOBILE */ +@media (max-width: 480px) { + .authShell { + padding: 40px 16px; } - .circle1 { - height: 15rem; - width: 10rem; - top: 10%; - right: -0%; + .card { + padding: 32px 20px; + border-radius: 18px; + } - // overflow: hidden; + .title { + font-size: 1.5rem; } - .ArrowBackIcon { - top: 2%; - left:4% + .subtitle { + margin-bottom: 22px; } } diff --git a/src/authentication/SignUp/GoogleSignup.jsx b/src/authentication/SignUp/GoogleSignup.jsx deleted file mode 100644 index 68814a76..00000000 --- a/src/authentication/SignUp/GoogleSignup.jsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useState, useEffect, useContext } from "react"; -import { useNavigate } from "react-router-dom"; -import styles from "./style/Signup.module.scss"; - -import { useGoogleLogin } from "@react-oauth/google"; -import axios from "axios"; -import AuthContext from "../../context/AuthContext"; -import google from "../../assets/images/google.png"; -import users from "../../data/user.json"; -import { Alert, MicroLoading } from "../../microInteraction"; -import { api } from "../../services"; - -export default function GoogleSignup({ setAlert }) { - // const [alert, setAlert] = useState(null); - const [codeResponse, setCodeResponse] = useState(null); - const [shouldNavigate, setShouldNavigate] = useState(false); - const [navigatePath, setNavigatePath] = useState("/"); - const authCtx = useContext(AuthContext); - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); - - const signUp = useGoogleLogin({ - onSuccess: (codeResponse) => setCodeResponse(codeResponse), - onError: (error) => console.error("SignUp failed:", error), - }); - - useEffect(() => { - if (codeResponse) { - handleSignUpSuccess(); - } - }, [codeResponse]); - - - - useEffect(() => { - if (shouldNavigate) { - navigate(navigatePath); - setShouldNavigate(false); // Reset state after navigation - } - }, [shouldNavigate, navigatePath, navigate]); - - const handleSignUpSuccess = async () => { - setIsLoading(true); - try { - - - // console.log("Google User Data:", googleUserData); - - try { - const response = await api.post("/api/auth/googleAuth", { - access_token: codeResponse.access_token, - }); - - if (response.status === 200 || response.status === 201) { - // User exists in the backend - const user = response.data.user; - - setAlert({ - type: "success", - message: response.status === 200 ?"User Already Registered! Logged In successfully":"User Registered! Logged In successfully", - position: "bottom-right", - duration: 3000, - }); - setNavigatePath("/"); - sessionStorage.removeItem("prevPage"); // Clean up - - setTimeout(() => { - localStorage.setItem("token",response.data.token); - authCtx.login( - user.name, - user.email, - user.img, - user.rollNumber, - user.school, - user.college, - user.contactNo, - user.year, - user.extra?.github, - user.extra?.linkedin, - user.extra?.designation, - user.access, - user.editProfileCount, - user.regForm, - user.blurhash, - response.data.token, - 9600000 - ); - }, 800); - } else { - // Handle unexpected response status - console.log("Unexpected backend response status:", response.status); - // handleFallbackOrCompleteProfile(googleUserData, googleResponse); - } - } catch (error) { - // API call error, fallback to local data - console.error("Backend API call failed:", error); - } - } catch (error) { - console.error("SignUp error:", error); - setAlert({ - type: "error", - message: "There was an error Signing Up. Please try again.", - position: "bottom-right", - duration: 3000, - }); - } finally { - setIsLoading(false); - } - }; - - - return ( - <> - - - ); -} diff --git a/src/authentication/SignUp/SignUP.jsx b/src/authentication/SignUp/SignUP.jsx index c2c34296..7ce99cf0 100644 --- a/src/authentication/SignUp/SignUP.jsx +++ b/src/authentication/SignUp/SignUP.jsx @@ -1,32 +1,32 @@ /* eslint-disable no-unused-vars */ /* eslint-disable no-dupe-keys */ /* eslint-disable react/no-unescaped-entities */ -import { useContext, useState } from "react"; +import { useContext, useState, useEffect } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useGoogleLogin } from "@react-oauth/google"; import styles from "./style/Signup.module.scss"; import { Input, Button, Text } from "../../components"; -import GoogleSignup from "./GoogleSignup"; import bcrypt from "bcryptjs"; -import { useEffect } from "react"; -import { Link, useNavigate } from "react-router-dom"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import OtpInputModal from "../../features/Modals/authentication/OtpInputModal"; +import MailOutlineIcon from "@mui/icons-material/MailOutline"; import { Alert, MicroLoading } from "../../microInteraction"; import { RecoveryContext } from "../../context/RecoveryContext"; import { api } from "../../services"; import AuthContext from "../../context/AuthContext"; -// import { validateData } from "../../utils/hooks/validation/validateSignupData"; +import heroBgImage from "../../assets/images/herobgimage.png"; +import GoogleIcon from "@mui/icons-material/Google"; const SignUp = () => { const navigate = useNavigate(); const [showModal, setShowModal] = useState(false); const [userObject, setUserObject] = useState({}); const { setEmail } = useContext(RecoveryContext); - const [OTP, setOTP] = useState(""); const [isTandChecked, setTandC] = useState(false); const [alert, setAlert] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [shouldNavigate, setShouldNavigate] = useState(false); - const [navigatePath, setNavigatePath] = useState("/"); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + const [mode, setMode] = useState("select"); const [showDropdown, setShowDropdown] = useState(false); const authCtx = useContext(AuthContext); @@ -50,16 +50,75 @@ const SignUp = () => { if (alert) { const { type, message, position, duration } = alert; Alert({ type, message, position, duration }); - setAlert(null); // Reset alert after displaying it + setAlert(null); } }, [alert]); - useEffect(() => { - if (shouldNavigate) { - navigate(navigatePath); - setShouldNavigate(false); // Reset state after navigation + const handleGoogleSignup = async (tokenResponse) => { + setIsGoogleLoading(true); + + try { + const response = await api.post("/api/auth/googleAuth", { + access_token: tokenResponse.access_token, + }); + + const user = response.data.user; + + localStorage.setItem("token", response.data.token); + authCtx.login( + user.name, + user.email, + user.img, + user.rollNumber, + user.school, + user.college, + user.contactNo, + user.year, + user.extra?.github, + user.extra?.linkedin, + user.extra?.designation, + user.access, + user.editProfileCount, + user.regForm, + user.blurhash, + response.data.token, + 9600000 + ); + + setAlert({ + type: "success", + message: + response.status === 200 + ? "User already registered. Logged in successfully" + : "User registered. Logged in successfully", + position: "bottom-right", + duration: 2800, + }); + + setTimeout(() => navigate("/"), 600); + } catch (error) { + setAlert({ + type: "error", + message: "There was an error signing up with Google.", + position: "bottom-right", + duration: 2800, + }); + } finally { + setIsGoogleLoading(false); } - }, [shouldNavigate, navigatePath, navigate]); + }; + + const signupWithGoogle = useGoogleLogin({ + onSuccess: handleGoogleSignup, + onError: () => { + setAlert({ + type: "error", + message: "Google signup failed. Please try again.", + position: "bottom-right", + duration: 2500, + }); + }, + }); const DataInp = (name, value) => { if (name === "college" && value.toLowerCase().startsWith("k")) { @@ -67,6 +126,7 @@ const SignUp = () => { } else { setShowDropdown(false); } + setUser({ ...showUser, [name]: value }); }; @@ -78,49 +138,25 @@ const SignUp = () => { setShowDropdown(false); }; - const validateData = (data, isTandChecked) => { + const validateData = (data, acceptedTerms) => { const errors = []; - if (!data.FirstName) { - errors.push("First Name is required."); - } - - if (!data.LastName) { - errors.push("Last Name is required."); - } + if (!data.FirstName) errors.push("First Name is required."); + if (!data.LastName) errors.push("Last Name is required."); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(data.email)) { - errors.push("Enter a valid email address."); - } + if (!emailRegex.test(data.email)) errors.push("Enter a valid email address."); if (data.contactNo.length < 10 || data.contactNo.length > 12) { errors.push("Contact Number must be between 10 and 12 digits."); } - if (!data.college) { - errors.push("College is required."); - } - - if (!data.school) { - errors.push("School is required."); - } - - if (!data.rollNumber) { - errors.push("Roll Number is required."); - } - - if (!data.year) { - errors.push("Year is required."); - } - - if (!data.Password) { - errors.push("Password is required."); - } - - if (!isTandChecked) { - errors.push("Please check the terms and conditions."); - } + if (!data.college) errors.push("College is required."); + if (!data.school) errors.push("School is required."); + if (!data.rollNumber) errors.push("Roll Number is required."); + if (!data.year) errors.push("Year is required."); + if (!data.Password) errors.push("Password is required."); + if (!acceptedTerms) errors.push("Please check the terms and conditions."); return errors; }; @@ -154,7 +190,7 @@ const SignUp = () => { year, } = showUser; - const name = FirstName + " " + LastName; + const name = `${FirstName} ${LastName}`; const saltRounds = parseInt(import.meta.env.VITE_BCRYPT, 10); const password = bcrypt.hashSync(Password, saltRounds); const user = { @@ -171,14 +207,15 @@ const SignUp = () => { setUserObject(user); try { - setAlert({ type: "info", message: "Verifying Email...", position: "bottom-right", duration: 3000, }); + setEmail(user.email); + const response = await api.post( "/api/auth/verifyEmail", { email: user.email }, @@ -186,14 +223,13 @@ const SignUp = () => { ); if (response.status === 200 || response.status === 201) { - setTimeout(() => { setShowModal(true); }, 3000); setAlert({ type: "success", - message: response.data.message || "Otp Sent to Email", + message: response.data.message || "Otp sent to email", position: "bottom-right", duration: 3000, }); @@ -220,59 +256,57 @@ const SignUp = () => { }; const handleVerifyOTP = async (enteredOTP) => { - if (enteredOTP) { - try { - const response = await api.post("/api/auth/register", { - ...userObject, - otp: enteredOTP, - }); + if (!enteredOTP) { + setAlert({ + type: "error", + message: "Validation Failed! Enter Valid OTP", + position: "bottom-right", + duration: 3000, + }); + return; + } - if (response.status == 200 || response.status == 201) { - // setLoad(false); - localStorage.setItem("token",response.data.token); - // console.log(response); - authCtx.login( - response.data.user.name, - response.data.user.email, - response.data.user.img, - response.data.user.rollNumber, - response.data.user.school, - response.data.user.college, - response.data.user.contactNo, - response.data.user.year, - response.data.user.extra?.github, - response.data.user.extra?.linkedin, - response.data.user.extra?.designation, - response.data.user.access, - response.data.user.editProfileCount, - response.data.user.regForm, - response.data.user.blurhash, - response.data.token, - 10800000 - ); - // console.log(authCtx); - navigate("/"); - } - } catch (error) { - setAlert({ - type: "error", - message: error?.response?.data?.message || "Enter valid Details", - position: "bottom-right", - duration: 3000, - }); - } + try { + const response = await api.post("/api/auth/register", { + ...userObject, + otp: enteredOTP, + }); - // Handle successful verification - } else { + if (response.status === 200 || response.status === 201) { + localStorage.setItem("token", response.data.token); + + authCtx.login( + response.data.user.name, + response.data.user.email, + response.data.user.img, + response.data.user.rollNumber, + response.data.user.school, + response.data.user.college, + response.data.user.contactNo, + response.data.user.year, + response.data.user.extra?.github, + response.data.user.extra?.linkedin, + response.data.user.extra?.designation, + response.data.user.access, + response.data.user.editProfileCount, + response.data.user.regForm, + response.data.user.blurhash, + response.data.token, + 10800000 + ); + + navigate("/"); + } + } catch (error) { setAlert({ type: "error", - message: "Validation Failed! Enter Valid OTP", + message: error?.response?.data?.message || "Enter valid details", position: "bottom-right", duration: 3000, }); - // console.log("Enter valid Otp"); } }; + const handleModalClose = () => { setShowModal(false); }; @@ -281,281 +315,241 @@ const SignUp = () => { setTandC((prevState) => !prevState); }; - // console.log(alert) - return ( <> -
- -
- -
+
+ + -
- -
-
-
-

- SignUp -

- -
-
-

or

-
-
-
-
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - className={styles.input} - /> -
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - className={styles.input} - /> -
+
+

Sign Up

+

Create your account to continue

+ + {mode === "select" && ( +
+ + +
+
+

OR

+
-
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - /> + + + + + Already have an account?{" "} + + Login + + +
+ )} + + {mode === "email" && ( + <> + + + +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> +
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - className={styles.input} - /> + +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> +
-
-
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - /> - {showDropdown && ( -
-
- Kalinga Institute of Industrial Technology + +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> + {showDropdown && ( +
+
+ Kalinga Institute of Industrial Technology +
-
- )} + )} +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> +
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - /> -
-
-
-
- DataInp("year", value)} - required - style={{ width: "96%" }} - className={styles.input} - /> + +
+
+ DataInp("year", value)} + required + style={{ width: "100%" }} + /> +
+
+ DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> +
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "96%" }} - className={styles.input} + + DataInp(e.target.name, e.target.value)} + required + style={{ width: "100%" }} + /> + +
+ + + + Agree to FED's + + Terms and Conditions + + And + + FED's Privacy Policy. + +
-
- DataInp(e.target.name, e.target.value)} - required - style={{ width: "98%" }} - className={styles.input} - /> - -
- - - Agree to FED's - - Terms and Conditions - - And - - FED's Privacy Policy. + {isLoading ? : "Sign Up"} + + + + Already have an account?{" "} + + Login -
- - - - Already Have an account?{" "} - - Login - - - -
-
+ + + )}
@@ -571,3 +565,4 @@ const SignUp = () => { }; export default SignUp; + diff --git a/src/authentication/SignUp/style/Signup.module.scss b/src/authentication/SignUp/style/Signup.module.scss index d459293e..1c53ba47 100644 --- a/src/authentication/SignUp/style/Signup.module.scss +++ b/src/authentication/SignUp/style/Signup.module.scss @@ -1,254 +1,231 @@ -.container { - background-color: #2f2f2f90; - border: 1px solid #ffffff61; - border-radius: 10px; - background: linear-gradient(90deg, rgba(32, 32, 32, 0.32) -11.52%, rgba(37, 37, 37, 0.43) 106.29%); - padding: 20px; - width: 30rem; +.authShell { + min-height: 100vh; display: flex; - flex-direction: row; - margin: auto; - margin-top: -20px; - scale: 0.85; + justify-content: center; + align-items: center; + padding: 64px 20px; + background: + var(--auth-bg-image), + +} +.backBtn { + position: fixed; + top: 18px; + left: 18px; + color: #fff; + z-index: 10; } - - - -.container:hover { - border: 1px solid #ffffff70; - box-shadow: 2px 3px #94949470; +/* CARD */ +.card { + width: 100%; + max-width: 640px; + padding: 32px 28px; + border-radius: 20px; + background: linear-gradient(160deg, rgba(19, 24, 35, 0.95), rgba(12, 16, 24, 0.92)); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(14px); + box-shadow: 0 20px 48px rgba(0, 0, 0, 0.55); +} + +/* TYPOGRAPHY */ +.title { + font-size: 1.85rem; + font-weight: 700; + color: #fff; + margin: 0; } -.container h1 { - font-size: 1.5rem; - margin-top: -0.5rem; +.subtitle { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.65); + margin: 6px 0 18px; } -.signin { - width: 35vw; + +/* OPTIONS */ +.options { display: flex; flex-direction: column; - margin-inline: auto; -} - -.signin h1{ - padding-top: 10px; - background: var(--primary); - width: 20%; - -webkit-background-clip: text; - color: transparent; + gap: 14px; } -.sideImage { +.socialButton, +.emailOption { width: 100%; - height: auto; - object-fit: cover; - border-radius: 10px; - background-color: #949494; - width: 28rem; - // height: 37rem; - margin-left: auto; - display: none; -} - -.circle { - height: 25rem; - width: 25rem; - background-color: rgba(255, 92, 0, 0.19); - filter: blur(100px); - position: absolute; - top: 20%; - left: -12%; - border-radius: 50%; - overflow: hidden; - z-index: 0; + height: 48px; + border-radius: 12px; + font-size: 0.9rem; + font-weight: 600; + display: flex; + gap:8x; + align-items: center; + justify-content: center; } - -.circle1 { - height: 25rem; - width: 25rem; - background-color: rgba(255, 92, 0, 0.19); - position: absolute; - - top: 5%; - right: -5%; - border-radius: 50%; - filter: blur(100px); +.socialButton { + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.04); + color: #fff; + gap: 8px; /* space between icon and text */ } -.ArrowBackIcon { - position: absolute; - top: 5%; - left: 30px; +.emailOption { + gap: 8px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(135deg, #ff8a3d, #ff5f1f); color: #fff; cursor: pointer; } -.google_btn { - border-radius: 0.34225rem; - border: 1.095px solid var(--primary-white, #ffffff50); - font-size: 1rem; +.socialIcon, +.mailIcon { + font-size: 18px !important; } -.google_btn:hover { - border: 1px solid #fff; +/* DIVIDER */ +.dividerRow { + display: flex; + align-items: center; + gap: 10px; } -.dropdown { - position: relative; - width: 96%; - margin-top: 5px; - background-color: white; - border: 1px solid #ccc; - color: black; - border-radius: 4px; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); - z-index: 2; - margin-left: 5px; +.divider { + flex: 1; + height: 1px; + background: rgba(255, 255, 255, 0.12); } -.dropdownItem { - padding: 10px; - cursor: pointer; - transition: 0.3s ease-in-out; +.dividerLabel { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.5); + letter-spacing: 1px; + margin: 0; } -.dropdownItem:hover { - background-color: #202020; - color:#f97507; +/* SWITCH BACK */ +.switchBack { + background: none; + border: none; + color: #ffc48b; + font-weight: 600; + margin-bottom: 10px; + padding: 0; } +/* FORM */ +.form { + display: flex; + flex-direction: column; + gap: 14px; +} +.formRow { + display: flex; + gap: 12px; +} - - -@media (max-width: 600px) { - .container { - width: 28rem; - margin-top: 13%; - margin-bottom: 5%; - } - - .signin { - width: 100%; - } - .sideImage { - display: none; - } - .circle { - height: 20rem; - width: 20rem; - top: 30%; - left: 0%; - z-index: 0; - } - - .circle1 { - height: 15rem; - width: 15rem; - top: 10%; - right: -10%; - } +.formCol { + flex: 1; } -@media (max-width: 800px) { - .container { - width: 30rem; - margin-top: 13%; - margin-bottom: 5%; - } - .sideImage { - display: none; - } +/* DROPDOWN */ +.dropdown { + width: 100%; + margin-top: 8px; + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.07), + rgba(255, 255, 255, 0.02) + ); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 18px; + overflow: hidden; + z-index: 5; + backdrop-filter: blur(12px); +} - .signin { - width: 100%; - } +.dropdownItem { + padding: 12px 16px; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.9); + cursor: pointer; +} - .circle { - height: 20rem; - width: 20rem; - top: 30%; - left: 0%; - z-index: 0; - } +/* TERMS */ +.termsRow { + display: flex; + align-items: flex-start; + gap: 8px; + margin-top: 4px; +} - .circle1 { - height: 15rem; - width: 15rem; - top: 10%; - right: -10%; - } +.termsCheckbox { + margin-top: 3px; + accent-color: #ff7b2f; } -@media (max-width: 1100px) { - .container { - width:30rem; - margin-top: 0%; - margin-bottom: 5%; - } +.termsText { + font-size: 0.78rem; + color: #cfcfcf; + line-height: 1.4; +} - .signin { - width: 100%; - } +/* SUBMIT */ +.submitBtn { + margin-top: 6px; + border-radius: 999px; + height: 46px; + font-weight: 600; + background: linear-gradient(140deg, #ff8a3d, #ff5f1f); + border: none; + color: #fff !important; +} - .circle { - height: 20rem; - width: 20rem; - top: 30%; - left: 0%; - z-index: 0; - } +/* FOOTER TEXT */ +.bottomText { + margin-top: 8px; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.65); + text-align: center; +} - .circle1 { - height: 15rem; - width: 15rem; - top: 10%; - right: -10%; - } +.linkAccent { + color: #ffbd84; + margin: 0 4px; + text-decoration: none; } -@media (max-width: 500px) { - .container { - flex-direction: column; - width:98vw; - // margin-top: auto; - // margin-bottom: auto; - margin-top: 25%; - +/* RESPONSIVE */ +@media (max-width: 768px) { + .card { + padding: 26px 20px; } - .signin { - width: 100%; - display: flex; - flex-direction: column; - margin-right: 5%; + .title { + font-size: 1.6rem; } +} - .circle { - height: 15rem; - width: 15rem; - top: 30%; - left: 0%; - z-index: 0; +@media (max-width: 560px) { + .authShell { + padding: 56px 12px 20px; + background-position: center top; } - .circle1 { - height: 15rem; - width: 15rem; - top: 10%; - right: -10%; + .card { + padding: 20px 14px; + border-radius: 16px; } - .ArrowBackIcon { - top: 3%; - left: 7%; + .formRow { + flex-direction: column; + gap: 10px; } -} -@media (max-width: 460px){ - .signin h1{ - width: 30%; + .title { + font-size: 1.45rem; } } diff --git a/src/components/Carousel/Carousel.jsx b/src/components/Carousel/Carousel.jsx index 1c126175..e7386477 100644 --- a/src/components/Carousel/Carousel.jsx +++ b/src/components/Carousel/Carousel.jsx @@ -4,67 +4,69 @@ import { Blurhash } from "react-blurhash"; import styles from "./styles/Carousel.module.scss"; import CarouselSkeleton from "../../layouts/Skeleton/Carousel/Carousel"; -function Carousel({ children, images, customStyles = {}, showSkeleton = true }) { +function Carousel({ children, images, showSkeleton = true }) { const [current, setCurrent] = useState(0); const [autoPlay, setAutoPlay] = useState(true); - const [isImageLoaded, setImageLoaded] = useState(false); + const [loadedImages, setLoadedImages] = useState({}); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - const loadingTimeout = setTimeout(() => { - setIsLoading(false); - }, 1000); + const contentCount = images ? images.length : Children.count(children); - return () => clearTimeout(loadingTimeout); + useEffect(() => { + const timeout = setTimeout(() => setIsLoading(false), 700); + return () => clearTimeout(timeout); }, []); useEffect(() => { - let timeOut = null; - if (autoPlay && !isLoading) { - timeOut = setTimeout(() => { - slideRight(); - }, 2500); - } - return () => clearTimeout(timeOut); + if (!autoPlay || isLoading) return; + const interval = setInterval(() => { + handleNext(); + }, 4000); + return () => clearInterval(interval); }, [current, autoPlay, isLoading]); - const slideRight = () => { - setCurrent((prevCurrent) => { - const contentCount = images ? images.length : Children.count(children); - return prevCurrent === contentCount - 1 ? 0 : prevCurrent + 1; - }); + const handleNext = () => { + setCurrent((prev) => (prev === contentCount - 1 ? 0 : prev + 1)); }; - const contentCount = images ? images.length : Children.count(children); + const handlePrev = () => { + setCurrent((prev) => (prev === 0 ? contentCount - 1 : prev - 1)); + }; - const handleImageLoad = () => { - setImageLoaded(true); + const handleImageLoad = (index) => { + setLoadedImages((prev) => ({ ...prev, [index]: true })); }; if (isLoading && showSkeleton) { - return ; + return ; } return ( -
-
{ - setAutoPlay(false); - }} - onMouseLeave={() => { - setAutoPlay(true); - }} - > -
- {images - ? images.map((image, index) => ( +
setAutoPlay(false)} + onMouseLeave={() => setAutoPlay(true)} + > +
+ {images + ? images.map((image, index) => { + const offset = (index - current + contentCount) % contentCount; + + return (
-
- {!isImageLoaded && ( +
+ {!loadedImages[index] && ( )} {image.title} handleImageLoad(index)} + className={styles.image} />
-
-

{image.title}

-
- )) - : Children.map(children, (child, index) => ( -
- {child} -
- ))} -
- {Array.from({ length: contentCount }).map((_, index) => ( -
setCurrent(index)} - >
+ ); + }) + : Children.map(children, (child, index) => ( +
+ {child} +
))} -
+
+ +
+ + +
+ {Array.from({ length: contentCount }).map((_, index) => ( + setCurrent(index)} + /> + ))}
+ +
); @@ -112,14 +120,8 @@ function Carousel({ children, images, customStyles = {}, showSkeleton = true }) Carousel.propTypes = { children: PropTypes.node, - images: PropTypes.arrayOf( - PropTypes.shape({ - image: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - }) - ), - customStyles: PropTypes.object, + images: PropTypes.array, showSkeleton: PropTypes.bool, }; -export default Carousel; +export default Carousel; \ No newline at end of file diff --git a/src/components/Carousel/styles/Carousel.module.scss b/src/components/Carousel/styles/Carousel.module.scss index 5f531894..75b24bd8 100644 --- a/src/components/Carousel/styles/Carousel.module.scss +++ b/src/components/Carousel/styles/Carousel.module.scss @@ -1,241 +1,120 @@ - - -.carousel { - display: flex; - height: 400.14px; - width: 582.01px; - max-width: 570px; - max-height: 350px; - margin-left: 30px; - box-sizing: border-box; - // margin-top: 5rem; -} - -.image_container { - position: relative; - width: 100%; - height: 100%; -} - -.blurhash { - position: absolute; - top: 0; - left: 0; +.carousel_outer { width: 100%; - height: 100%; - z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 28px; } -.carousel_wrapper { +.carousel { position: relative; - width: 100%; - height: 100%; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: flex-end; - flex-direction: column; + width: min(100%, 720px); + aspect-ratio: 16 / 10; + perspective: 1400px; } -.carousel_card { - display: flex; - flex: 1; +/* Cards */ +.card { position: absolute; - width: 100%; - height: 100%; + inset: 0; + border-radius: 26px; overflow: hidden; - border-radius: 72px; - border: 6px solid white; - box-sizing: border-box; - box-shadow: 0px 0px 11px 3px rgba(0, 0, 0, 0.15); - opacity: 0; - pointer-events: none; - transform: scale(1); - transition: 0.5s ease-in-out; + background: #111; + transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1); + box-shadow: 0 25px 60px rgba(0, 0, 0, 0.45); } -.carousel_card_active { +.active { + transform: translate(0, 0) scale(1); opacity: 1; - transform: scale(1); - pointer-events: all; + z-index: 3; } -.card_image { - width: 100%; - height: 100%; - object-fit: cover; -} -.card_image.loaded { +.layerOne { + transform: translate(40px, -40px) scale(0.92); + opacity: 0.85; z-index: 2; } -.card_overlay { - position: absolute; - width: 100%; - height: 100%; - // background-color: rgba(0, 0, 0, 0.5); - display: flex; - padding: 40px 30px; - align-items: flex-end; -} - -.card_title { - color: white; - font-size: 30px; -} - -.carousel_pagination { - position: absolute; - // bottom: 5px; - left: 50%; - transform: translate(-50%, 0); - margin-bottom: 10px; -} - -.pagination_dot { - height: 10px; - width: 10px; - background-color: #f5f5f5; - border-radius: 50%; - display: inline-block; - margin-left: 10px; - cursor: pointer; -} - -.pagination_dot:hover { - transform: scale(1.2); +.layerTwo { + transform: translate(80px, -80px) scale(0.85); + opacity: 0.65; + z-index: 1; } -.pagination_dot_active { - background-color: steelblue; +.hidden { + opacity: 0; + transform: scale(0.8); + z-index: 0; } - -@media screen and (max-width:400px) { - .carousel { - height: 18rem; - width: 85vw; - margin-top: 0rem; - margin-right: 1.5rem; - } - - - .carousel_card { - border-radius: 40px; - border: 2px solid white; - } +.imageWrapper { + width: 100%; + height: 100%; } -@media screen and (min-width:400px) { - .carousel { - height: 17rem; - width: 85vw; - margin-top: 2rem; - margin-right: 1.5rem; - } - - .carousel_card { - border-radius: 50px; - border: 6px solid white; - } +.image { + width: 100%; + height: 100%; + object-fit: cover; } -@media screen and (max-width:700px ) { - - .carousel_card { - border-radius: 40px; - border: 2px solid white; - } +/* Controls */ +.controls { + display: flex; + align-items: center; + gap: 32px; +} + +/* Modern Clip Button */ +.clipBtn { + padding: 10px 28px; + font-weight: 600; + font-size: 14px; + letter-spacing: 0.06em; + background: transparent; + border: 2px solid #ff8a3d; + border-radius: 999px; + cursor: pointer; + color: transparent; + background-image: linear-gradient(#ff8a3d, #ff8a3d); + background-clip: text; + -webkit-background-clip: text; + transition: all 0.3s ease; } -@media screen and (min-width:700px) { - .carousel { - - height: 13rem; - // width: 36rem; - width: 20rem; - margin-top:10px ; - } - - .carousel_card { - border-radius: 40px; - border: 2px solid white; - } +.clipBtn:hover { + background: #ff8a3d; + color: #111; } -@media screen and (min-width:600px) { - .carousel { - - height: 11rem; - // width: 36rem; - width: 17rem; - margin-top:10px ; - } - - .carousel_card { - border-radius: 40px; - border: 2px solid white; - } +/* Pagination */ +.pagination { + display: flex; + gap: 10px; } -@media screen and (min-width:900px) { - .carousel { - - height: 17rem; - // width: 36rem; - width: 27rem; - margin-top:10px ; - - - } - - .carousel_card { - border-radius: 40px; - border: 2px solid white; - } +.dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: all 0.25s ease; } -@media screen and (min-width:800px) { - .carousel { - - height: 14rem; - // width: 36rem; - width: 22rem; - margin-top:10px ; - margin-right: 2rem; - - - } - - .carousel_card { - border-radius: 40px; - border: 2px solid white; - } +.dotActive { + background: #ff8a3d; + transform: scale(1.4); } -@media screen and (min-width:1200px) { - .carousel { - - height: 35rem; - width: 46rem; - +@media (max-width: 768px) { + .layerOne { + transform: translate(20px, -20px) scale(0.94); } - .carousel_card { - border-radius: 72px; - border: 6px solid white; - } - -} -@media screen and (min-width:1100px) { - .carousel { - display: flex; - height: 25rem; - width: 40rem; + .layerTwo { + transform: translate(40px, -40px) scale(0.88); } - .carousel_card { - border-radius: 52px; - border: 4px solid white; - } - } \ No newline at end of file diff --git a/src/components/Chatbot/Chatbot.jsx b/src/components/Chatbot/Chatbot.jsx index f8289546..5fb23e4c 100644 --- a/src/components/Chatbot/Chatbot.jsx +++ b/src/components/Chatbot/Chatbot.jsx @@ -177,8 +177,8 @@ const Chatbot = () => { const emailResponse = { id: messages.length + 2, text: emailResult.success - ? '✅ Your message has been sent to FED! The team will get back to you soon. 📧' - : '❌ Sorry, there was an error sending your email. Please try again later.', + ? ' Your message has been sent to FED! The team will get back to you soon. 📧' + : ' Sorry, there was an error sending your email. Please try again later.', isUser: false, timestamp: new Date(), }; @@ -376,8 +376,7 @@ const Chatbot = () => { onClick={toggleChatbot} aria-label="Open Chat" > - -
+ )} diff --git a/src/components/Chatbot/Chatbot.module.scss b/src/components/Chatbot/Chatbot.module.scss index a78f8408..64f63c08 100644 --- a/src/components/Chatbot/Chatbot.module.scss +++ b/src/components/Chatbot/Chatbot.module.scss @@ -37,25 +37,13 @@ $tablet: 768px; $mobile: 480px; // Sizes -$toggle-button-size: 72px; +$toggle-button-size: 58px; $border-radius-medium: 16px; // ============================================ // ANIMATIONS // ============================================ -@keyframes bounce { - - 0%, - 100% { - transform: translateY(0); - } - - 50% { - transform: translateY(-8px); - } -} - @keyframes fadeIn { from { opacity: 0; @@ -99,7 +87,7 @@ $border-radius-medium: 16px; .chatbotToggle { position: fixed; bottom: $spacing-xl; - right: $spacing-xl; + right: 60px; width: $toggle-button-size; height: $toggle-button-size; border-radius: 50%; @@ -110,44 +98,18 @@ $border-radius-medium: 16px; align-items: center; justify-content: center; cursor: pointer; - box-shadow: 0 8px 24px rgba(244, 43, 3, 0.4); + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.35); z-index: $z-toggle; transition: all $transition-fast; - animation: bounce 2s infinite; backdrop-filter: blur($glass-blur); &:hover { - transform: scale(1.1); - animation: none; - box-shadow: 0 12px 32px rgba(244, 43, 3, 0.5); + transform: none; + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.35); } &:active { - transform: scale(0.95); - } -} - -.pulseRing { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - height: 100%; - border-radius: 50%; - border: 3px solid rgba(255, 190, 11, 0.6); - animation: pulseRing 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -@keyframes pulseRing { - 0% { - transform: translate(-50%, -50%) scale(1); - opacity: 1; - } - - 100% { - transform: translate(-50%, -50%) scale(1.5); - opacity: 0; + transform: none; } } @@ -188,6 +150,12 @@ $border-radius-medium: 16px; overflow: hidden; } +:global(body.fed-modal-open) .chatbotToggle, +:global(body.fed-modal-open) .chatbotContainer, +:global(body.fed-modal-open) .backdrop { + display: none !important; +} + @keyframes slideInUp { from { transform: translateY(20px); @@ -671,9 +639,9 @@ $border-radius-medium: 16px; .chatbotToggle { bottom: $spacing-md; - right: $spacing-md; - width: 64px; - height: 64px; + right: 26px; + width: 54px; + height: 54px; } .message { @@ -697,4 +665,4 @@ $border-radius-medium: 16px; .title { font-size: 1.1rem; } -} \ No newline at end of file +} diff --git a/src/components/Core/Input.jsx b/src/components/Core/Input.jsx index d4f883e9..410c1b02 100644 --- a/src/components/Core/Input.jsx +++ b/src/components/Core/Input.jsx @@ -2,30 +2,41 @@ import { forwardRef, useRef, useState } from "react"; import PropTypes from "prop-types"; import { FaRegCalendarAlt } from "react-icons/fa"; import Select, { components } from "react-select"; -import DatePicker from "react-date-picker"; import { AiOutlineDown } from "react-icons/ai"; import { FaEye, FaEyeSlash } from "react-icons/fa"; import DatePickerWithTime from "react-datepicker"; +import { isValid, parse } from "date-fns"; import styles from "./styles/Core.module.scss"; const CustomInput = forwardRef( - ({ value, onClick, placeholder = "Select Date & Time" }, ref) => ( + ( + { value, onClick, placeholder = "Select Date & Time", className, style }, + ref + ) => (

{value || placeholder} @@ -35,8 +46,9 @@ const CustomInput = forwardRef( size={18} style={{ position: "absolute", - top: "20%", - right: "8px", + top: "50%", + right: "12px", + transform: "translateY(-50%)", }} />

@@ -52,57 +64,67 @@ CustomInput.propTypes = { }; const customStyles = { - control: (provided) => ({ + control: (provided, state) => ({ ...provided, - display: "flex", - outline: "none", - width: "99.5%", - fontSize: "12px", - backgroundColor: "transparent", - borderRadius: "4px", + minHeight: "52px", + borderRadius: "999px", color: "#fff", - marginBottom: "0", - maxHeight: "40px", - marginLeft: "8px", - marginRight: "8px", - marginTop: "4px", - position: "relative", - border: "1px solid rgba(211, 211, 211, 0.5)", - boxShadow: "none", + fontSize: "0.95rem", + background: + "linear-gradient(145deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.02))", + border: state.isFocused + ? "1px solid rgba(255, 165, 90, 0.95)" + : "1px solid rgba(255, 255, 255, 0.2)", + boxShadow: state.isFocused + ? "0 0 0 4px rgba(255, 133, 50, 0.18), 0 10px 28px rgba(0, 0, 0, 0.3)" + : "none", + cursor: "pointer", "&:hover": { - borderColor: "#fff !important", + borderColor: state.isFocused + ? "rgba(255, 165, 90, 0.95)" + : "rgba(255, 255, 255, 0.2)", }, }), + valueContainer: (provided) => ({ + ...provided, + padding: "0 16px", + }), menu: (provided) => ({ ...provided, - width: "99.5%", - marginLeft: "8px", + marginTop: "8px", + borderRadius: "16px", + border: "1px solid rgba(255, 255, 255, 0.18)", + background: + "linear-gradient(145deg, rgba(16, 19, 28, 0.98), rgba(12, 16, 24, 0.96))", + overflow: "hidden", }), menuPortal: (provided) => ({ ...provided, zIndex: 111 }), + menuList: (provided) => ({ + ...provided, + padding: "6px", + }), placeholder: (provided) => ({ ...provided, - display: "flex", - alignItems: "center", - marginTop: "-7px", + color: "rgba(255, 255, 255, 0.5)", }), option: (provided, state) => ({ ...provided, - color: state.isSelected ? "#FF8A00" : "#2D2D2D", - backgroundColor: state.isSelected ? "#2D2D2D" : "#fff", + color: state.isSelected ? "#ffffff" : "rgba(255, 255, 255, 0.9)", + background: state.isSelected + ? "linear-gradient(140deg, #ff8a3d, #ff5f1f)" + : "transparent", cursor: "pointer", - width: "99%", - border: "none", - margin: "0 auto", - borderRadius: "4px", + borderRadius: "12px", + margin: "2px 0", + fontSize: "0.92rem", "&:hover": { - transition: "ease-in-out 0.3s", - backgroundColor: "#2D2D2D", - color: "#FF8A00", - margin: "2px auto", + background: state.isSelected + ? "linear-gradient(140deg, #ff8a3d, #ff5f1f)" + : "rgba(255, 255, 255, 0.08)", + color: "#fff", }, "&:active": { - backgroundColor: "#2D2D2D", - color: "#FF8A00", + background: "rgba(255, 255, 255, 0.12)", }, }), indicatorSeparator: (provided) => ({ @@ -112,25 +134,14 @@ const customStyles = { singleValue: (provided) => ({ ...provided, color: "#fff", - display: "flex", - alignItems: "center", - marginTop: "-7px", - fontSize:"larger" + fontSize: "0.95rem", }), }; const DropdownIndicator = (props) => { return ( - + ); }; @@ -157,6 +168,19 @@ const Input = (props) => { const [showPassword, setshowPassword] = useState(false); const [previewFile, setpreviewFile] = useState(null); + const parseDateValue = (input) => { + if (!input) return null; + if (input instanceof Date) return input; + + if (typeof input === "string") { + const parsed = parse(input, "MMMM do yyyy, h:mm:ss a", new Date()); + if (isValid(parsed)) return parsed; + } + + const fallback = new Date(input); + return Number.isNaN(fallback.getTime()) ? null : fallback; + }; + const filterPassedTime = (time) => { const currentDate = new Date(); const selectedDate = new Date(time); @@ -230,46 +254,50 @@ const Input = (props) => { case "date": return (
- - } + placeholderText={placeholder} + className={`${styles.input} ${styles.inputDate} ${className}`} + showPopperArrow={false} + popperPlacement="bottom-start" + popperClassName="fed-datepicker-popper" + calendarClassName="fed-datepicker fed-datepicker-compact" + withPortal + portalId="fed-datepicker-portal" {...rest} />
); case "datetime-local": return ( -
+
} + popperPlacement="bottom-start" + popperClassName="fed-datepicker-popper" + calendarClassName="fed-datepicker fed-datepicker-datetime" + showPopperArrow={false} + withPortal + portalId="fed-datepicker-portal" + popperModifiers={[ + { name: "offset", options: { offset: [0, 8] } }, + { name: "preventOverflow", options: { boundary: "viewport", padding: 8 } }, + ]} {...rest} />
diff --git a/src/components/Core/styles/Core.module.scss b/src/components/Core/styles/Core.module.scss index f6f8cc94..1d9e11d7 100644 --- a/src/components/Core/styles/Core.module.scss +++ b/src/components/Core/styles/Core.module.scss @@ -1,105 +1,225 @@ +/* ================= Buttons ================= */ + .main_Button { - display: flex; + display: inline-flex; justify-content: center; align-items: center; - margin-top: 20px; - outline: none; - border: none; - padding: 8px 16px; - font-size: 12px; - background-color: transparent; - border: 1px solid rgba(211, 211, 211, 0.5); + + padding: 12px 22px; + font-size: 0.95rem; + font-weight: 600; + line-height: 1; + + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.22); + + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.12), + rgba(255, 255, 255, 0.04) + ); + + backdrop-filter: blur(10px); + color: #ffffff; cursor: pointer; - border-radius: 4px; - font-weight: 500; + + transition: background 0.2s ease, box-shadow 0.2s ease; } -.h1 { - font-size: 2em; - font-weight: bold; +.main_Button:hover:not(:disabled) { + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.16), + rgba(255, 255, 255, 0.06) + ); + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25); } -.h2 { - font-size: 1.75em; - font-weight: bold; +.main_Button:active:not(:disabled) { + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); } -.h3 { - font-size: 1.5em; - font-weight: 800; +.main_Button:disabled { + opacity: 0.55; + cursor: not-allowed; } -.h4 { - font-size: 1.25em; - font-weight: 700; +/* ================= Typography ================= */ + +.h1 { font-size: 2.2rem; font-weight: 800; } +.h2 { font-size: 1.9rem; font-weight: 700; } +.h3 { font-size: 1.6rem; font-weight: 700; } +.h4 { font-size: 1.3rem; font-weight: 600; } +.h5 { font-size: 1.05rem; font-weight: 600; } +.h6 { font-size: 0.9rem; font-weight: 500; } + +.p { font-size: 1rem; font-weight: 400; } +.body2 { font-size: 0.875rem; font-weight: 400; } + +/* ================= Input Container ================= */ + +.containerInput { + position: relative; + width: 100%; + + display: flex; + flex-direction: column; + gap: 8px; /* controls space between label and input */ + overflow: visible; } -.h5 { - font-size: 1em; +/* ================= Input Label ================= */ + +.inputLabel { + color: rgba(255, 255, 255, 0.85); + font-size: 0.8rem; font-weight: 600; + letter-spacing: 0.02em; +} + +/* ================= Input Base ================= */ + +.input { + width: 100%; + min-height: 52px; + padding: 0 16px; + + display: flex; + align-items: center; + + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.2); + + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.07), + rgba(255, 255, 255, 0.02) + ); + + backdrop-filter: blur(12px); + color: #ffffff; + font-size: 0.95rem; + line-height: 1.2; + + transition: border-color 0.2s ease, box-shadow 0.2s ease; } -.h6 { - font-size: 0.875em; - font-weight: 500; +.input::placeholder { + color: rgba(255, 255, 255, 0.5); } -.p { - font-size: 1em; - font-weight: normal; +.input:focus, +.input:focus-visible { + border-color: rgba(255, 165, 90, 0.95); + box-shadow: + 0 0 0 4px rgba(255, 133, 50, 0.18), + 0 10px 28px rgba(0, 0, 0, 0.3); + outline: none; } -.body2 { - font-size: 0.875em; - font-weight: normal; +/* ================= Textarea ================= */ + +.inputTxtArea { + min-height: 160px; + padding: 16px; + border-radius: 28px; + resize: none; } -.containerInput { +/* ================= Date & Select ================= */ + +.inputDate { + padding-right: 44px; +} + +.datePickerWrap { position: relative; - width: auto; + z-index: 2; } -.input { - display: flex; - outline: none; + +:global(.react-calendar) { + background: transparent; border: none; - padding: 10px; - font-size: 12px; - margin: 6px; - margin-bottom: 10px; - background-color: transparent; - border: 1px solid rgba(211, 211, 211, 0.5) !important; - border-radius: 4px; + color: inherit; +} + +:global(.react-calendar__navigation button) { color: #fff; - position: relative; - width: auto; } -.input:hover { - border: 1px solid #fff !important; - transition: 0.3s ease-in-out; + +:global(.react-calendar__month-view__weekdays) { + color: rgba(255, 255, 255, 0.7); } -.inputTxtArea { - min-height: 185px; - width: 380px; - resize: none; +:global(.react-calendar__tile) { + color: #fff; + border-radius: 10px; + padding: 0.45em 0.2em; } -.inputSelect { + +:global(.react-calendar__tile:enabled:hover), +:global(.react-calendar__tile:enabled:focus) { + background: rgba(255, 255, 255, 0.08); +} + +:global(.react-calendar__tile--active) { + background: linear-gradient(140deg, #ff8a3d, #ff5f1f); color: #fff; - width: 380px !important; } -.inputSelect option { - color: black; + +.input :global(.react-select__control) { + min-height: 52px; + border-radius: 999px; +} + +.input :global(.react-select__value-container) { + padding: 0 16px; } -.inputDate { - padding: 6px !important; - width: auto !important; - height: 40px !important; - position: relative; + +.input :global(.react-select__indicator-separator) { + display: none; +} + +.input :global(.react-select__menu) { + border-radius: 16px; + overflow: hidden; +} + +/* ================= Password Icon ================= */ + +.eyeIcon { + position: absolute; + right: 18px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + opacity: 0.75; + transition: opacity 0.2s ease; } + +.eyeIcon:hover { + opacity: 1; +} + +/* ================= Number Spinner Removal ================= */ + input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - -moz-appearance: none; appearance: none; margin: 0; } + +/* ================= Optional Glow Animation ================= */ + +@keyframes fieldGlow { + 0% { + box-shadow: + 0 0 0 4px rgba(255, 133, 50, 0.15), + 0 8px 20px rgba(0, 0, 0, 0.25); + } + 100% { + box-shadow: + 0 0 0 4px rgba(255, 133, 50, 0.25), + 0 12px 32px rgba(0, 0, 0, 0.35); + } +} diff --git a/src/components/EventCard/EventCard.jsx b/src/components/EventCard/EventCard.jsx index ce0399f3..ab745cb0 100644 --- a/src/components/EventCard/EventCard.jsx +++ b/src/components/EventCard/EventCard.jsx @@ -11,7 +11,7 @@ import { PiClockCountdownDuotone } from "react-icons/pi"; import { IoIosLock, IoIosStats } from "react-icons/io"; import { MdGroups } from "react-icons/md"; -import { FaUser, FaRupeeSign,FaEye} from "react-icons/fa"; +import { FaUser, FaRupeeSign, FaEye } from "react-icons/fa"; import { QrCode } from "lucide-react"; import { parse, differenceInMilliseconds, formatDistanceToNow } from "date-fns"; import { Button } from "../Core"; @@ -25,9 +25,8 @@ import { TeamDetailsModal } from "../../features/Modals"; const EventCard = (props) => { const { data, - onOpen, + onOpen = () => {}, type, - modalpath, customStyles = {}, showShareButton = true, showRegisterButton = true, @@ -36,7 +35,7 @@ const EventCard = (props) => { onEdit, onDelete, enableEdit, - isLoading, + isLoading = false, isRegisteredInRelatedEvents, eventName, } = props; @@ -332,14 +331,10 @@ const EventCard = (props) => { duration: 3000, }); } else { - setNavigatePath("/Events/" + data.id + "/Form"); - setTimeout(() => { - setShouldNavigate(true); - }, 1000); - setTimeout(() => { + navigate(`/Events/${data.id}/details`); setIsMicroLoading(false); - }, 3000); + }, 300); } } else { setIsMicroLoading(true); @@ -363,13 +358,13 @@ const EventCard = (props) => { } return ( -
+
setisHovered(true)} onMouseLeave={() => setisHovered(false)} className={style.card} style={customStyles.card} - // data-aos={aosDisable ? "" : "fade-up"} + // data-aos={aosDisable ? "" : "fade-up"} >
{
{ + e.stopPropagation(); + handleShare(); + }} > {
{ + e.stopPropagation(); + handleQRCode(); + }} title="View Attendance QR Code" > { {type === "ongoing" && ( -

+

{info.participationType === "Team" ? ( <> @@ -470,32 +471,37 @@ const EventCard = (props) => { )} -
+ {info.eventAmount ? ( -

+ {info.eventAmount} -

+
) : ( -

Free

+ + Free + )} -
-

+ +
)}
{type === "ongoing" && showRegisterButton && (
{ - // if ( - // btnTxt === "Locked" && - // authCtx.isLoggedIn && - // authCtx.user.access === "USER" - // ) { - // } - // }} + // onMouseEnter={() => { + // if ( + // btnTxt === "Locked" && + // authCtx.isLoggedIn && + // authCtx.user.access === "USER" + // ) { + // } + // }} > - +
)} - +
@@ -575,7 +581,7 @@ const EventCard = (props) => {
{info.eventdescription}
- + { ); if (isConfirmed && onDelete) { authCtx.eventData = data; - onDelete(); + onDelete(data.id); } }} variant="secondary" @@ -641,7 +647,7 @@ const EventCard = (props) => { />
)} - + {/* Team Details Modal */} { formId={data.id} eventTitle={info.eventTitle} /> - +
); @@ -657,17 +663,17 @@ const EventCard = (props) => { EventCard.propTypes = { data: PropTypes.object.isRequired, - onOpen: PropTypes.func.isRequired, + onOpen: PropTypes.func, type: PropTypes.string.isRequired, - modalpath: PropTypes.string.isRequired, customStyles: PropTypes.object, showShareButton: PropTypes.bool, showRegisterButton: PropTypes.bool, additionalContent: PropTypes.node, aosDisable: PropTypes.bool, onEdit: PropTypes.func, + onDelete: PropTypes.func, enableEdit: PropTypes.bool, - isLoading: PropTypes.bool.isRequired, + isLoading: PropTypes.bool, }; -export default EventCard; \ No newline at end of file +export default EventCard; diff --git a/src/components/EventCard/styles/EventCard.module.scss b/src/components/EventCard/styles/EventCard.module.scss index 1ed1b89d..82118661 100644 --- a/src/components/EventCard/styles/EventCard.module.scss +++ b/src/components/EventCard/styles/EventCard.module.scss @@ -15,6 +15,10 @@ overflow: hidden; } +.cardWrapper { + position: relative; +} + .sharecontainer { position: absolute; z-index: 2; @@ -141,7 +145,12 @@ width: 1rem; } -.price p { +.price { + display: flex; + align-items: center; +} + +.priceValue { display: flex; align-items: center; color: #fff; @@ -188,7 +197,7 @@ text-overflow:ellipsis; } -.eventname p { +.eventMeta { font-size: 0.9rem; width: auto; display: flex; @@ -309,10 +318,10 @@ a { background-clip: text; } - .eventname p { + .eventMeta { font-size: 0.7rem; display: flex; align-items: center; margin-top: 0.1rem; } -} \ No newline at end of file +} diff --git a/src/components/FoxMascot/FoxMascot.jsx b/src/components/FoxMascot/FoxMascot.jsx new file mode 100644 index 00000000..250270da --- /dev/null +++ b/src/components/FoxMascot/FoxMascot.jsx @@ -0,0 +1,309 @@ +/** + * @fileoverview FoxMascot Component - Main canvas wrapper for 3D fox animation + * @module components/FoxMascot/FoxMascot + */ + +import React, { useRef, useEffect, useState, useCallback, Suspense } from 'react'; +import { Canvas } from '@react-three/fiber'; +import { OrbitControls, PerspectiveCamera } from '@react-three/drei'; +import * as THREE from 'three'; +import FoxModel from './FoxModel'; +import useAnimationSequence from './useAnimationSequence'; +import styles from './FoxMascot.module.scss'; + +/** + * FoxMascot Component - Renders the 3D fox mascot animation overlay + * @param {Object} props + * @param {Function} props.onComplete - Called when animation finishes or is skipped + * @param {boolean} props.isVisible - Controls visibility of the component + */ +const FoxMascot = ({ onComplete, isVisible = true }) => { + const foxRef = useRef(); + const canvasRef = useRef(); + const [showSkip, setShowSkip] = useState(true); + const [currentPhase, setCurrentPhase] = useState(null); + const [isLoaded, setIsLoaded] = useState(false); + + /** + * Handle phase change from animation sequence + */ + const handlePhaseChange = useCallback((phase) => { + setCurrentPhase(phase); + console.log(`[FoxMascot] Phase: ${phase}`); + }, []); + + /** + * Handle animation complete + */ + const handleComplete = useCallback(() => { + console.log('[FoxMascot] Animation complete'); + setShowSkip(false); + if (onComplete) { + onComplete(); + } + }, [onComplete]); + + // Animation sequence hook + const animation = useAnimationSequence(foxRef, handleComplete, handlePhaseChange); + + /** + * Start animation when fox model is loaded + * Sequence: Jump → Run (move left) → Walk (continue left) → Sit + * Combines skeletal animations with position movement + */ + /** + * AVAILABLE FOX ANIMATIONS: + * ------------------------- + * Fox_Attack_Paws, Fox_Attack_Tail, Fox_Falling, Fox_Falling_Left, + * Fox_Idle, Fox_Jump, Fox_Jump_InAir, Fox_Jump_Pivot_InPlace, + * Fox_Run, Fox_Run_InPlace, Fox_Run_Left, Fox_Run_Left_InPlace, + * Fox_Run_Right, Fox_Run_Right_InPlace, Fox_Sit1, Fox_Sit2_Idle, + * Fox_Sit3_StandUp, Fox_Sit_Idle_Break, Fox_Sit_No, Fox_Sit_Yes, + * Fox_Somersault, Fox_Somersault_InPlace, Fox_Walk, Fox_Walk_Back, + * Fox_Walk_Back_InPlace, Fox_Walk_InPlace, Fox_Walk_Left, + * Fox_Walk_Left_InPlace, Fox_Walk_Right, Fox_Walk_Right_InPlace + */ + const handleFoxLoaded = useCallback(() => { + setIsLoaded(true); + console.log('[FoxMascot] Fox loaded - IN-PLACE ANIMATION with GSAP movement'); + + if (!foxRef.current) return; + + const fox = foxRef.current; + const group = fox.getGroup(); + + if (!group) { + console.error('[FoxMascot] Could not get group for position animation'); + return; + } + + // Animation durations (seconds) + const JUMP_DURATION = 1.2; + const RUN_DURATION = 1.5; + const FALL_DURATION = 1.2; // Falling animation + const IDLE_DURATION = 1.0; // Stand up / recover + const TURN_DURATION = 0.8; // Turn to face user + const WALK_DURATION = 2.0; // Walk to final position + const SIT_DURATION = 0.53; + + const crossfade = 0.3; // Smooth crossfade between animations + + // Starting position (right side of screen) + const START_X = 55; + const END_X = -20; // Final position (left side) + + // Set initial position + group.position.x = START_X; + group.position.y = -35; + + console.log('[FoxMascot] Animation sequence:'); + console.log(' 1. Jump → 2. Run → 3. Fall'); + console.log(' 4. Idle (recover) → 5. Turn to face user'); + console.log(' 6. Walk → 7. Sit'); + + // Import GSAP and create timeline + import('gsap').then(({ default: gsap }) => { + + // Calculate movement distances + const jumpDistance = 5; + const runDistance = 30; + const fallDistance = 3; // Slight forward during fall + const walkDistance = 37; // Walk to final position + + // Position checkpoints + const afterJumpX = START_X - jumpDistance; // 50 + const afterRunX = afterJumpX - runDistance; // 20 + const afterFallX = afterRunX - fallDistance; // 17 + const afterWalkX = afterFallX - walkDistance; // -20 + + // Create GSAP timeline + const tl = gsap.timeline({ + onComplete: () => { + console.log('[FoxMascot] ✓ Animation sequence complete!'); + console.log(`[FoxMascot] Final position: ${group.position.x.toFixed(2)}`); + } + }); + + // ===== PHASE 1: JUMP ===== + tl.add(() => { + fox.playAnimation('Fox_Jump', 0, false, 1); + console.log('[FoxMascot] ▶ Phase 1: JUMP'); + }) + .to(group.position, { + x: afterJumpX, + duration: JUMP_DURATION, + ease: 'power2.out' + }, '<') + + // ===== PHASE 2: RUN ===== + .add(() => { + fox.playAnimation('Fox_Run_InPlace', crossfade, true, 1); + console.log('[FoxMascot] ▶ Phase 2: RUN'); + }) + .to(group.position, { + x: afterRunX, + duration: RUN_DURATION, + ease: 'linear' + }, '<') + + // ===== PHASE 3: FALL (cute stumble) ===== + .add(() => { + fox.playAnimation('Fox_Falling', crossfade, false, 1); + console.log('[FoxMascot] ▶ Phase 3: FALL (oops!)'); + }) + .to(group.position, { + x: afterFallX, + duration: FALL_DURATION, + ease: 'power2.out' + }, '<') + + // ===== PHASE 4: IDLE (recover/stand up) ===== + .add(() => { + fox.playAnimation('Fox_Idle', crossfade, true, 1); + console.log('[FoxMascot] ▶ Phase 4: IDLE (recovering)'); + }) + .to({}, { duration: IDLE_DURATION }) + + // ===== PHASE 5: TURN HEAD TO FACE USER ===== + // Rotate only the head bone while staying in Idle (no leg/body movement) + .add(() => { + console.log('[FoxMascot] ▶ Phase 5: TURNING HEAD TO FACE USER'); + fox.setHeadRotation(0, true); // Enable head rotation + }) + .to({ headX: 0 }, { + headX: 1, // Turn head LEFT to look at user + duration: TURN_DURATION, + ease: 'power2.inOut', + onUpdate: function () { + const xVal = this.targets()[0].headX; + fox.setHeadRotation(xVal, true); + } + }) + + // ===== PHASE 5b: TURN HEAD BACK TO ORIGINAL ===== + .add(() => { + console.log('[FoxMascot] ▶ Phase 5b: TURNING HEAD BACK'); + }) + .to({ headX: 1 }, { + headX: 0, // Turn head back to original direction + duration: TURN_DURATION * 0.7, + ease: 'power2.inOut', + onUpdate: function () { + const xVal = this.targets()[0].headX; + fox.setHeadRotation(xVal, true); + } + }) + + // ===== PHASE 6: WALK ===== + .add(() => { + fox.setHeadRotation(0, false); // Disable head override + fox.playAnimation('Fox_Walk_InPlace', crossfade, true, 1); + console.log('[FoxMascot] ▶ Phase 6: WALK'); + }) + .to(group.position, { + x: afterWalkX, + duration: WALK_DURATION, + ease: 'linear' + }, '<') + + // ===== PHASE 7: TURN TO FACE USER ===== + .add(() => { + fox.playAnimation('Fox_Walk_Left_InPlace', crossfade, true, 1); + console.log('[FoxMascot] ▶ Phase 7: TURN TO FACE USER'); + }) + .to(group.rotation, { + y: Math.PI / 12, // Rotate slightly to face camera + duration: 1.0, + ease: 'power2.inOut' + }, '<') + + // ===== PHASE 8: SIT (facing user) ===== + .add(() => { + fox.playAnimation('Fox_Sit2_Idle', crossfade, true, 1); // Use idle sit - more forward looking + console.log('[FoxMascot] ▶ Phase 8: SIT (facing user)'); + }) + // Tilt fox upward so it looks at the screen/user + .to(group.rotation, { + x: -0.30, // Tilt more to look at user + duration: 0.5, + ease: 'power2.out' + }, '<+0.3'); + + }); + + }, []); + + /** + * Handle skip button click + */ + const handleSkip = useCallback(() => { + animation.skip(); + }, [animation]); + + // DISABLED: Start animation once loaded + // useEffect(() => { + // if (isLoaded && foxRef.current && isVisible) { + // animation.play(); + // } + // }, [isLoaded, isVisible]); + + if (!isVisible) { + return null; + } + + return ( +
+ {/* Skip Button */} + {showSkip && ( + + )} + + {/* Three.js Canvas */} + + {/* Lighting */} + + + + + {/* Camera - FOV=50, z=100 where size was correct */} + + + {/* Fox Model - PROPERLY SIZED AND POSITIONED */} + {/* Base model is ~27 units, scale=0.5 gives ~13 unit fox (visible height ~93 units) */} + + + + + {/* Debug controls (uncomment to debug) */} + {/* */} + +
+ ); +}; + +export default FoxMascot; diff --git a/src/components/FoxMascot/FoxMascot.module.scss b/src/components/FoxMascot/FoxMascot.module.scss new file mode 100644 index 00000000..34ed3f58 --- /dev/null +++ b/src/components/FoxMascot/FoxMascot.module.scss @@ -0,0 +1,62 @@ +/** + * FoxMascot Styles + * Full-screen overlay for 3D fox mascot animation + */ + +.foxMascotContainer { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9998; + /* Below chatbot modal (9999), above everything else */ + overflow: hidden; +} + +.canvas { + width: 100%; + height: 100%; + background: transparent; +} + +.skipButton { + position: fixed; + bottom: 100px; + right: 20px; + padding: 10px 20px; + background: rgba(255, 255, 255, 0.9); + border: 2px solid #ff6b35; + border-radius: 25px; + color: #ff6b35; + font-size: 14px; + font-weight: 600; + cursor: pointer; + pointer-events: auto; + z-index: 10000; + transition: all 0.2s ease; + backdrop-filter: blur(5px); + box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3); + + &:hover { + background: #ff6b35; + color: white; + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4); + } + + &:active { + transform: scale(0.98); + } +} + +/* Mobile adjustments */ +@media (max-width: 768px) { + .skipButton { + bottom: 80px; + right: 15px; + padding: 8px 16px; + font-size: 12px; + } +} \ No newline at end of file diff --git a/src/components/FoxMascot/FoxModel.jsx b/src/components/FoxMascot/FoxModel.jsx new file mode 100644 index 00000000..f8e3da5b --- /dev/null +++ b/src/components/FoxMascot/FoxModel.jsx @@ -0,0 +1,370 @@ +/** + * @fileoverview FoxModel Component - Loads and animates the 3D fox mascot + * @module components/FoxMascot/FoxModel + * + * FIXES APPLIED: + * 1. Using Center from drei to auto-center the model geometry + * 2. Computing bounding box to understand model dimensions + * 3. Positioning the group AFTER centering + * 4. Removed cloning - using original scene with Center + */ + +import React, { useRef, useEffect, useMemo, forwardRef, useImperativeHandle } from 'react'; +import { useGLTF, useAnimations, Center } from '@react-three/drei'; +import { useFrame } from '@react-three/fiber'; +import * as THREE from 'three'; + +/** + * AVAILABLE FOX ANIMATIONS: + * ------------------------- + * Fox_Attack_Paws - Attack with paws + * Fox_Attack_Tail - Attack with tail + * Fox_Falling - Falling animation + * Fox_Falling_Left - Falling to the left + * Fox_Idle - Standard idle/breathing + * Fox_Jump - Jump and land + * Fox_Jump_InAir - In-air jump pose + * Fox_Jump_Pivot_InPlace - Pivot jump in place + * Fox_Run - Running with root motion + * Fox_Run_InPlace - Running in place (treadmill) + * Fox_Run_Left - Running while turning left + * Fox_Run_Left_InPlace - Running left in place + * Fox_Run_Right - Running while turning right + * Fox_Run_Right_InPlace - Running right in place + * Fox_Sit1 - Transition from standing to sitting + * Fox_Sit2_Idle - Idle while sitting + * Fox_Sit3_StandUp - Standing up from sit + * Fox_Sit_Idle_Break - Idle break while sitting + * Fox_Sit_No - Shaking head no while sitting + * Fox_Sit_Yes - Nodding yes while sitting + * Fox_Somersault - Flip/tumble animation + * Fox_Somersault_InPlace - Somersault in place + * Fox_Walk - Walking with root motion + * Fox_Walk_Back - Walking backwards + * Fox_Walk_Back_InPlace - Walking backwards in place + * Fox_Walk_InPlace - Walking in place (treadmill) + * Fox_Walk_Left - Walking while turning left + * Fox_Walk_Left_InPlace - Walking left in place + * Fox_Walk_Right - Walking while turning right + * Fox_Walk_Right_InPlace - Walking right in place + */ + +// Animation name constants +const ANIMATIONS = { + // Basic + IDLE: 'Fox_Idle', + JUMP: 'Fox_Jump', + JUMP_INAIR: 'Fox_Jump_InAir', + JUMP_PIVOT_INPLACE: 'Fox_Jump_Pivot_InPlace', + FALLING: 'Fox_Falling', + FALLING_LEFT: 'Fox_Falling_Left', + + // Attack + ATTACK_PAWS: 'Fox_Attack_Paws', + ATTACK_TAIL: 'Fox_Attack_Tail', + + // Run + RUN: 'Fox_Run', + RUN_INPLACE: 'Fox_Run_InPlace', + RUN_LEFT: 'Fox_Run_Left', + RUN_LEFT_INPLACE: 'Fox_Run_Left_InPlace', + RUN_RIGHT: 'Fox_Run_Right', + RUN_RIGHT_INPLACE: 'Fox_Run_Right_InPlace', + + // Walk + WALK: 'Fox_Walk', + WALK_INPLACE: 'Fox_Walk_InPlace', + WALK_LEFT: 'Fox_Walk_Left', + WALK_LEFT_INPLACE: 'Fox_Walk_Left_InPlace', + WALK_RIGHT: 'Fox_Walk_Right', + WALK_RIGHT_INPLACE: 'Fox_Walk_Right_InPlace', + WALK_BACK: 'Fox_Walk_Back', + WALK_BACK_INPLACE: 'Fox_Walk_Back_InPlace', + + // Sit + SIT: 'Fox_Sit1', + SIT_IDLE: 'Fox_Sit2_Idle', + SIT_STANDUP: 'Fox_Sit3_StandUp', + SIT_IDLE_BREAK: 'Fox_Sit_Idle_Break', + SIT_NO: 'Fox_Sit_No', + SIT_YES: 'Fox_Sit_Yes', + + // Tricks + SOMERSAULT: 'Fox_Somersault', + SOMERSAULT_INPLACE: 'Fox_Somersault_InPlace', +}; + +// Model path - must be in public folder for Vite +const MODEL_PATH = '/models/fox.glb'; + +/** + * FoxModel component - Loads GLB model and manages animations + * Uses Center from drei to properly center the model geometry + */ +const FoxModel = forwardRef(({ position = [0, 0, 0], rotation = [0, 0, 0], scale = 1, onLoaded }, ref) => { + const group = useRef(); + const { scene, animations } = useGLTF(MODEL_PATH); + const { actions, mixer } = useAnimations(animations, group); + + // Current playing animation tracking + const currentAnimation = useRef(null); + + // Head rotation state (applied every frame after mixer update) + const headRotation = useRef({ y: 0, x: 0, enabled: false }); + const headBoneRef = useRef(null); + + // Compute bounding box and log model info on mount + useEffect(() => { + if (scene) { + // Compute bounding box to understand model size + const box = new THREE.Box3().setFromObject(scene); + const size = box.getSize(new THREE.Vector3()); + const center = box.getCenter(new THREE.Vector3()); + + console.log('[FoxModel] === Model Info ==='); + console.log('[FoxModel] Bounding box size:', size.x.toFixed(2), size.y.toFixed(2), size.z.toFixed(2)); + console.log('[FoxModel] Bounding box center:', center.x.toFixed(2), center.y.toFixed(2), center.z.toFixed(2)); + console.log('[FoxModel] Animations available:', Object.keys(actions)); + + // Log DURATION of each animation clip + console.log('[FoxModel] === Animation Durations ==='); + animations.forEach(clip => { + console.log(`[FoxModel] ${clip.name}: ${clip.duration.toFixed(2)}s`); + }); + + // LOG ALL BONES in the model + console.log('[FoxModel] === ALL BONES ==='); + const bones = []; + scene.traverse((child) => { + if (child.isBone) { + const worldPos = new THREE.Vector3(); + child.getWorldPosition(worldPos); + bones.push({ + name: child.name, + x: worldPos.x.toFixed(4), + y: worldPos.y.toFixed(4), + z: worldPos.z.toFixed(4) + }); + console.log(`[FoxModel] BONE: "${child.name}" at (${worldPos.x.toFixed(4)}, ${worldPos.y.toFixed(4)}, ${worldPos.z.toFixed(4)})`); + } + }); + console.log('[FoxModel] Total bones found:', bones.length); + + console.log('[FoxModel] Props:', { position, rotation, scale }); + } + + if (onLoaded) { + onLoaded(); + } + }, [scene, actions, animations, onLoaded, position, rotation, scale]); + + /** + * Get the duration of an animation clip in seconds + */ + const getAnimationDuration = (animationName) => { + const clip = animations.find(c => c.name === animationName); + return clip ? clip.duration : 0; + }; + + /** + * Crossfade to a new animation smoothly + * @param {string} animationName - Name of the animation to play + * @param {number} duration - Crossfade duration (default 0.5) + * @param {boolean|null} loop - Force loop on/off, or null for auto-detect + * @param {number} timeScale - Playback speed: 1=normal, 0.5=half speed, 2=double speed + */ + const playAnimation = (animationName, duration = 0.5, loop = null, timeScale = 1) => { + const nextAction = actions[animationName]; + if (!nextAction) { + console.warn(`[FoxModel] Animation "${animationName}" not found`); + return; + } + + if (currentAnimation.current === animationName) { + return; + } + + console.log(`[FoxModel] Playing animation: ${animationName} at speed ${timeScale}x`); + + const prevAction = currentAnimation.current ? actions[currentAnimation.current] : null; + + const shouldLoop = loop !== null ? loop : + animationName.includes('Walk') || + animationName.includes('Run') || + animationName.includes('Idle'); + + nextAction.reset(); + nextAction.setLoop(shouldLoop ? THREE.LoopRepeat : THREE.LoopOnce, shouldLoop ? Infinity : 1); + nextAction.clampWhenFinished = !shouldLoop; + nextAction.setEffectiveWeight(1); + nextAction.setEffectiveTimeScale(timeScale); // Use the timeScale parameter + nextAction.play(); + + if (prevAction) { + prevAction.crossFadeTo(nextAction, duration, true); + } + + currentAnimation.current = animationName; + }; + + /** + * Stop all animations + */ + const stopAllAnimations = () => { + Object.values(actions).forEach(action => { + if (action) action.stop(); + }); + currentAnimation.current = null; + }; + + /** + * Get the world position of a bone (for tracking root motion) + * Returns the position of the root/hip bone or falls back to bounding box center + */ + const getRootBonePosition = () => { + if (!scene) return new THREE.Vector3(); + + // Try to find root/hip bone + let rootBone = null; + scene.traverse((child) => { + if (child.isBone && ( + child.name.toLowerCase().includes('root') || + child.name.toLowerCase().includes('hip') || + child.name.toLowerCase().includes('pelvis') + )) { + rootBone = child; + } + }); + + if (rootBone) { + const worldPos = new THREE.Vector3(); + rootBone.getWorldPosition(worldPos); + return worldPos; + } + + // Fallback: use bounding box center + const box = new THREE.Box3().setFromObject(scene); + return box.getCenter(new THREE.Vector3()); + }; + + /** + * Get the current bounding box of the model (for position tracking) + */ + const getBoundingBox = () => { + if (!scene) return null; + const box = new THREE.Box3().setFromObject(scene); + return { + center: box.getCenter(new THREE.Vector3()), + min: box.min.clone(), + max: box.max.clone() + }; + }; + + /** + * Force update the mixer to apply current animation pose + * Call this after playAnimation to ensure bones reflect the new pose + */ + const forceUpdate = (deltaTime = 0) => { + if (mixer) { + mixer.update(deltaTime); + } + }; + + /** + * Get the head bone for procedural rotation + * Searches for bones with 'head' or 'neck' in the name + */ + const getHeadBone = () => { + if (!scene) return null; + + let headBone = null; + scene.traverse((child) => { + if (child.isBone && ( + child.name.toLowerCase().includes('head') || + child.name.toLowerCase().includes('neck') + )) { + // Prefer 'head' over 'neck' + if (!headBone || child.name.toLowerCase().includes('head')) { + headBone = child; + } + } + }); + return headBone; + }; + + /** + * Set head rotation target - applied every frame after mixer updates + * @param {number} xRotation - X rotation (left/right turn) in radians + * @param {boolean} enabled - Whether to apply head rotation + */ + const setHeadRotation = (xRotation = 0, enabled = true) => { + headRotation.current = { x: xRotation, enabled }; + // Cache the head bone + if (!headBoneRef.current) { + headBoneRef.current = getHeadBone(); + } + console.log(`[FoxModel] Head rotation set: X=${xRotation.toFixed(2)}, enabled=${enabled}`); + }; + + /** + * Apply head rotation AFTER animation mixer updates + * This ensures our rotation overrides the animation + */ + useFrame(() => { + if (headRotation.current.enabled && headBoneRef.current) { + headBoneRef.current.rotation.x = headRotation.current.x; + } + }); + + /** + * Get the scene for bone access + */ + const getScene = () => scene; + + // Expose methods to parent via ref + useImperativeHandle(ref, () => ({ + playAnimation, + stopAllAnimations, + getGroup: () => group.current, + getAnimationDuration, + getRootBonePosition, + getBoundingBox, + forceUpdate, + getMixer: () => mixer, + getHeadBone, + setHeadRotation, + getScene, + ANIMATIONS, + })); + + // Use the scene directly without cloning + // Center component will handle geometry centering + console.log('[FoxModel] Rendering at position:', position, 'scale:', scale); + + return ( + <> + {/* + * Key fix: Use group for GSAP/position control + * Center component auto-centers the model geometry + */} + +
+ +
+
+ + ); +}); + +FoxModel.displayName = 'FoxModel'; + +// Preload the model +useGLTF.preload(MODEL_PATH); + +export default FoxModel; +export { ANIMATIONS }; diff --git a/src/components/FoxMascot/index.jsx b/src/components/FoxMascot/index.jsx new file mode 100644 index 00000000..5d7bc261 --- /dev/null +++ b/src/components/FoxMascot/index.jsx @@ -0,0 +1,8 @@ +/** + * @fileoverview FoxMascot Component Export + * @module components/FoxMascot + */ + +export { default } from './FoxMascot'; +export { default as FoxModel, ANIMATIONS } from './FoxModel'; +export { default as useAnimationSequence, PHASES } from './useAnimationSequence'; diff --git a/src/components/FoxMascot/useAnimationSequence.js b/src/components/FoxMascot/useAnimationSequence.js new file mode 100644 index 00000000..8d74f7c9 --- /dev/null +++ b/src/components/FoxMascot/useAnimationSequence.js @@ -0,0 +1,248 @@ +/** + * @fileoverview useAnimationSequence Hook - GSAP timeline for fox mascot animation + * Uses mathematical camera frustum calculation for screen-to-world conversion + * @module components/FoxMascot/useAnimationSequence + */ + +import { useRef, useCallback, useEffect } from 'react'; +import gsap from 'gsap'; + +/** + * Animation phases configuration + */ +const PHASES = { + EMERGE: 'emerge', + WALK_LEFT: 'walkLeft', + WALK_RIGHT: 'walkRight', + TRANSFORM: 'transform', +}; + +/** + * Hook to manage the complete fox mascot animation sequence using GSAP + * @param {Object} foxRef - Ref to FoxModel component + * @param {Function} onComplete - Callback when animation completes + * @param {Function} onPhaseChange - Callback when animation phase changes + * @returns {Object} Animation control methods + */ +const useAnimationSequence = (foxRef, onComplete, onPhaseChange) => { + const timeline = useRef(null); + const isPlaying = useRef(false); + const currentPhase = useRef(null); + + /** + * Set current phase and notify + */ + const setPhase = useCallback((phase) => { + currentPhase.current = phase; + if (onPhaseChange) { + onPhaseChange(phase); + } + }, [onPhaseChange]); + + /** + * Calculate visible area at z=0 using camera frustum math + * Formula: visibleHeight = 2 * tan(FOV/2) * distance + * Camera: FOV=50, position z=100 + */ + const getVisibleBounds = useCallback(() => { + const fov = 50; // degrees (must match FoxMascot.jsx) + const cameraZ = 100; // must match FoxMascot.jsx + const aspectRatio = window.innerWidth / window.innerHeight; + + // Convert FOV to radians and calculate visible height at z=0 + const vFOVRad = (fov * Math.PI) / 180; + const visibleHeight = 2 * Math.tan(vFOVRad / 2) * cameraZ; + const visibleWidth = visibleHeight * aspectRatio; + + return { + halfWidth: visibleWidth / 2, + halfHeight: visibleHeight / 2 + }; + }, []); + + /** + * Calculate positions based on viewport using camera frustum math + */ + const getPositions = useCallback(() => { + const bounds = getVisibleBounds(); + + // Position at 90% towards each edge (very close to chatbot icon) + const rightEdge = bounds.halfWidth * 0.90; + const bottomEdge = -bounds.halfHeight * 0.85; + const leftEdge = -bounds.halfWidth * 0.75; + + console.log('[Fox] Visible bounds:', bounds); + console.log('[Fox] Positions: right=', rightEdge, 'bottom=', bottomEdge); + + return { + startX: rightEdge, // Right side (near chatbot) + startY: bottomEdge, // Bottom + centerX: 0, // Center + leftEdgeX: leftEdge, // Left side + groundY: bottomEdge // Ground level + }; + }, [getVisibleBounds]); + + /** + * Build and play the master timeline + */ + const play = useCallback(() => { + if (!foxRef.current || isPlaying.current) return; + + const foxModel = foxRef.current; + const group = foxModel.getGroup(); + if (!group) return; + + const { startX, startY, centerX, leftEdgeX, groundY } = getPositions(); + + // Scale adjusted for mathematical positioning (previous was too small) + // With FOV=50, z=100, the visible area is ~93 units, so we need larger scale + const targetScale = 0.001; + + console.log('[Fox] Animation starting with:', { startX, startY, groundY, targetScale }); + + // Kill any existing timeline + if (timeline.current) { + timeline.current.kill(); + } + + isPlaying.current = true; + + // Create master timeline + timeline.current = gsap.timeline({ + onComplete: () => { + isPlaying.current = false; + if (onComplete) onComplete(); + }, + }); + + const tl = timeline.current; + + // Phase 1: Emerge from bottom-right (0s - 1s) + tl.call(() => { + setPhase(PHASES.EMERGE); + foxModel.playAnimation('Fox_Jump', 0.2); + }) + .set(group.position, { x: startX, y: startY, z: 0 }) // Bottom-right corner + .set(group.rotation, { y: -Math.PI / 2 }) // Face left (-90 degrees) + // Pop in with scale + .to(group.scale, { + x: targetScale, y: targetScale, z: targetScale, + duration: 0.4, + ease: 'back.out(1.7)' + }) + // Jump up slightly then land + .to(group.position, { + y: groundY + 3, + duration: 0.3, + ease: 'power2.out' + }) + .to(group.position, { + y: groundY, + duration: 0.2, + ease: 'bounce.out' + }) + + // Phase 2: Walk left across screen (1s - 4s) + .call(() => { + setPhase(PHASES.WALK_LEFT); + foxModel.playAnimation('Fox_Walk', 0.3); + }) + .to(group.position, { + x: centerX, + duration: 2.5, + ease: 'none', + }) + // Stop in center, do idle animation + .call(() => foxModel.playAnimation('Fox_Idle', 0.2)) + .to({}, { duration: 0.8 }) // Pause for idle + .call(() => foxModel.playAnimation('Fox_Sit_Yes', 0.3)) + .to({}, { duration: 0.6 }) + + // Phase 3: Walk right / return (5s - 8s) + .call(() => { + setPhase(PHASES.WALK_RIGHT); + foxModel.playAnimation('Fox_Walk', 0.3); + }) + // Turn around to face right + .to(group.rotation, { y: Math.PI * 0.5, duration: 0.3, ease: 'power2.inOut' }) + .to(group.position, { + x: startX, + duration: 2.5, + ease: 'none', + }) + + // Phase 4: Transform back to icon (8s - 9s) + .call(() => { + setPhase(PHASES.TRANSFORM); + foxModel.playAnimation('Fox_Idle', 0.2); + }) + // Move to bottom right corner + .to(group.position, { + y: startY, + duration: 0.2, + ease: 'power2.in' + }) + // Shrink back to nothing + .to(group.scale, { + x: 0, y: 0, z: 0, + duration: 0.4, + ease: 'power2.in' + }); + + }, [foxRef, onComplete, onPhaseChange, setPhase, getPositions]); + + /** + * Skip to end of animation + */ + const skip = useCallback(() => { + if (timeline.current) { + timeline.current.progress(1); + timeline.current.kill(); + } + isPlaying.current = false; + if (onComplete) onComplete(); + }, [onComplete]); + + /** + * Pause animation + */ + const pause = useCallback(() => { + if (timeline.current) { + timeline.current.pause(); + } + }, []); + + /** + * Resume animation + */ + const resume = useCallback(() => { + if (timeline.current) { + timeline.current.resume(); + } + }, []); + + /** + * Cleanup on unmount + */ + useEffect(() => { + return () => { + if (timeline.current) { + timeline.current.kill(); + } + }; + }, []); + + return { + play, + skip, + pause, + resume, + isPlaying: () => isPlaying.current, + getCurrentPhase: () => currentPhase.current, + PHASES, + }; +}; + +export default useAnimationSequence; +export { PHASES }; diff --git a/src/components/index.jsx b/src/components/index.jsx index 782a130d..e6217b51 100644 --- a/src/components/index.jsx +++ b/src/components/index.jsx @@ -3,7 +3,8 @@ export * from './Core'; export * from './Form'; +export {default as Dialog} from './ui/Dialog'; export {default as Carousel} from './Carousel/Carousel'; export {default as EventCard} from './EventCard/EventCard'; export {default as SocialEmbed} from './SocialEmbed/SocialEmbed'; -export {default as TeamCard} from './TeamCard/TeamCard'; \ No newline at end of file +export {default as TeamCard} from './TeamCard/TeamCard'; diff --git a/src/components/ui/Dialog.jsx b/src/components/ui/Dialog.jsx new file mode 100644 index 00000000..87b3726a --- /dev/null +++ b/src/components/ui/Dialog.jsx @@ -0,0 +1,194 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import styles from "./Dialog.module.scss"; + +const ANIMATION_MS = 180; + +const getFocusableElements = (container) => { + if (!container) return []; + const selectors = [ + "a[href]", + "area[href]", + "button:not([disabled])", + "input:not([disabled]):not([type='hidden'])", + "select:not([disabled])", + "textarea:not([disabled])", + "iframe", + "object", + "embed", + "[contenteditable='true']", + "[tabindex]:not([tabindex='-1'])", + ]; + return Array.from(container.querySelectorAll(selectors.join(","))).filter( + (el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden") + ); +}; + +const Dialog = ({ + open, + defaultOpen = false, + onOpenChange, + closeOnBackdrop = true, + closeOnEsc = true, + size = "md", + initialFocusRef, + ariaLabel, + ariaLabelledBy, + ariaDescribedBy, + role = "dialog", + className = "", + overlayClassName = "", + contentClassName = "", + contentStyle, + children, +}) => { + const isControlled = open !== undefined; + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const isOpen = isControlled ? open : uncontrolledOpen; + const [shouldRender, setShouldRender] = useState(isOpen); + const contentRef = useRef(null); + const lastActiveElementRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setShouldRender(true); + return undefined; + } + const timer = setTimeout(() => setShouldRender(false), ANIMATION_MS); + return () => clearTimeout(timer); + }, [isOpen]); + + const requestClose = () => { + if (!isControlled) { + setUncontrolledOpen(false); + } + if (onOpenChange) { + onOpenChange(false); + } + }; + + useEffect(() => { + if (!isOpen) return undefined; + + const body = document.body; + const html = document.documentElement; + const previousOverflow = body.style.overflow; + const previousPaddingRight = body.style.paddingRight; + const scrollbarWidth = window.innerWidth - html.clientWidth; + + body.classList.add("fed-modal-open"); + body.style.overflow = "hidden"; + if (scrollbarWidth > 0) { + body.style.paddingRight = `${scrollbarWidth}px`; + } + + return () => { + body.classList.remove("fed-modal-open"); + body.style.overflow = previousOverflow; + body.style.paddingRight = previousPaddingRight; + }; + }, [isOpen]); + + useEffect(() => { + if (!isOpen) return undefined; + + lastActiveElementRef.current = document.activeElement; + const focusTarget = + initialFocusRef?.current || + getFocusableElements(contentRef.current)[0] || + contentRef.current; + + if (focusTarget && focusTarget.focus) { + focusTarget.focus(); + } + + return () => { + if ( + lastActiveElementRef.current && + lastActiveElementRef.current.focus + ) { + lastActiveElementRef.current.focus(); + } + }; + }, [isOpen, initialFocusRef]); + + useEffect(() => { + if (!isOpen || !closeOnEsc) return undefined; + + const handleKeyDown = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); + requestClose(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen, closeOnEsc]); + + const handleKeyDown = (event) => { + if (event.key !== "Tab") return; + + const focusable = getFocusableElements(contentRef.current); + if (focusable.length === 0) { + event.preventDefault(); + return; + } + + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + const active = document.activeElement; + + if (event.shiftKey) { + if (active === first || active === contentRef.current) { + event.preventDefault(); + last.focus(); + } + } else if (active === last) { + event.preventDefault(); + first.focus(); + } + }; + + const portalTarget = useMemo(() => { + if (typeof document === "undefined") return null; + return document.body; + }, []); + + if (!shouldRender || !portalTarget) return null; + + return createPortal( +
+
{ + if (!closeOnBackdrop) return; + if (event.target === event.currentTarget) { + requestClose(); + } + }} + /> +
+ {children} +
+
, + portalTarget + ); +}; + +export default Dialog; diff --git a/src/components/ui/Dialog.module.scss b/src/components/ui/Dialog.module.scss new file mode 100644 index 00000000..b981cda9 --- /dev/null +++ b/src/components/ui/Dialog.module.scss @@ -0,0 +1,102 @@ +.dialogRoot { + position: fixed; + inset: 0; + z-index: 3000; + display: flex; + align-items: center; + justify-content: center; + padding: clamp(16px, 2vw, 28px); +} + +.dialogOverlay { + position: absolute; + inset: 0; + /* Darkened overlay for better contrast against the black modal */ + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + opacity: 0; + /* Smoother, premium easing curve */ + transition: opacity 250ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.dialogContent { + position: relative; + z-index: 1; + box-sizing: border-box; + /* Flex layout makes organizing inner headers/footers much easier */ + display: flex; + flex-direction: column; + + width: min(92vw, var(--dialog-max-width, 560px)); + max-height: min(90vh, var(--dialog-max-height, 860px)); + overflow: auto; + /* Prevents the background page from scrolling when scrolling inside the modal */ + overscroll-behavior: contain; + + /* Changed from grey to pure black */ + background: var(--dialog-surface, #000000); + /* Slightly more visible border to frame the black background */ + border: var(--dialog-border, 1px solid rgba(255, 255, 255, 0.12)); + border-radius: var(--dialog-radius, 16px); + padding: var(--dialog-padding, 1.5rem); + + /* Deepened shadow */ + box-shadow: var(--dialog-shadow, 0 24px 48px -12px rgba(0, 0, 0, 1)); + + transform: translateY(12px) scale(0.96); + opacity: 0; + transition: transform 300ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms ease; +} + +.dialogRoot[data-state="open"] .dialogOverlay { + opacity: 1; +} + +.dialogRoot[data-state="open"] .dialogContent { + opacity: 1; + transform: translateY(0) scale(1); +} + +/* Size Variants */ +.dialogRoot[data-size="sm"] { + --dialog-max-width: 420px; +} + +.dialogRoot[data-size="md"] { + --dialog-max-width: 560px; +} + +.dialogRoot[data-size="lg"] { + --dialog-max-width: 760px; +} + +.dialogRoot[data-size="xl"] { + --dialog-max-width: 960px; +} + +.dialogRoot[data-size="full"] { + --dialog-max-width: 100vw; + --dialog-max-height: 100vh; + --dialog-radius: 0; + --dialog-padding: 0; + /* Removes border for full screen */ + --dialog-border: none; +} + +/* Accessibility */ +@media (prefers-reduced-motion: reduce) { + .dialogOverlay, + .dialogContent { + transition: none; + } +} + +/* Mobile Adjustments */ +@media (max-width: 640px) { + .dialogContent { + width: min(94vw, var(--dialog-max-width, 560px)); + max-height: 92vh; + /* Slightly smaller padding on mobile */ + --dialog-padding: 1.25rem; + } +} \ No newline at end of file diff --git a/src/components/ui/ModalCard.module.scss b/src/components/ui/ModalCard.module.scss new file mode 100644 index 00000000..fb3c2d7b --- /dev/null +++ b/src/components/ui/ModalCard.module.scss @@ -0,0 +1,121 @@ +.card { + position: relative; + width: min(100%, 30rem); + padding: 2rem 1.8rem 1.6rem 1.8rem; + border-radius: 20px; + color: #ffffff; + background: + radial-gradient(circle at top left, rgba(255, 120, 0, 0.08), transparent 40%), + #0b0b0b; + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: + 0 30px 80px rgba(0, 0, 0, 0.85), + 0 0 0 1px rgba(255, 255, 255, 0.04) inset; + backdrop-filter: blur(14px); + overflow: hidden; +} + +.card::before { + content: ""; + position: absolute; + inset: 0; + border-radius: 20px; + padding: 1px; + background: linear-gradient( + 120deg, + rgba(255, 120, 0, 0.4), + transparent 40%, + transparent 60%, + rgba(255, 120, 0, 0.3) + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +.cardLg { + width: min(100%, 42rem); +} + +.cardXl { + width: min(100%, 58rem); +} + +.cardWide { + width: min(100%, 72rem); +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.4rem; +} + +.title { + font-size: 1.15rem; + font-weight: 600; + letter-spacing: 0.5px; +} + +.subtitle { + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.55); + margin-top: 0.25rem; +} + +.closeBtn { + height: 36px; + width: 36px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + color: #ffffff; +} + +.divider { + height: 1px; + width: 100%; + background: linear-gradient( + to right, + transparent, + rgba(255, 255, 255, 0.1), + transparent + ); + margin-bottom: 1.5rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.note { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.6); +} + +@media (max-width: 640px) { + .card, + .cardLg, + .cardXl, + .cardWide { + padding: 1.6rem 1.35rem 1.4rem 1.35rem; + } +} diff --git a/src/data/Sponser.json b/src/data/Sponser.json index 9de63e67..9bb27730 100644 --- a/src/data/Sponser.json +++ b/src/data/Sponser.json @@ -1,25 +1,22 @@ [ - { - "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b1e566f93e724d817493_eef89a14894ab60c2de4591433117cb8_347615ad16304501c571bf1208b83615bc2eb3b7.jpg" - }, - { - "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b277c3dff77928f42d07_42506cd40d9f845726c5b38fa6135dab_1737828471512.jpeg" - }, - { - "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898aa1700b8583e9b0cea09_9a90e8c4f7933718410931fff16e1d67d0b1a728%20(1)-modified.png" - }, - { - "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4a473e0c91daf83aee0_SR%20burger%20point%202.png" - }, - - { - "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898a9a97a1a9e6121d28cfb_988e7f74979dc9924fdc07590a81526d0d19ddc4.png" - }, - - { - "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4713f09aa52852051a1_Adda%20Cafe%20-%20Logo%202.png" - }, - + { + "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b1e566f93e724d817493_eef89a14894ab60c2de4591433117cb8_347615ad16304501c571bf1208b83615bc2eb3b7.jpg" + }, + { + "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b277c3dff77928f42d07_42506cd40d9f845726c5b38fa6135dab_1737828471512.jpeg" + }, + { + "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898aa1700b8583e9b0cea09_9a90e8c4f7933718410931fff16e1d67d0b1a728%20(1)-modified.png" + }, + { + "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4a473e0c91daf83aee0_SR%20burger%20point%202.png" + }, + { + "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898a9a97a1a9e6121d28cfb_988e7f74979dc9924fdc07590a81526d0d19ddc4.png" + }, + { + "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4713f09aa52852051a1_Adda%20Cafe%20-%20Logo%202.png" + }, { "image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474142/WebImages/idm0kbdoybdmiklcgvkv.png" }, @@ -59,17 +56,43 @@ { "image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474142/WebImages/jvvudiuohzmgczo188us.png" }, - { "image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723476284/WebImages/t4bje7l7nqvusixwr423.jpg" }, { "image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474152/WebImages/ox8zpeciavvell8jvmgd.png" }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618449/Confique_sgmn7g.png" + }, { "image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474144/WebImages/tqkublfvrp45lut6pvva.png" }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618655/Anime_no_Sekai_mapjx2.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618655/Teology_od0lpf.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Beardo_nzyawr.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Exsolvia_rmueuy.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Pure_Veggie_Bliss_jkhyzu.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Marg_tara_y1mied.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618657/Kiti_Kitchen_oekgkf.png" + }, + { + "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618658/Short_Cut_ngohps.png" + }, { "image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474145/WebImages/ctvvq74mtiltxhiniwsf.png" } -] +] \ No newline at end of file diff --git a/src/features/Modals/EditProfile/EditImage.jsx b/src/features/Modals/EditProfile/EditImage.jsx index bd7aee0c..73caa28b 100644 --- a/src/features/Modals/EditProfile/EditImage.jsx +++ b/src/features/Modals/EditProfile/EditImage.jsx @@ -3,7 +3,8 @@ import AvatarEditor from "react-avatar-editor"; import { FaUpload } from "react-icons/fa"; import style from "./styles/editImage.module.scss"; import AuthContext from "../../../context/AuthContext"; -import { Button } from "../../../components"; +import { Button, Dialog } from "../../../components"; +import modalCard from "../../../components/ui/ModalCard.module.scss"; import { X } from "lucide-react"; import { Alert, MicroLoading } from "../../../microInteraction"; import { api } from "../../../services"; @@ -176,41 +177,20 @@ const EditImage = (props) => { }; return ( -
{ + if (!next) closeModal(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", }} > -
-
-
+
@@ -316,10 +296,8 @@ const EditImage = (props) => { )}
-
-
-
+ ); }; diff --git a/src/features/Modals/EditProfile/EditProfile.jsx b/src/features/Modals/EditProfile/EditProfile.jsx index 8b6f3d8b..2d3cd750 100644 --- a/src/features/Modals/EditProfile/EditProfile.jsx +++ b/src/features/Modals/EditProfile/EditProfile.jsx @@ -1,7 +1,8 @@ import { useState, useEffect, useContext } from "react"; import AuthContext from "../../../context/AuthContext"; import styles from "./styles/EditProfile.module.scss"; -import { Button, Input } from "../../../components"; +import { Button, Dialog, Input } from "../../../components"; +import modalCard from "../../../components/ui/ModalCard.module.scss"; import { X } from "lucide-react"; import AOS from "aos"; import "aos/dist/aos.css"; @@ -130,46 +131,25 @@ const EditProfile = ({ handleModalClose }) => { }; return ( -
{ + if (!next) handleModalClose(); + }} + contentStyle={{ + "--dialog-surface": "transparent", + "--dialog-padding": "0", + "--dialog-border": "none", + "--dialog-shadow": "none", }} > -
+
- <> -
-

@@ -177,6 +157,7 @@ const EditProfile = ({ handleModalClose }) => {

- {/*
*/}
-
+ ); }; -export default EventModal; \ No newline at end of file +export default EventModal; diff --git a/src/features/Modals/Event/EventModal/styles/EventModal.module.scss b/src/features/Modals/Event/EventModal/styles/EventModal.module.scss index 1810df49..b491df1f 100644 --- a/src/features/Modals/Event/EventModal/styles/EventModal.module.scss +++ b/src/features/Modals/Event/EventModal/styles/EventModal.module.scss @@ -2,11 +2,7 @@ .card { width: 32rem; - background: linear-gradient( - 90deg, - rgba(32, 32, 32, 0.95) -11.52%, - rgba(37, 37, 37, 0.95) 106.29% - ); + background: linear-gradient(145deg, rgba(18, 18, 22, 0.96), rgba(10, 10, 14, 0.96)); font-size: 0.75rem; color: white; text-align: center; @@ -14,8 +10,9 @@ // top: 2rem; // left: 35%; z-index: 5; - border-radius: 1.26863rem; - border: 1.2px solid rgba(214, 214, 214, 0.81); + border-radius: 1.1rem; + border: 1px solid rgba(255, 138, 0, 0.28); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.5); overflow: hidden; } @@ -119,7 +116,7 @@ background-clip: text; } -.eventname p { +.eventMeta { font-size: 0.9rem; display: flex; align-items: center; @@ -127,6 +124,17 @@ color: #fff; } +.price { + display: flex; + align-items: center; +} + +.priceValue { + display: flex; + align-items: center; + color: #fff; +} + .registerbtn button { border-radius: 0.35788rem; background: var( @@ -161,7 +169,7 @@ } .closeModal:hover { - color: #f97507; + color: #ff8a00; } .card button { @@ -169,9 +177,9 @@ } .share { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgb(143, 139, 139); - color: #f97507; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 138, 0, 0.4); + color: #ff9a42; position: absolute; top: 0.8rem; right: 2.8rem; @@ -317,7 +325,7 @@ input:checked + .slider:before { .eventname { font-size: 0.95rem; } - .eventname p{ + .eventMeta{ font-size: 0.7rem; } @@ -498,4 +506,4 @@ input:checked + .slider:before { .backimg{ height: auto; } -} \ No newline at end of file +} diff --git a/src/features/Modals/Event/EventStats/EventStats.jsx b/src/features/Modals/Event/EventStats/EventStats.jsx index 471d3872..fd7558d1 100644 --- a/src/features/Modals/Event/EventStats/EventStats.jsx +++ b/src/features/Modals/Event/EventStats/EventStats.jsx @@ -11,6 +11,7 @@ import defaultImg from "../../../../assets/images/defaultImg.jpg"; import { api } from "../../../../services"; import styles from "../EventModal/styles/EventModal.module.scss"; import AuthContext from "../../../../context/AuthContext"; +import { Dialog } from "../../../../components"; const EventStats = ({ onClosePath }) => { const navigate = useNavigate(); @@ -151,185 +152,143 @@ const EventStats = ({ onClosePath }) => { const yearCounts = year || {}; return ( -
{ + if (!next) handleModalClose(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", }} >
-
- {data && ( - <> - - - + + + + +
+ {isLoading ? ( + - -
- {isLoading ? ( - - ) : ( -
+ + + -
+
+
+ {info.eventTitle} +
+ {authCtx.user.access === "ADMIN" && (
- {info.eventTitle} +
- {authCtx.user.access === "ADMIN" && ( + )} +
+ +
+
+ {/* First column for the toggle switch and total count */} +
- -
- )} -
- -
-
- {/* First column for the toggle switch and total count */} -
-
- - {viewTeams ? "Back to Users" : "Switch to Teams"} - - -
- - Total{" "} - {viewTeams - ? "Registered Teams" - : "Registered Users"}{" "} - :{" "} - - {viewTeams - ? data[0]?.regTeamNames?.length || 0 - : data[0]?.totalRegistrationCount || 0} - + {viewTeams ? "Back to Users" : "Switch to Teams"} +
- {/* Second column for year counts and download */} { fontWeight: "500", textAlign: "left", marginBottom: "1rem", - marginLeft: "1.5rem", - marginTop: "0.5rem", + marginLeft: "2rem", }} > - Year Counts: -
- {Object.keys(yearCounts).length > 0 ? ( - Object.entries(yearCounts).map( - ([year, count]) => ( -
- - {year}: - {" "} - {count} -
- ) - ) - ) : ( - No data available - )} -
+ {viewTeams + ? data[0]?.regTeamNames?.length || 0 + : data[0]?.totalRegistrationCount || 0} +
+ + {/* Second column for year counts and download */} + + Year Counts: +
+ {Object.keys(yearCounts).length > 0 ? ( + Object.entries(yearCounts).map(([year, count]) => ( +
+ + {year}: + {" "} + {count} +
+ )) + ) : ( + No data available + )} +
+
+
- setSearchQuery(e.target.value)} - className={styles.searchInput} - /> + setSearchQuery(e.target.value)} + className={styles.searchInput} + /> -
- {isSearching ? ( - - ) : viewTeams ? ( - filteredTeams && filteredTeams.length > 0 ? ( - filteredTeams.map((team, index) => ( -
- Team -
{team}
-
- )) - ) : ( -
- - No Teams found - -
- ) - ) : filteredUsers && filteredUsers.length > 0 ? ( - filteredUsers.map((user, index) => ( +
+ {isSearching ? ( + + ) : viewTeams ? ( + filteredTeams && filteredTeams.length > 0 ? ( + filteredTeams.map((team, index) => (
User -
{user}
+
{team}
)) ) : ( @@ -449,20 +409,42 @@ const EventStats = ({ onClosePath }) => { }} > - No Users found + No Teams found
- )} -
+ ) + ) : filteredUsers && filteredUsers.length > 0 ? ( + filteredUsers.map((user, index) => ( +
+ User +
{user}
+
+ )) + ) : ( +
+ No Users found +
+ )}
- )} -
- - )} -
+
+ )} +
+ + )}
-
+ ); }; diff --git a/src/features/Modals/Event/LiveEventPopup/LiveEventPopup.jsx b/src/features/Modals/Event/LiveEventPopup/LiveEventPopup.jsx index 6d8fc840..c3c4551b 100644 --- a/src/features/Modals/Event/LiveEventPopup/LiveEventPopup.jsx +++ b/src/features/Modals/Event/LiveEventPopup/LiveEventPopup.jsx @@ -83,4 +83,4 @@ const LiveEventPopup = () => { ); }; -export default LiveEventPopup; +export default LiveEventPopup; \ No newline at end of file diff --git a/src/features/Modals/Event/LiveEventPopup/styles/LiveEventPopup.module.scss b/src/features/Modals/Event/LiveEventPopup/styles/LiveEventPopup.module.scss index b9170599..80ebf1a0 100644 --- a/src/features/Modals/Event/LiveEventPopup/styles/LiveEventPopup.module.scss +++ b/src/features/Modals/Event/LiveEventPopup/styles/LiveEventPopup.module.scss @@ -128,4 +128,4 @@ .popupContent img { width: 100%; } -} +} \ No newline at end of file diff --git a/src/features/Modals/Event/QRCodeModal/QRCodeModal.jsx b/src/features/Modals/Event/QRCodeModal/QRCodeModal.jsx index 46e3b5b2..09c5e4d3 100644 --- a/src/features/Modals/Event/QRCodeModal/QRCodeModal.jsx +++ b/src/features/Modals/Event/QRCodeModal/QRCodeModal.jsx @@ -1,17 +1,17 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { X } from 'lucide-react'; -import { QRCodeSVG } from 'qrcode.react'; -import AuthContext from '../../../../context/AuthContext'; -import { RecoveryContext } from '../../../../context/RecoveryContext'; -import { api } from '../../../../services'; -import { Alert } from '../../../../microInteraction'; -import style from './styles/QRCodeModal.module.scss'; +import React, { useState, useEffect, useContext } from "react"; +import { X } from "lucide-react"; +import { QRCodeSVG } from "qrcode.react"; +import AuthContext from "../../../../context/AuthContext"; +import { RecoveryContext } from "../../../../context/RecoveryContext"; +import { api } from "../../../../services"; +import style from "./styles/QRCodeModal.module.scss"; +import { Dialog } from "../../../../components"; -const QRCodeModal = ({ onClose, eventId, onAttendanceMarked }) => { +const QRCodeModal = ({ onClose, eventId }) => { const [qrCodeData, setQrCodeData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [attendanceMarked, setAttendanceMarked] = useState(false); + const authCtx = useContext(AuthContext); const recoveryCtx = useContext(RecoveryContext); @@ -19,123 +19,109 @@ const QRCodeModal = ({ onClose, eventId, onAttendanceMarked }) => { fetchAttendanceCode(); }, [eventId]); - useEffect(() => { - if (onAttendanceMarked) { - setAttendanceMarked(true); - } - }, [onAttendanceMarked]); - const fetchAttendanceCode = async () => { try { setIsLoading(true); setError(null); const teamCode = recoveryCtx.teamCode; - + let url = `/api/form/attendanceCode/${eventId}`; - if (teamCode && teamCode.trim() !== '') { + if (teamCode && teamCode.trim() !== "") { url += `?teamCode=${encodeURIComponent(teamCode)}`; } - const token = localStorage.getItem('token'); + const token = localStorage.getItem("token"); const response = await api.get(url, { headers: { - 'Authorization': token - } + Authorization: token, + }, }); if (response.status === 200) { setQrCodeData(response.data.attendanceToken); } else { - throw new Error('Failed to fetch attendance code'); + throw new Error("Failed to fetch attendance code"); } - } catch (error) { - setError(error?.response?.data?.message || 'Failed to generate QR code. Please try again.'); + } catch (err) { + setError( + err?.response?.data?.message || + "Failed to generate QR code. Please try again." + ); } finally { setIsLoading(false); } }; - const handleClose = () => { - onClose(); - }; - - const handleRetry = () => { - fetchAttendanceCode(); - }; - - const handleAttendanceMarked = () => { - setAttendanceMarked(true); - }; - - const handleOK = () => { - onClose(); - }; - - const handleScanNewAttendee = () => { - setAttendanceMarked(false); - setQrCodeData(null); - fetchAttendanceCode(); - }; - return ( -
-
-
-
-
- -
-
Attendance QR Code
-
- -
- {isLoading ? ( -
-
-

Generating QR Code...

-
- ) : error ? ( -
-

{error}

- + { + if (!next) onClose(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", + }} + > +
+
+
+
Attendance QR Code
+
+
- ) : qrCodeData ? ( -
-
- +
+ +
+ {isLoading ? ( +
+
+

Generating QR Code...

- -
-

- Show this QR code to event organizers for attendance verification. -

-

- This attendance QR code can be used only once. Do not share it with others.

+ ) : error ? ( +
+

{error}

+
-
- ) : ( -
-

No attendance code available

-
- )} + ) : qrCodeData ? ( +
+
+ +
+ +
+

+ Show this QR code to event organizers for attendance verification. +

+

+ This QR code can be used only once. Do not share it. +

+
+
+ ) : ( +
+

No attendance code available.

+
+ )} +
-
+
); }; diff --git a/src/features/Modals/Event/QRCodeModal/styles/QRCodeModal.module.scss b/src/features/Modals/Event/QRCodeModal/styles/QRCodeModal.module.scss index 18b8f56e..40eb4d0f 100644 --- a/src/features/Modals/Event/QRCodeModal/styles/QRCodeModal.module.scss +++ b/src/features/Modals/Event/QRCodeModal/styles/QRCodeModal.module.scss @@ -1,113 +1,105 @@ .qrContainer { - position: fixed; - top: 0; - left: 0; width: 100%; height: 100%; - z-index: 50; display: flex; justify-content: center; align-items: center; - padding: 1rem; } -.overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(5px); - z-index: 5; +/* Premium dark static card */ +.card { + position: relative; + width: min(28rem, 94vw); + padding: 2rem 1.8rem 1.6rem 1.8rem; + border-radius: 20px; + + background: radial-gradient( + circle at top left, + rgba(255, 120, 0, 0.08), + transparent 40% + ), + #0b0b0b; + + border: 1px solid rgba(255, 255, 255, 0.06); + + box-shadow: + 0 30px 80px rgba(0, 0, 0, 0.85), + 0 0 0 1px rgba(255, 255, 255, 0.04) inset; + + backdrop-filter: blur(14px); + overflow: hidden; } -.maindiv { - z-index: 1000; - border-radius: 16px; - padding: 2rem; - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background: rgba(42, 42, 42, 0.95); - border: 2px solid #444; - width: 100%; - max-width: 450px; - min-height: 400px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +/* Static gradient border */ +.card::before { + content: ""; + position: absolute; + inset: 0; + border-radius: 20px; + padding: 1px; + background: linear-gradient( + 120deg, + rgba(255, 120, 0, 0.4), + transparent 40%, + transparent 60%, + rgba(255, 120, 0, 0.3) + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; } .header { - position: relative; display: flex; - justify-content: space-between; align-items: center; - margin-bottom: 2rem; - width: 100%; + justify-content: space-between; + margin-bottom: 1.8rem; } -.closebtn { - cursor: pointer; - position: absolute; - right: 0; - top: 0; - z-index: 20; - font-size: 1.2rem; +.title { + font-size: 1.15rem; + font-weight: 600; + letter-spacing: 0.5px; color: #ffffff; - transition: color 0.3s ease; - padding: 0.5rem; - - &:hover { - color: #D14206; - transition: .2s linear; - } } -.title{ - font-weight:700; - margin-left:6.5rem; - font-size:1rem; - background: linear-gradient( - 260deg, - rgba(255, 190, 11, 0.84) -29.7%, - rgba(244, 43, 3, 0.84) 128.34% -); -color: transparent; --webkit-background-clip: text; -background-clip: text; -} +.closebtn { + height: 36px; + width: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); +} .content { - flex: 1; display: flex; flex-direction: column; - justify-content: center; align-items: center; text-align: center; - width: 100%; + gap: 1.2rem; } .loadingContainer { display: flex; flex-direction: column; align-items: center; - gap: 1rem; - color: #f97507; - - p { - margin: 0; - font-size: 1rem; - } + gap: 0.9rem; + color: rgba(255, 255, 255, 0.75); } .spinner { - width: 40px; - height: 40px; - border: 3px solid rgba(249, 117, 7, 0.3); - border-top: 3px solid #f97507; + width: 38px; + height: 38px; + border: 3px solid rgba(255, 255, 255, 0.15); + border-top: 3px solid #ffffff; border-radius: 50%; animation: spin 1s linear infinite; } @@ -121,44 +113,35 @@ background-clip: text; display: flex; flex-direction: column; align-items: center; - gap: 1rem; + gap: 0.9rem; color: #ff6b6b; - - p { - margin: 0; - font-size: 1rem; - } } .retryBtn { - background: linear-gradient(135deg, #f97507, #ff8c42); + background: #ffffff; border: none; - border-radius: 8px; - padding: 0.75rem 1.5rem; - color: white; + border-radius: 10px; + padding: 0.6rem 1.3rem; + color: #000000; font-weight: 600; cursor: pointer; - transition: transform 0.2s ease; - - } .qrContent { display: flex; flex-direction: column; align-items: center; - gap: 1.5rem; + gap: 1rem; width: 100%; } .qrWrapper { - padding: 1.5rem; - border-radius: 12px; + padding: 1.2rem; + border-radius: 16px; display: flex; justify-content: center; - background-color: #ffffff; align-items: center; - backdrop-filter: blur(10px); + background-color: #ffffff; } .qrCode { @@ -168,229 +151,25 @@ background-clip: text; .codeInfo { display: flex; flex-direction: column; - align-items: center; - gap: 0.5rem; - background: rgba(250, 250, 250, 0.1); - padding: 1rem; - border-radius: 8px; + gap: 0.6rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 0.9rem 1rem; + border-radius: 12px; width: 100%; } -.codeLabel { - margin: 0; - font-size: 0.9rem; - color: #f97507; - font-weight: 600; -} - -.codeValue { - margin: 0; - font-size: 1.2rem; - font-weight: 700; - color: #f97507; - letter-spacing: 1px; - font-family: 'Courier New', monospace; - transition: color 0.2s ease; - - &:hover { - color: #ff8c42; - } -} - .instruction { margin: 0; - font-size: 0.9rem; - color: #ccc; - line-height: 1.4; + font-size: 0.88rem; + color: rgba(255, 255, 255, 0.7); + line-height: 1.5; } .noCodeContainer { display: flex; flex-direction: column; align-items: center; - gap: 1rem; - color: #ffa500; - - p { - margin: 0; - font-size: 1rem; - } -} - -.successContainer { - display: flex; - flex-direction: column; - align-items: center; - gap: 1.5rem; - color: #4CAF50; - text-align: center; - - h3 { - margin: 0; - font-size: 1.5rem; - font-weight: 600; - color: #4CAF50; - } - - p { - margin: 0; - font-size: 1rem; - color: #ccc; - } -} - -.successIcon { - font-size: 3rem; - color: #4CAF50; - background: rgba(76, 175, 80, 0.1); - border-radius: 50%; - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - border: 3px solid #4CAF50; -} - -.buttonGroup { - display: flex; - gap: 1rem; - margin-top: 1rem; -} - -.okBtn { - background: linear-gradient(135deg, #4CAF50, #45a049); - border: none; - border-radius: 8px; - padding: 0.75rem 1.5rem; - color: white; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s ease; - - &:hover { - transform: translateY(-2px); - } -} - -.scanNewBtn { - background: linear-gradient(135deg, #f97507, #ff8c42); - border: none; - border-radius: 8px; - padding: 0.75rem 1.5rem; - color: white; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s ease; - - &:hover { - transform: translateY(-2px); - } -} - -// Responsive design -@media (max-width: 768px) { - .qrContainer { - padding: 0.5rem; - } - - .maindiv { - width: 100%; - padding: 1.5rem; - min-height: 350px; - border-radius: 12px; - } - - .title { - font-size: 1.3rem; - } - - .qrWrapper { - padding: 1rem; - } - - .qrCode { - width: 180px; - height: 180px; - } -} - -@media (max-width: 480px) { - .qrContainer { - padding: 0.25rem; - } - - .maindiv { - padding: 1rem; - min-height: 320px; - } - - .title { - font-size: 1.2rem; - } - - .header { - margin-bottom: 1.5rem; - } - - .qrWrapper { - padding: 0.75rem; - } - - .qrCode { - width: 160px; - height: 160px; - } - - .codeInfo { - padding: 0.75rem; - } - - .instruction { - font-size: 0.85rem; - } - - .successContainer { - gap: 1rem; - - h3 { - font-size: 1.3rem; - } - - p { - font-size: 0.9rem; - } - } - - .successIcon { - width: 60px; - height: 60px; - font-size: 2rem; - } - - .buttonGroup { - flex-direction: column; - gap: 0.75rem; - } - - .okBtn, .scanNewBtn { - padding: 0.6rem 1.2rem; - font-size: 0.9rem; - } -} - -@media (max-width: 360px) { - .maindiv { - padding: 0.75rem; - min-height: 300px; - } - - .title { - font-size: 1.1rem; - } - - .qrCode { - width: 140px; - height: 140px; - } -} \ No newline at end of file + gap: 0.75rem; + color: rgba(255, 255, 255, 0.7); +} \ No newline at end of file diff --git a/src/features/Modals/Event/ShareModal/ShareModal.jsx b/src/features/Modals/Event/ShareModal/ShareModal.jsx index a900555f..0ae60e85 100644 --- a/src/features/Modals/Event/ShareModal/ShareModal.jsx +++ b/src/features/Modals/Event/ShareModal/ShareModal.jsx @@ -1,122 +1,97 @@ -import React from 'react'; +import React, { useEffect } from "react"; import { ShareSocial } from "react-share-social"; import style from "./styles/ShareModal.module.scss"; -import { X } from 'lucide-react'; -import AOS from 'aos'; -import 'aos/dist/aos.css'; +import { X } from "lucide-react"; +import AOS from "aos"; +import "aos/dist/aos.css"; +import { Dialog } from "../../../../components"; + +const Share = ({ onClose, urlpath, teamData }) => { + useEffect(() => { + AOS.init({ once: true }); + }, []); + + const shareUrl = urlpath ? urlpath : teamData?.teamCode; + const titleText = urlpath ? "Share Link" : teamData?.teamName; + const subtitleText = urlpath + ? "Send this link to others" + : "Invite others using this code"; -const Share = (props) => { - const{onClose,urlpath,teamData}=props; const sharestyle = { root: { - background: 'rgba(42, 42, 42, 0.9)', - borderRadius: '10px', - border: '2px solid #444', - width: '22rem', - height: '15rem', - boxShadow: '0 6px 10px 3px rgba(24, 15, .3)', - color: '#f97507', - padding: '1rem', - position: 'relative', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', + background: "transparent", + borderRadius: "0", + border: "none", + width: "100%", + height: "auto", + boxShadow: "none", + padding: "0", + color: "#ffffff", }, copyContainer: { - border: '1px solid #f97507', - background: 'rgba(0, 0, 0, 0.4)', - borderRadius: '5px', - padding: '0.5rem', - margin: '0.5rem 0', + border: "1px solid rgba(255,255,255,0.08)", + background: "rgba(255,255,255,0.04)", + borderRadius: "10px", + padding: "0.75rem 1rem", + margin: "1rem 0", + fontSize: "0.85rem", + color: "#ffffff", }, copyUrl: { - overflowY: 'scroll', - scrollbarWidth: 'none', - msOverflowStyle: 'none', + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }, title: { - color: '#f97507', - fontStyle: 'italic', - marginBottom: '1rem', + display: "none", }, }; return ( -
-
-
- {urlpath ?
- -
-
Share
- console.log(data)} - />
- : -
- + { + if (!next) onClose(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", + }} + > +
+
+
+
+
{titleText}
+
{subtitleText}
+
+ +
+ +
+
+ +
+ + console.log(data)} + />
-
{teamData.teamName}
- console.log(data)} - />
- } -
-
+ ); -} +}; -export default Share; +export default Share; \ No newline at end of file diff --git a/src/features/Modals/Event/ShareModal/ShareTeamData.jsx b/src/features/Modals/Event/ShareModal/ShareTeamData.jsx index c2075a24..cf79ae8b 100644 --- a/src/features/Modals/Event/ShareModal/ShareTeamData.jsx +++ b/src/features/Modals/Event/ShareModal/ShareTeamData.jsx @@ -14,6 +14,8 @@ import { import styles from "./styles/ShareTeamData.module.scss"; import { X } from "lucide-react"; import JSConfetti from "js-confetti"; +import { Dialog } from "../../../../components"; +import modalCard from "../../../../components/ui/ModalCard.module.scss"; const jsConfetti = new JSConfetti(); @@ -29,12 +31,6 @@ const ShareTeamData = ({ onClose, teamData, successMessage }) => { jsConfetti.addConfetti({ confettiColors: ["#FF8A00", "#FFD700", "#FF4500", "#FF69B4"], }); - document.body.style.overflow = "hidden"; - - return () => { - - document.body.style.overflow = ""; - }; }, []); const handleCopy = () => { @@ -45,17 +41,38 @@ const ShareTeamData = ({ onClose, teamData, successMessage }) => { }; const handleClose = () => { - document.body.style.overflow = ""; // Re-enable scrolling onClose(); }; return ( -
-
-
-
- + { + if (!next) handleClose(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", + }} + > +
+
+
+
+ {successMessage ? "Registration Successful" : "Team Details"} +
+
+ Share your team information +
+
+
+
{/* Conditional rendering for successMessage */} {successMessage && ( @@ -72,10 +89,7 @@ const ShareTeamData = ({ onClose, teamData, successMessage }) => {
Your Team Code: {teamCode}

-
@@ -163,7 +177,7 @@ const ShareTeamData = ({ onClose, teamData, successMessage }) => {
)}
-
+ ); }; diff --git a/src/features/Modals/Event/ShareModal/styles/ShareModal.module.scss b/src/features/Modals/Event/ShareModal/styles/ShareModal.module.scss index 9b93bf74..a0c6c95f 100644 --- a/src/features/Modals/Event/ShareModal/styles/ShareModal.module.scss +++ b/src/features/Modals/Event/ShareModal/styles/ShareModal.module.scss @@ -1,48 +1,107 @@ .shareContainer { - position: fixed; - top: 0; - left: 0; width: 100%; height: 100%; - z-index: 50; -} - -.overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(5px); - z-index: 5; } .maindiv { - z-index: 1000; - - border-radius: 10px; - padding: 2rem; position: relative; display: flex; justify-content: center; align-items: center; - margin-top: 9rem; } -.closebtn { - cursor: pointer; +/* Premium static dark card */ +.card { + position: relative; + width: min(28rem, 94vw); + padding: 2rem 1.8rem 1.6rem 1.8rem; + border-radius: 20px; + + background: radial-gradient( + circle at top left, + rgba(255, 120, 0, 0.08), + transparent 40% + ), + #0c0c0c; + + border: 1px solid rgba(255, 255, 255, 0.06); + + box-shadow: + 0 30px 80px rgba(0, 0, 0, 0.8), + 0 0 0 1px rgba(255, 255, 255, 0.04) inset; + + backdrop-filter: blur(14px); + overflow: hidden; +} + +/* Static gradient border */ +.card::before { + content: ""; position: absolute; - right: 2.5rem; - top: 2.4rem; - font-size: 1.2rem; - color: white; - z-index: 20; + inset: 0; + border-radius: 20px; + padding: 1px; + background: linear-gradient( + 120deg, + rgba(255, 120, 0, 0.4), + transparent 40%, + transparent 60%, + rgba(255, 120, 0, 0.3) + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.8rem; } -.closebtn:hover { - color: #D14206; - transition:.2s linear; +.titleWrapper { + display: flex; + flex-direction: column; } +.title { + font-size: 1.15rem; + font-weight: 600; + letter-spacing: 0.6px; + color: #ffffff; +} +.subtitle { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.45); + margin-top: 0.25rem; +} + +.closebtn { + height: 36px; + width: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.divider { + height: 1px; + width: 100%; + background: linear-gradient( + to right, + transparent, + rgba(255, 255, 255, 0.1), + transparent + ); + margin-bottom: 1.5rem; +} \ No newline at end of file diff --git a/src/features/Modals/Event/ShareModal/styles/ShareTeamData.module.scss b/src/features/Modals/Event/ShareModal/styles/ShareTeamData.module.scss index 9e06a287..ac379add 100644 --- a/src/features/Modals/Event/ShareModal/styles/ShareTeamData.module.scss +++ b/src/features/Modals/Event/ShareModal/styles/ShareTeamData.module.scss @@ -1,163 +1,140 @@ -.shareContainer { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 50; -} - -.overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(5px); - z-index: 5; - // text-align: left; -} - .maindiv { - z-index: 1000; - background: rgba(42, 42, 42, 0.9); - border-radius: 10px; gap: 0.7rem; - padding: 2rem; position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; height: auto; - padding: 30px; overflow-y: auto; - margin-top: 5.5rem; margin-left: auto; margin-right: auto; - width: 30rem; - box-shadow: 0 6px 10px 3px rgba(24, 15, 15, 0.3); - color: #f97507; - word-wrap: break-word; // Handles long words - word-break: break-word; // Ensures text breaks within words if needed - overflow-wrap: break-word; // For better compatibility - white-space: normal; // Prevents single-line text overflow + width: min(100%, 30rem); + max-height: min(80vh, 36rem); + color: #ffffff; + word-wrap: break-word; + word-break: break-word; + overflow-wrap: break-word; + white-space: normal; + padding: 2.5rem 1.5rem 1.5rem; // space for close button } - +/* FIXED CLOSE BUTTON POSITION */ .closebtn { - cursor: pointer; position: absolute; - right: 1rem; top: 1rem; + right: 1rem; + cursor: pointer; font-size: 1.2rem; color: white; z-index: 20; } .closebtn:hover { - color: #d14206; - transition: 0.2s linear; + color: #D14206; + transition: .2s linear; } .copyContainer { - border: 1px solid #d14206; - background: rgba(0, 0, 0, 0.4); - border-radius: 5px; - padding: 0.5rem; - margin: 0.5rem 0; - // text-align: left; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 0.75rem 1rem; + margin: 0.75rem 0; display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem; color: #ffffff90; } +.copyContainer p { + flex: 1 1 14rem; + min-width: 0; + margin: 0; +} + .copyUrl { display: block; max-width: 100%; overflow: hidden; - word-wrap: break-word; // Ensure long URLs break properly - text-overflow: ellipsis; // Optionally add ellipsis for overflowing text - white-space: normal; // Allow text wrapping + word-wrap: break-word; + text-overflow: ellipsis; + white-space: normal; } -.shareTitle{ - font-weight:700; - margin-left:6.5rem; - font-size:1rem; +.shareTitle { + font-weight: 700; + margin-left: 0; + font-size: 1rem; background: linear-gradient( 260deg, rgba(255, 190, 11, 0.84) -29.7%, rgba(244, 43, 3, 0.84) 128.34% -); -color: transparent; --webkit-background-clip: text; -background-clip: text; + ); + color: transparent; + -webkit-background-clip: text; + background-clip: text; } -.registrationTitle{ - font-weight:bold; - // margin-left:-9.5rem; +.registrationTitle { + font-weight: bold; margin-top: 0.5rem; - font-size:1.5rem; + font-size: 1.5rem; background: linear-gradient( 260deg, rgba(255, 190, 11, 0.84) -29.7%, rgba(244, 43, 3, 0.84) 128.34% -); -color: transparent; --webkit-background-clip: text; -background-clip: text; -margin-bottom: 0.5rem; + ); + color: transparent; + -webkit-background-clip: text; + background-clip: text; + margin-bottom: 0.5rem; } .copyButton { - color: #ffffff90; - background: #2a2a2a; - border: none; - border-radius: 5px; - padding: 0.5rem 1rem; + color: #ffffff; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 10px; + padding: 0.5rem 0.9rem; cursor: pointer; font-size: 0.8rem; font-weight: 700; transition: 0.2s linear; - margin-left: 10px; - border-radius: 10px + margin-left: auto; + white-space: nowrap; } - -@media (max-width:500px){ - .maindiv{ - width: 20rem; - height: 60vh; - overflow-y: auto; - margin-top: 9rem; +@media (max-width: 500px) { + .maindiv { + width: 100%; + max-height: 80vh; + overflow-y: auto; + padding: 2.2rem 1rem 1rem; } - .copyContainer{ - font-size:0.8rem; + .copyContainer { + font-size: 0.8rem; } - .shareTitle{ - font-size: 0.8rem; - margin-left: 5rem; + .shareTitle { + font-size: 0.8rem; + margin-left: 0; } - .registrationTitle{ - font-size: 1rem; - // margin-top: -20px; - // margin-left: -2.5rem; + .registrationTitle { + font-size: 1rem; } .copyButton { - font-size: 0.6rem; - width:30%; -} - + font-size: 0.7rem; + width: auto; + } -} + .closebtn { + top: 0.8rem; + right: 0.8rem; + } +} \ No newline at end of file diff --git a/src/features/Modals/Event/TeamDetailsModal/TeamDetailsModal.jsx b/src/features/Modals/Event/TeamDetailsModal/TeamDetailsModal.jsx index 0315153d..90124cc1 100644 --- a/src/features/Modals/Event/TeamDetailsModal/TeamDetailsModal.jsx +++ b/src/features/Modals/Event/TeamDetailsModal/TeamDetailsModal.jsx @@ -5,6 +5,8 @@ import { FaCopy, FaCheck } from "react-icons/fa"; import { api } from "../../../../services"; import { Alert, MicroLoading } from "../../../../microInteraction"; import styles from "./style/TeamDetailsModal.module.scss"; +import { Dialog } from "../../../../components"; +import modalCard from "../../../../components/ui/ModalCard.module.scss"; const TeamDetailsModal = ({ isOpen, onClose, formId, eventTitle }) => { const [teamDetails, setTeamDetails] = useState(null); @@ -74,23 +76,33 @@ const TeamDetailsModal = ({ isOpen, onClose, formId, eventTitle }) => { } }; - const handleBackdropClick = (e) => { - if (e.target === e.currentTarget) { - onClose(); - } - }; - if (!isOpen) return null; return ( -
-
-
-

Team Details

-
+
{isLoading ? ( @@ -201,13 +213,13 @@ const TeamDetailsModal = ({ isOpen, onClose, formId, eventTitle }) => { )}
-
+
-
+ ); }; @@ -218,4 +230,4 @@ TeamDetailsModal.propTypes = { eventTitle: PropTypes.string, }; -export default TeamDetailsModal; \ No newline at end of file +export default TeamDetailsModal; diff --git a/src/features/Modals/Event/TeamDetailsModal/style/TeamDetailsModal.module.scss b/src/features/Modals/Event/TeamDetailsModal/style/TeamDetailsModal.module.scss index e82c50f0..4cc7fbd3 100644 --- a/src/features/Modals/Event/TeamDetailsModal/style/TeamDetailsModal.module.scss +++ b/src/features/Modals/Event/TeamDetailsModal/style/TeamDetailsModal.module.scss @@ -1,72 +1,22 @@ -.modalOverlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.7); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; - padding: 20px; -} - .modal { - background: rgb(28, 28, 28); - border-radius: 12px; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; scrollbar-width: none; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); - animation: slideIn 0.3s ease-out; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-20px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } } .modalHeader { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0px 15px; - border-bottom: 1px solid #000000; - background: linear-gradient(135deg, #f97507 0%, #ff6b35 100%); - color: white; - border-radius: 12px 12px 0 0; - - h2 { - margin: 0; - font-size: 1.5rem; - font-weight: 600; - } + padding: 0; } .closeButton { background: none; border: none; - color: rgb(0, 0, 0); + color: inherit; cursor: pointer; - padding: 8px ; - border-radius: 50%; - transition: background-color 0.2s; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - background-color: rgba(255, 255, 255, 0.2); - } + padding: 0; + border-radius: 10px; } .modalContent { @@ -98,11 +48,11 @@ h3 { margin: 0; background: linear-gradient( - rgba(244, 43, 3, 0.84) 0%, - rgba(255, 190, 11, 0.84) 100% - ); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + rgba(244, 43, 3, 0.84) 0%, + rgba(255, 190, 11, 0.84) 100% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; font-size: 1.25rem; font-weight: 600; } @@ -111,9 +61,9 @@ .teamInfo { margin-bottom: 24px; padding: 20px; - background: #232323; + background: rgba(255, 255, 255, 0.04); border-radius: 8px; - border: 1px solid #dd5c00; + border: 1px solid rgba(255, 255, 255, 0.08); } .teamHeader { @@ -141,9 +91,9 @@ justify-content: space-between; align-items: center; padding: 12px 16px; - background: rgb(29, 29, 29); + background: rgba(255, 255, 255, 0.04); border-radius: 6px; - border: 1px solid #585858; + border: 1px solid rgba(255, 255, 255, 0.08); .label { font-weight: 600; @@ -152,7 +102,7 @@ } .value { - color: #cc5c06; + color: #f59e0b; font-weight: 500; } } @@ -166,18 +116,18 @@ .teamCode { font-family: 'Courier New', monospace; font-weight: 600; - color: #f97507; - background: #474747; + color: #f59e0b; + background: rgba(255, 255, 255, 0.08); padding: 6px 12px; border-radius: 4px; - border: 1px solid #da7603; + border: 1px solid rgba(255, 255, 255, 0.1); font-size: 0.875rem; } .copyButton { - background: #f97507; + background: rgba(255, 255, 255, 0.08); color: white; - border: none; + border: 1px solid rgba(255, 255, 255, 0.15); padding: 8px; border-radius: 4px; cursor: pointer; @@ -187,7 +137,7 @@ justify-content: center; &:hover { - background: #ea580c; + background: rgba(255, 255, 255, 0.16); } &:active { @@ -223,9 +173,9 @@ display: flex; gap: 16px; padding: 16px; - background: #232323; + background: rgba(255, 255, 255, 0.04); border-radius: 8px; - border: 1px solid #e27100; + border: 1px solid rgba(255, 255, 255, 0.08); transition: box-shadow 0.2s; &:hover { @@ -239,7 +189,7 @@ height: 60px; border-radius: 50%; overflow: hidden; - border: 2px solid #f97507; + border: 2px solid rgba(255, 255, 255, 0.1); img { width: 100%; @@ -267,7 +217,7 @@ .memberName { margin: 0 0 8px 0; - color: #afafaf; + color: #ffffff; font-size: 1.125rem; font-weight: 600; } @@ -282,7 +232,7 @@ display: flex; align-items: center; gap: 8px; - color: #6b7280; + color: rgba(255, 255, 255, 0.65); font-size: 0.875rem; svg { @@ -303,18 +253,15 @@ } .modalFooter { - padding: 20px 24px; - border-top: 1px solid #202020; + margin-top: 1.5rem; display: flex; justify-content: flex-end; - background: #131313; - border-radius: 0 0 12px 12px; } .closeBtn { - background: #d06202; - color: white; - border: none; + background: rgba(255, 255, 255, 0.08); + color: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.15); padding: 10px 20px; border-radius: 6px; cursor: pointer; @@ -323,7 +270,7 @@ transition: background-color 0.2s; &:hover { - background: #be4f01; + background: rgba(255, 255, 255, 0.16); } } @@ -341,10 +288,6 @@ .modalHeader { padding: 0px 20px; - - h2 { - font-size: 1.25rem; - } } .teamDetailItem { @@ -370,4 +313,4 @@ .memberDetails { align-items: center; } -} \ No newline at end of file +} diff --git a/src/features/Modals/Profile/Admin/PreviewForm.jsx b/src/features/Modals/Profile/Admin/PreviewForm.jsx index 1d4ce420..ca817bc0 100644 --- a/src/features/Modals/Profile/Admin/PreviewForm.jsx +++ b/src/features/Modals/Profile/Admin/PreviewForm.jsx @@ -1,9 +1,9 @@ import { useContext, useEffect, useRef, useState } from "react"; import styles from "./styles/Preview.module.scss"; import AuthContext from "../../../../context/AuthContext"; -import { Button, Text } from "../../../../components"; +import { Button, Dialog, Text } from "../../../../components"; import Section from "./SectionModal"; -import { Link, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { X } from "lucide-react"; import { getOutboundList } from "../../../../sections/Profile/Admin/Form/NewForm/NewForm"; import Complete from "../../../../assets/images/Complete.svg"; @@ -32,9 +32,11 @@ const PreviewForm = ({ form, sections = [], open, + inline = false, meta = [], handleClose, showCloseBtn, + teamCode, // [v2] invite link team code }) => { const navigate = useNavigate(); const authCtx = useContext(AuthContext); @@ -66,18 +68,6 @@ const PreviewForm = ({ }, 1000); }, []); - useEffect(() => { - if (open) { - document.body.classList.add(styles.noScroll); - } else { - document.body.classList.remove(styles.noScroll); - } - - return () => { - document.body.classList.remove(styles.noScroll); - }; - }, [open]); - useEffect(() => { constructSections(); }, [sections]); @@ -166,7 +156,33 @@ const PreviewForm = ({ const participationType = eventData?.participationType; const successMessage = eventData?.successMessage; console.log(participationType); - const handleAutoClose = () => { + const handleAutoClose = async () => { + // [v2] If teamCode is present (invite link), auto-join the team after registration + if (teamCode && participationType === "Team") { + try { + const joinResponse = await api.post("/api/form/joinTeam", { + formId: form.id, + teamCode: teamCode, + }); + if (joinResponse.data?.success) { + Alert({ + type: "success", + message: joinResponse.data.message || `Joined team successfully!`, + position: "bottom-right", + duration: 3000, + }); + const eventId = joinResponse.data.data?.eventId || form.id; + setTimeout(() => { + navigate(`/Events/${form.id}/team`, { replace: true }); + }, 1000); + return; + } + } catch (joinErr) { + console.error("Auto-join failed:", joinErr); + // Fall through to normal flow if join fails + } + } + setTimeout(() => { if (participationType === "Team") { setTeamCode(code); @@ -485,254 +501,380 @@ const PreviewForm = ({ }; const renderPaymentScreen = () => { - const { eventType, receiverDetails, eventAmount } = formData; + const { eventType, receiverDetails, eventAmount } = formData; - const handleDownloadQR = async () => { - try { - let imageUrl = - typeof receiverDetails.media === "string" - ? receiverDetails.media - : URL.createObjectURL(receiverDetails.media); + const handleDownloadQR = async () => { + try { + let imageUrl = + typeof receiverDetails.media === "string" + ? receiverDetails.media + : URL.createObjectURL(receiverDetails.media); - let blobUrl = imageUrl; + let blobUrl = imageUrl; - if (typeof receiverDetails.media === "string") { - const response = await fetch(imageUrl); - const blob = await response.blob(); - blobUrl = URL.createObjectURL(blob); - } + if (typeof receiverDetails.media === "string") { + const response = await fetch(imageUrl); + const blob = await response.blob(); + blobUrl = URL.createObjectURL(blob); + } - const link = document.createElement("a"); - link.href = blobUrl; - link.download = "qr-code.png"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const link = document.createElement("a"); + link.href = blobUrl; + link.download = "qr-code.png"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); - if (typeof receiverDetails.media !== "string") { - URL.revokeObjectURL(blobUrl); + if (typeof receiverDetails.media !== "string") { + URL.revokeObjectURL(blobUrl); + } + } catch (error) { + console.error("Error downloading QR code:", error); + alert("Failed to download QR code."); } - } catch (error) { - console.error("Error downloading QR code:", error); - alert("Failed to download QR code."); - } - }; - - const handleCopyUPIID = () => { - if (receiverDetails.upi) { - navigator.clipboard.writeText(receiverDetails.upi) - .then(() => { - alert("UPI ID copied to clipboard!"); - }) - .catch(() => { - alert("Failed to copy UPI ID."); - }); - } - }; - - if (eventType === "Paid" && currentSection.name === "Payment Details") { - return ( -
- {receiverDetails.media && ( - {"QR-Code"} - )} + }; - {/* ✅ Download & Copy Buttons */} -
- - -
+ const handleCopyUPIID = () => { + if (receiverDetails.upi) { + navigator.clipboard.writeText(receiverDetails.upi) + .then(() => { + alert("UPI ID copied to clipboard!"); + }) + .catch(() => { + alert("Failed to copy UPI ID."); + }); + } + }; -

- Make the payment of{" "} - ₹{eventAmount}{" "} - using QR-Code or Pay using UPI ID:{" "} - {receiverDetails.upi} (No Refund Policy) -

-
- ); - } + {receiverDetails.media && ( + {"QR-Code"} + )} - return null; -}; + {/* Download & Copy Buttons */} +
+ + +
+ +

+ Make the payment of{" "} + ₹{eventAmount}{" "} + using QR-Code or Pay using UPI ID:{" "} + {receiverDetails.upi} (No Refund Policy) +

+
+ ); + } + + return null; + }; return ( <> - open && ( -
-
-
- {showCloseBtn && - (isEditing ? ( -
- -
- ) : ( - -
+ {open && + (inline ? ( +
+
+
+ {showCloseBtn && ( + + )} +
+
+ {eventData?.eventTitle || "Preview Event"}
- - ))} - - {eventData?.eventTitle || "Preview Event"} - - {isLoading ? ( - - ) : !isCompleted.includes("Submitted") ? ( -
-
- - {currentSection.name} - - + Complete the registration form to secure your spot. +
+
+ {isLoading ? ( + + ) : !isCompleted.includes("Submitted") ? ( +
+
+
+ {currentSection.name} +
+
+ {currentSection.description} +
+
+ {renderPaymentScreen()} +
+
+ {inboundList() && inboundList().backSection && ( + + )} + +
+
+ ) : isSuccess ? ( +
- {currentSection.description} - -
- {renderPaymentScreen()} -
-
- {inboundList() && inboundList().backSection && ( - - )} -
+ ) : ( +
- {inboundList() && inboundList().nextSection ? ( - "Next" - ) : isMicroLoading ? ( - - ) : ( - "Submit" - )} - -
-
- ) : isSuccess ? ( -
- Complete - - Form Submitted Successfully! Thank you for your time. - -
- ) : ( -
- -

- Error Submitting your Form -

- There is an error submitting the form. If you have made any - payment, please fill up your payment details again. There is - no need to pay again. -
+ +

+ Error Submitting your Form +

+ There is an error submitting the form. If you have made any + payment, please fill up your payment details again. There is + no need to pay again. +
+
+ )}
- )} +
-
-
- ) + ) : ( + { + if (!next) handleClose(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", + }} + > +
+
+
+ {showCloseBtn && ( + + )} +
+
+ {eventData?.eventTitle || "Preview Event"} +
+
+ Complete the registration form to secure your spot. +
+
+ {isLoading ? ( + + ) : !isCompleted.includes("Submitted") ? ( +
+
+
+ {currentSection.name} +
+
+ {currentSection.description} +
+
+ {renderPaymentScreen()} +
+
+ {inboundList() && inboundList().backSection && ( + + )} + +
+
+ ) : isSuccess ? ( +
+ Complete + + Form Submitted Successfully! Thank you for your time. + +
+ ) : ( +
+ +

+ Error Submitting your Form +

+ There is an error submitting the form. If you have made + any payment, please fill up your payment details again. + There is no need to pay again. +
+
+ )} +
+
+
+
+ ))} ); diff --git a/src/features/Modals/Profile/Admin/SectionModal.jsx b/src/features/Modals/Profile/Admin/SectionModal.jsx index f150f12b..d3b8a341 100644 --- a/src/features/Modals/Profile/Admin/SectionModal.jsx +++ b/src/features/Modals/Profile/Admin/SectionModal.jsx @@ -20,6 +20,8 @@ const Section = ({ section, handleChange }) => { type={field.type} value={value} name={field.name} + className={styles.formInput} + containerClassName={styles.formInputContainer} onChange={(e) => handleChange(field, e.target.value)} />
@@ -42,21 +44,10 @@ const Section = ({ section, handleChange }) => { const renderTeamFields = () => { return getTeamFields().map((team, index) => ( -
+
{team.map((field, index) => (
{ label={`${field.name} ${field.isRequired ? "*" : ""}`} type={field.type} name={field.name} - style={{ width: "100%" }} + className={styles.formInput} + containerClassName={styles.formInputContainer} value={ field.type === "file" || field.type === "image" ? field.onChangeValue?.name @@ -107,6 +99,8 @@ const Section = ({ section, handleChange }) => { label={`${field.name} ${field.isRequired ? "*" : ""}`} type={field.type} name={field.name} + className={styles.formInput} + containerClassName={styles.formInputContainer} value={ field.type === "file" || field.type === "image" ? field.onChangeValue?.name diff --git a/src/features/Modals/Profile/Admin/styles/Preview.module.scss b/src/features/Modals/Profile/Admin/styles/Preview.module.scss index 3b5dd216..c0e9553b 100644 --- a/src/features/Modals/Profile/Admin/styles/Preview.module.scss +++ b/src/features/Modals/Profile/Admin/styles/Preview.module.scss @@ -6,25 +6,25 @@ right: 0; bottom: 0; position: fixed; - background: rgba(7, 7, 7, 0.5); - z-index: 50; - backdrop-filter: blur(2px); + z-index: 2000; overflow: hidden; } +.pagePreview { + display: flex; + flex-direction: column; + width: 100%; + min-height: calc(100vh - 120px); + background: transparent; + padding: 1.5rem 1.25rem 2rem; +} + .previewContainerWrapper { - background: linear-gradient( - 90deg, - rgba(32, 32, 32, 0.95) -11.52%, - rgba(37, 37, 37, 0.95) 106.29% - ); - border-radius: 1.26863rem; - backdrop-filter: blur(4px); - border: 1.2px solid rgba(214, 214, 214, 0.81); - width: 35rem; - height: 90%; + position: relative; + width: min(820px, 94vw); + height: min(86vh, 860px); margin: auto; z-index: 100; overflow: hidden; @@ -33,14 +33,32 @@ align-items: center; } +.pagePreviewWrapper { + position: relative; + background: #121216; + border-radius: 14px; + border: 1px solid #242428; + width: min(1100px, 100%); + margin: 0 auto; + overflow: hidden; +} + .previewContainer { width: 100%; height: 100%; - padding: 2rem; + padding: 2.25rem 2.5rem 2.5rem; overflow-y: auto; overflow-x: hidden; /* Added to prevent horizontal overflow */ } +.inlineContainer { + padding: 2rem 2.25rem 2.5rem; +} + +.inlineContainer .formHeader { + margin-bottom: 1.25rem; +} + .previewContainer::-webkit-scrollbar { width: 6px; } @@ -56,21 +74,24 @@ .previewContainer > * { - padding-right: 1rem; + padding-right: 0; } .closeBtn{ position: absolute; - width: 100%; - right: 3px; - display: flex; - justify-content: flex-end; + right: 0.75rem; + top: 0.75rem; + display: inline-flex; + align-items: center; + justify-content: center; color: #fff; cursor: pointer; z-index: 5; - top: 1rem; - right: 1rem; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.12); + padding: 0.4rem; + border-radius: 8px; } .closeBtn:hover{ @@ -82,29 +103,96 @@ overflow: hidden; } -.formFieldContainer { +.formHeader { + display: flex; + flex-direction: column; + gap: 0.35rem; + padding-bottom: 1.25rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + margin-bottom: 1.5rem; +} + +.formTitle { + font-size: 1.5rem; + font-weight: 700; + color: #f5f5f5; + letter-spacing: 0.2px; +} + +.formSubtitle { + font-size: 0.95rem; + color: #a9a9ad; +} + +.sectionHeader { display: flex; - flex-direction: row; - flex-wrap: wrap; - // justify-content: center; - // justify-content: space-evenly; - column-gap: 3rem; + flex-direction: column; + gap: 0.25rem; + margin-bottom: 1rem; +} + +.sectionTitle { + font-size: 1rem; + font-weight: 600; + color: #f1f1f1; +} + +.sectionDesc { + font-size: 0.8rem; + color: #888a90; +} + +.formActions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.25rem; +} + +.formFieldContainer { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 1.5rem; + row-gap: 1.25rem; width: 100%; - padding: 1em; - padding-left: 2rem; - row-gap: 1rem; - border-radius: 8px; - border: 0.5px solid transparent; - background-color: rgba(128, 127, 126, 0.066); - margin-bottom: 1em; + padding: 1.25rem; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.06); + background-color: rgba(18, 18, 22, 0.85); + margin-bottom: 1.25rem; +} + +.formFieldContainer > div { + width: 100%; +} + +.formInputContainer { + margin-top: 0; +} + +.formInput { + border-radius: 12px !important; + border: 1px solid rgba(255, 255, 255, 0.12) !important; + background: #0f0f12 !important; + box-shadow: none !important; + font-size: 0.95rem !important; +} + +.formInput:focus, +.formInput:focus-visible { + border-color: rgba(255, 140, 66, 0.9) !important; + box-shadow: 0 0 0 3px rgba(255, 140, 66, 0.18) !important; } -.formFieldContainer:hover { - border: 0.5px solid #ff88008c; - transition: 0.5s ease-in-out; + +.teamContainer { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + width: 100%; } -.formFieldContainer:active { - border: 0.5px solid #ff88008c; - transition: 0.5s ease-in-out; + +.teamField { + width: 100%; } @media (min-width: 481px) and (max-width:800px){ @@ -116,35 +204,50 @@ @media (max-width: 480px){ .previewContainerWrapper{ - height: 70%; - width: 90%; + height: 82vh; + width: 94vw; } .previewContainer{ height: 100%; width: 100%; - margin-left: 1rem; - padding: 1rem; + margin-left: 0; + padding: 1.25rem; } - .closeBtn{ - width: 80%; + .formFieldContainer{ + grid-template-columns: 1fr; + padding: 1rem; } } +@media (max-width: 640px) { + .previewContainer { + padding: 1.5rem; + } + + .formTitle { + font-size: 1.3rem; + } + + .teamContainer { + grid-template-columns: 1fr; + } +} + @media (max-width: 380px) { .previewContainerWrapper{ - height: 70%; - width: 90%; + height: 82vh; + width: 94vw; } .previewContainer{ height: 100%; width: 100%; - margin-left: 1rem; + margin-left: 0; } .formFieldContainer{ - width: 115%; - margin-left: -1rem; + width: 100%; + margin-left: 0; } -} \ No newline at end of file +} diff --git a/src/features/Modals/authentication/OtpInputModal.jsx b/src/features/Modals/authentication/OtpInputModal.jsx index 9eec8bf6..47d74f36 100644 --- a/src/features/Modals/authentication/OtpInputModal.jsx +++ b/src/features/Modals/authentication/OtpInputModal.jsx @@ -1,53 +1,49 @@ import React from "react"; import OtpInput from "../../../components/OtpInput/OtpInput"; import { X } from "lucide-react"; +import { Dialog } from "../../../components"; +import modalCard from "../../../components/ui/ModalCard.module.scss"; const OtpInputModal = (props) => { const { onVerify, handleClose } = props; - return ( -
{ + if (!next) handleClose(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", }} > -
-
- +
+
+
+
OTP Verification
+
Enter the code to continue
+
+
+
+
-
+ ); }; diff --git a/src/index.scss b/src/index.scss index 5970d3b3..bd160c95 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,7 +1,5 @@ @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap"); @import "/src/assets/styles/Global.scss"; -@import 'react-date-picker/dist/DatePicker.css'; -@import 'react-calendar/dist/Calendar.css'; @import "./assets/styles/Global.scss"; @import "react-datepicker/dist/react-datepicker.css"; @@ -10,43 +8,102 @@ padding: 0px; box-sizing: border-box; font-family: "Open Sans", sans-serif; -} - -.react-date-picker__wrapper { - border: none !important; + scrollbar-width: thin; + scrollbar-color: rgba(255, 106, 0, 0.95) #000000; } html { scroll-behavior: smooth; } +/* chrome, edge, safari */ +*::-webkit-scrollbar { + width: 2px; + height: 2px; +} + +*::-webkit-scrollbar-track { + background: #000000; +} + +*::-webkit-scrollbar-thumb { + background: linear-gradient( + 180deg, + rgba(255, 138, 0, 0.98) 0%, + rgba(255, 94, 0, 0.95) 55%, + rgba(229, 28, 35, 0.92) 100% + ); + border-radius: 999px; +} + +*::-webkit-scrollbar-thumb:hover { + background: linear-gradient( + 180deg, + rgba(255, 164, 56, 1) 0%, + rgba(255, 108, 26, 0.98) 55%, + rgba(241, 42, 49, 0.95) 100% + ); +} + body { background-color: $page-color; color: $text-color; overflow-x: hidden; } +body.fed-modal-open { + overflow: hidden; +} + +html.route-scrollbar-hidden, +body.route-scrollbar-hidden { + scrollbar-width: none; +} + +html.route-scrollbar-hidden::-webkit-scrollbar, +body.route-scrollbar-hidden::-webkit-scrollbar { + width: 0; + height: 0; +} + .page { display: flex; flex-direction: column; justify-content: center; align-items: center; - background-color: $page-color; + background-color: transparent; overflow: hidden; - margin-top: 90px; + margin-top: 0; + width: 100%; + position: relative; } .page.omega-page { background-color: #000000; } +.page.event-page-theme { + background-color: #110a09; +} + +.event-theme-root { + background-color: #110a09 !important; + + // Ensure any other blackish themes are overridden + * { + border-color: rgba(17, 10, 9, 0.5) !important; + } +} + .authpage { display: flex; flex-direction: column; justify-content: center; - align-items: center; + align-items: stretch; background-color: $page-color; overflow: hidden; + width: 100vw; + min-height: 100vh; } .pageExt { @@ -54,26 +111,173 @@ body { width: 100%; background-color: $page-color; overflow: hidden; + position: relative; } -body::-webkit-scrollbar { - width: 10px; - height: 0px; - transition: .2s linear; - background-color: #292929; +.react-datepicker-wrapper { + width: 100% !important; } -body::-webkit-scrollbar-thumb { - border-radius: 22px; - background: var(--primary); - // border: 1.5px solid hwb(0 11% 89%); +.fed-datepicker-popper { + z-index: 9999 !important; + background: transparent !important; } -body::-webkit-scrollbar-track { - padding: 0px 20px; - background-color: transparent; +.fed-datepicker { + font-family: "Open Sans", sans-serif; + background: #0b0b0b !important; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + border-radius: 16px !important; + box-shadow: 0 28px 70px rgba(0, 0, 0, 0.75) !important; + overflow: hidden; + display: flex; + align-items: stretch; + opacity: 1 !important; + backdrop-filter: none !important; + filter: none !important; + mix-blend-mode: normal !important; + isolation: isolate; } -.react-datepicker-wrapper { - width: 100% !important; -} \ No newline at end of file +.fed-datepicker-compact { + width: 280px; + font-size: 0.85rem; + padding: 6px; +} + +.fed-datepicker-datetime { + width: 320px; + font-size: 0.82rem; + padding: 6px; +} + +.fed-datepicker .react-datepicker__month-container, +.fed-datepicker .react-datepicker__time-container, +.fed-datepicker .react-datepicker__month { + background: #0b0b0b !important; +} + +.fed-datepicker .react-datepicker__month-container { + float: none; +} + +.fed-datepicker .react-datepicker__day--disabled, +.fed-datepicker .react-datepicker__time-list-item--disabled { + color: rgba(255, 255, 255, 0.3) !important; + pointer-events: none; +} + +.fed-datepicker .react-datepicker__header { + background: #0b0b0b !important; + border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; +} + +.fed-datepicker .react-datepicker__current-month, +.fed-datepicker .react-datepicker-time__header, +.fed-datepicker .react-datepicker__day-name, +.fed-datepicker .react-datepicker__day, +.fed-datepicker .react-datepicker__time-name { + color: #f5f5f5 !important; +} + +.fed-datepicker .react-datepicker__day { + border-radius: 10px; +} + +.fed-datepicker-compact .react-datepicker__day-name, +.fed-datepicker-compact .react-datepicker__day, +.fed-datepicker-compact .react-datepicker__time-name { + width: 1.85rem; + line-height: 1.85rem; + margin: 0.12rem; + font-size: 0.8rem; +} + +.fed-datepicker-compact .react-datepicker__current-month, +.fed-datepicker-compact .react-datepicker-time__header { + font-size: 0.9rem; +} + +.fed-datepicker-compact .react-datepicker__header { + padding: 6px 6px 2px; +} + +.fed-datepicker-compact .react-datepicker__month { + margin: 0.4rem; +} + +.fed-datepicker-datetime .react-datepicker__day-name, +.fed-datepicker-datetime .react-datepicker__day, +.fed-datepicker-datetime .react-datepicker__time-name { + width: 1.7rem; + line-height: 1.7rem; + margin: 0.1rem; + font-size: 0.78rem; +} + +.fed-datepicker-datetime .react-datepicker__current-month, +.fed-datepicker-datetime .react-datepicker-time__header { + font-size: 0.85rem; +} + +.fed-datepicker-datetime .react-datepicker__header { + padding: 6px 6px 2px; +} + +.fed-datepicker-datetime .react-datepicker__month { + margin: 0.35rem; +} + +.fed-datepicker .react-datepicker__day--selected, +.fed-datepicker .react-datepicker__day--keyboard-selected { + background: linear-gradient(140deg, #ff8a3d, #ff5f1f) !important; + color: #ffffff !important; +} + +.fed-datepicker .react-datepicker__day:hover { + background: rgba(255, 255, 255, 0.08) !important; +} + +.fed-datepicker .react-datepicker__time-container { + border-left: 1px solid rgba(255, 255, 255, 0.08) !important; + width: 120px; + float: none; + margin: 0; + align-self: stretch; +} + +.fed-datepicker-datetime .react-datepicker__time-container { + width: 96px; +} + +.fed-datepicker .react-datepicker__time-container .react-datepicker__time { + background: #0b0b0b !important; + height: 100%; +} + +.fed-datepicker .react-datepicker__time-list { + background: #0b0b0b !important; + height: 100%; + max-height: none; + overflow-y: auto; +} + +.fed-datepicker .react-datepicker__time-list-item { + color: #e6e6e6 !important; + border-radius: 8px; + cursor: pointer; +} + +.fed-datepicker .react-datepicker__time-list-item--selected { + background: rgba(255, 138, 0, 0.2) !important; + color: #fff !important; +} + +.fed-datepicker .react-datepicker__time-list-item:hover { + background: rgba(242, 242, 242, 0.25) !important; + color: #ffffff !important; +} + +.fed-datepicker .react-datepicker__triangle { + display: none !important; +} diff --git a/src/layouts/Footer/Footer.jsx b/src/layouts/Footer/Footer.jsx index 3f96e5b4..51ed3596 100644 --- a/src/layouts/Footer/Footer.jsx +++ b/src/layouts/Footer/Footer.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React from "react"; import { Link, useLocation } from "react-router-dom"; import { HashLink } from "react-router-hash-link"; import { @@ -11,14 +11,19 @@ import { import { FaXTwitter } from "react-icons/fa6"; import styles from "./styles/Footer.module.scss"; +import footerBgImage from "../../assets/images/herobgimage.png"; export default function Footer() { const location = useLocation(); const isOmega = location.pathname.includes("/Omega"); + const currentYear = new Date().getFullYear(); return (
-
+
Explore Home Events Team @@ -64,17 +66,15 @@ export default function Footer() { Contact Alumni @@ -82,9 +82,8 @@ export default function Footer() { href="http://medium.com/@fedkiit" target="_blank" rel="noopener noreferrer" - className={`${styles.footerleftlink} ${ - isOmega ? styles.omegaLink : "" - }`} + className={`${styles.footerleftlink} ${isOmega ? styles.omegaLink : "" + }`} > Blog @@ -95,18 +94,16 @@ export default function Footer() { Manifesto Partners @@ -119,56 +116,48 @@ export default function Footer() { href="https://www.linkedin.com/company/fedkiit/" target="_blank" rel="noopener noreferrer" - className={`${styles.link1} ${ - isOmega ? styles.omegaSocialLink : "" - }`} + className={`${styles.link1} ${isOmega ? styles.omegaSocialLink : "" + }`} >
@@ -185,24 +174,24 @@ export default function Footer() {
Terms and conditions

&

Privacy policy
-

© 2024, fedkiit

+

+ © {currentYear}, FEDKIIT +

{/*
*/}
diff --git a/src/layouts/Footer/styles/Footer.module.scss b/src/layouts/Footer/styles/Footer.module.scss index 5e52d045..f6687759 100644 --- a/src/layouts/Footer/styles/Footer.module.scss +++ b/src/layouts/Footer/styles/Footer.module.scss @@ -1,133 +1,127 @@ .f1 { - padding: 40px; - padding-bottom: 20px; - background-color: rgba(30, 30, 30, 1); - color: white; + padding: 40px 40px 20px; + min-height: 380px; + + background: + + var(--footer-bg-image); + + background-size: cover; + background-position: center 30%; // show bottom part + background-repeat: no-repeat; + + color: #ffffff; overflow-x: hidden; + + &.eventF1 { + background-color: #110a09; + } } +/* ================= Logo ================= */ + .logodiv { display: flex; align-items: center; + gap: 16px; } .fed { - // font-family: "Oswald", sans-serif; font-weight: 700; - margin-left: 20px; font-size: 30px; line-height: 36px; - text-shadow: 1px 0px 0.496831px #f45725; + text-shadow: 1px 0px 0.5px #f45725; } +/* ================= Layout ================= */ + .flexdiv { - margin-top: 40px; + margin: 40px 0 35px; display: flex; justify-content: space-between; align-items: flex-start; width: 100%; - margin-bottom: 35px; } .footerleft { display: flex; - flex-direction: row; justify-content: space-between; width: 100%; margin-bottom: 20px; } -.footerleftlink { - padding-top: 0.6rem; - color: #afafaf; - text-decoration: none; - font-family: "Manrope", sans-serif; - font-weight: 700; - font-size: 17px; - line-height: 24px; -} - .row1, .row2 { display: flex; flex-direction: column; - font-size: 20px; + font-size: 18px; } .row1 h4, .row2 h4, .footerright h4 { - margin-bottom: 10px; - color: #ffffff; - // font-family: "Oswald", sans-serif; - font-weight: 500; - font-size: 20px; + margin-bottom: 12px; + font-weight: 600; + font-size: 18px; +} + +/* ================= Links ================= */ + +.footerleftlink, +.link1 { + padding-top: 0.6rem; + color: #afafaf; + text-decoration: none; + font-family: "Manrope", sans-serif; + font-weight: 600; + font-size: 16px; line-height: 24px; + margin-bottom: 8px; + transition: color 0.3s ease; +} + +.footerleftlink:hover, +.link1:hover { + color: #ff8a00; } +/* ================= Icons ================= */ .icondiv { display: flex; - align-items: flex-start; - justify-content: flex-start; flex-direction: column; color: #afafaf; } .icondiv2 { display: flex; - align-items: flex-start; - justify-content: space-between; - flex-direction: row; - color: #afafaf; -} - -.link1 { + justify-content: flex-start; + gap: 14px; color: #afafaf; - text-decoration: none; - font-family: "Manrope", sans-serif; - font-weight: 700; - font-size: 17px; - line-height: 24px; - margin-bottom: 10px; } -.icon { - margin-right: 5px; - margin-bottom: 3px; - margin-top: 10px; - font-size: 40px; -} - -.link1 :hover{ - color: #FF8A00; - transition: 0.3s ease-in-out; +.icon, +.omegaSocialLink { + font-size: 36px; + transition: color 0.3s ease; } -.Linkstyles:hover{ - background: var(--primary); - background-clip: text; - color: transparent; - transition: 0.3s ease-in-out; +.omegaSocialLink:hover { + color: #38ccff; } -.footerleftlink:hover{ - background: var(--primary); - background-clip: text; - color: transparent; - transition: 0.3s ease-in-out; -} +/* ================= Bottom Section ================= */ .bottomdiv { margin-top: 20px; - padding: 10px 0; - border-top: 1px solid #5c5c5a; + padding: 14px 0; + border-top: 1px solid rgba(255, 255, 255, 0.15); } .bottomleft { font-family: "Manrope", sans-serif; - font-weight: 700; - font-size: 15px; + font-weight: 600; + font-size: 14px; text-align: center; margin-top: 10px; } @@ -136,120 +130,89 @@ display: flex; justify-content: space-between; margin-top: 10px; -} -.terms_and_policies a{ - text-decoration: none; - color: #ffffff; + a { + text-decoration: none; + color: #ffffff; + transition: color 0.3s ease; + + &:hover { + color: #ff8a00; + } + } } +/* ================= Misc ================= */ .tap1Div { display: flex; justify-content: space-between; align-items: center; gap: 8px; - } + .copyrightPTag { text-align: center; } -.dotDiv{ + +.dotDiv { background-color: #ffffff; - height: 8px; - width: 8px; + height: 6px; + width: 6px; border-radius: 50%; - margin-top: 8px; + margin-top: 6px; } -.omegaLink { - transition: color 0.3s ease; -} - -.omegaLink:hover { - color: #38ccff; // Change this to the desired hover color -} - -.omegaSocialLink { - transition: color 0.3s ease; - margin-right: 5px; - margin-bottom: 3px; - margin-top: 10px; - font-size: 40px; -} - -.omegaSocialLink:hover { - &.link1 :hover{ - color:#38ccff; - } - color: #38ccff;// Change this to the desired hover color -} +/* ================= Tablet ================= */ -@media screen and (max-width: 768px) { +@media (max-width: 768px) { .flexdiv { flex-direction: column; align-items: center; + gap: 40px; } - .footerleft{ - width: 100%; + .footerleft { + flex-direction: column; align-items: center; + gap: 30px; } } -@media screen and (max-width: 480px) { +/* ================= Mobile ================= */ + +@media (max-width: 480px) { .logodiv { - display: flex; - align-items: center; justify-content: flex-start; - gap: 10px; } - .icon { - font-size: 30px; - flex-direction: column; - + .fed { + font-size: 24px; } - - .fed { - margin-left: 0; - font-size: 25px; + .icon, + .omegaSocialLink { + font-size: 30px; } .footerleft { - display: flex; align-items: flex-start; - justify-content: space-between; - flex-wrap: wrap; - gap: 1rem; + gap: 20px; text-align: left; - width: 100%; - } - - .row1, .row2 { - margin-top: 10px; } .icondiv2 { - display: flex; justify-content: center; - align-items: center; - width: 88vw; + width: 100%; } - .socialh4{ - visibility: hidden; - } - .bottomdiv{ - font-size: 10px; + .bottomdiv { + font-size: 12px; } } -@media screen and (max-width: 400px){ - .icondiv2{ - margin-left: -0.8rem; +@media (max-width: 400px) { + .icondiv2 { + margin-left: -0.5rem; } -} - - +} \ No newline at end of file diff --git a/src/layouts/Navbar/styles/Navbar.module.scss b/src/layouts/Navbar/styles/Navbar.module.scss index e07ae69c..4805c2fa 100644 --- a/src/layouts/Navbar/styles/Navbar.module.scss +++ b/src/layouts/Navbar/styles/Navbar.module.scss @@ -7,14 +7,7 @@ justify-content: center; z-index: 10; top: 0; - - // background: linear-gradient( - // 91deg, - // rgba(48, 48, 48, 0.3) -147.64%, - // rgba(217, 217, 217, 0) 143.9% - // ); - // margin-top: 8px; - + padding-top: 8px; } .logo_div { @@ -52,6 +45,16 @@ width: 100%; padding: 5px 30px 0px 25px; border-radius: 50px; + background: linear-gradient( + 95deg, + rgba(16, 19, 28, 0.78) 0%, + rgba(26, 32, 44, 0.58) 60%, + rgba(14, 17, 24, 0.76) 100% + ); + border: 1px solid rgba(255, 255, 255, 0.14); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.32); } .navLinks { @@ -217,11 +220,11 @@ .navItems { border-radius: 1.65rem; - border: 1.652px solid white; + border: 1px solid rgba(255, 255, 255, 0.3); background: linear-gradient( - 91deg, - rgba(217, 217, 217, 0.3) -147.64%, - rgba(217, 217, 217, 0) 143.9% + 100deg, + rgba(255, 255, 255, 0.2) -130%, + rgba(255, 255, 255, 0.03) 135% ); display: flex; @@ -334,15 +337,17 @@ .navbar { margin-top: 0; border-radius: 0; + padding-top: 0; } .navbarContent { - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); display: flex; align-items: flex-start; padding-top: 25px; padding-right: 0px; + border-radius: 0; + border-left: none; + border-right: none; } .mobNav { @@ -371,7 +376,11 @@ flex-direction: column; align-items: center; width: 90%; - background-color: #202020; + background: linear-gradient( + 160deg, + rgba(16, 20, 30, 0.95), + rgba(10, 12, 18, 0.92) + ); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); position: absolute; @@ -415,4 +424,4 @@ .logo_text { margin-left: -25px; } -} \ No newline at end of file +} diff --git a/src/layouts/Profile/ProfileLayout/styles/ProfileLayout.module.scss b/src/layouts/Profile/ProfileLayout/styles/ProfileLayout.module.scss index ade8aa6a..81cfe81a 100644 --- a/src/layouts/Profile/ProfileLayout/styles/ProfileLayout.module.scss +++ b/src/layouts/Profile/ProfileLayout/styles/ProfileLayout.module.scss @@ -1,28 +1,47 @@ .main { - height: 100%; + min-height: 100vh; width: 100%; - background-color: #1c1c1c; position: relative; overflow: hidden; + background: + radial-gradient(circle at 85% 15%, rgba(255, 119, 0, 0.2) 0%, transparent 42%), + radial-gradient(circle at 8% 90%, rgba(255, 126, 26, 0.17) 0%, transparent 40%), + linear-gradient(140deg, #0e0e11 0%, #17171d 45%, #121218 100%); + padding: 1rem 0 1rem; } -.frameMiddle { - height: 20vw; - width: 15vw; - background-color: #ff5e00a7; - filter: blur(100px); +.bodyChildren { + position: relative; + z-index: 3; +} + +.frameMiddle, +.frameBottom { position: absolute; - top: 40%; - right: -8%; border-radius: 50%; + filter: blur(110px); + z-index: 1; + pointer-events: none; } + +.frameMiddle { + height: clamp(180px, 24vw, 380px); + width: clamp(180px, 24vw, 380px); + background: rgba(255, 107, 26, 0.36); + top: 16%; + right: -7%; +} + .frameBottom { - height: 20vw; - width: 15vw; - background-color: #ff5e00a7; - filter: blur(100px); - position: absolute; - bottom: -26%; + height: clamp(180px, 24vw, 380px); + width: clamp(180px, 24vw, 380px); + background: rgba(255, 94, 0, 0.32); + bottom: -12%; left: -8%; - border-radius: 50%; +} + +@media screen and (max-width: 768px) { + .main { + padding: 0.45rem 0 0.7rem; + } } diff --git a/src/layouts/Profile/Sidebar/Sidebar.jsx b/src/layouts/Profile/Sidebar/Sidebar.jsx index 9e896715..d92be152 100644 --- a/src/layouts/Profile/Sidebar/Sidebar.jsx +++ b/src/layouts/Profile/Sidebar/Sidebar.jsx @@ -1,27 +1,25 @@ import React, { useContext, useEffect, useRef, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { MdLogout } from "react-icons/md"; import { TbUserEdit } from "react-icons/tb"; import { SlCalender } from "react-icons/sl"; import { SiReacthookform } from "react-icons/si"; import { FaRegNewspaper, FaCertificate } from "react-icons/fa"; import { LuClipboardList } from "react-icons/lu"; +import { FiUser } from "react-icons/fi"; import AuthContext from "../../../context/AuthContext"; import styles from "./styles/Sidebar.module.scss"; import defaultImg from "../../../assets/images/defaultImg.jpg"; import camera from "../../../assets/images/camera.svg"; import { EditImage } from "../../../features"; -import { Link, NavLink } from "react-router-dom"; +import { NavLink } from "react-router-dom"; -const Sidebar = ({ activepage, handleChange }) => { +const Sidebar = ({ activepage, handleChange, isMobileOpen, closeMobileSidebar }) => { const [designation, setDesignation] = useState(""); const authCtx = useContext(AuthContext); const [imagePrv, setimagePrv] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const imgRef = useRef(null); - const navigate = useNavigate(); const [openModal, setOpenModal] = useState(false); useEffect(() => { @@ -41,11 +39,6 @@ const Sidebar = ({ activepage, handleChange }) => { } }, [authCtx.user.access, authCtx.user.email]); - const handleLogout = () => { - navigate("/"); - authCtx.logout(); - }; - const handleName = () => { const maxLength = 20; const name = authCtx.user.name || ""; @@ -66,6 +59,31 @@ const Sidebar = ({ activepage, handleChange }) => { setimagePrv(url); }; + const activeKey = String(activepage || "").toLowerCase(); + const isActive = (...keys) => keys.some((key) => key.toLowerCase() === activeKey); + + const handleMenuSelect = (page) => { + handleChange(page); + if (window.innerWidth <= 768) { + closeMobileSidebar?.(); + } + }; + + const renderMenuItem = ({ label, to, page, Icon, activeKeys = [] }) => { + const itemActive = isActive(page, ...activeKeys); + return ( +
handleMenuSelect(page)} + > + + + {label} + +
+ ); + }; + // Check if user is attendance-only const isAttendanceOnly = authCtx.user.email === "attendance@fedkiit.com"; @@ -74,187 +92,84 @@ const Sidebar = ({ activepage, handleChange }) => { const isMobile = window.innerWidth <= 768; if (isMobile) { - return ( -
handleChange("Attendance")} - style={{ - background: - activepage === "Attendance" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: - activepage === "Attendance" ? "text" : "initial", - backgroundClip: activepage === "Attendance" ? "text" : "initial", - color: activepage === "Attendance" ? "transparent" : "inherit", - }} - > - {" "} - Attendance -
- ); + return renderMenuItem({ + label: "Attendance", + to: "/profile/attendance", + page: "Attendance", + Icon: LuClipboardList, + }); } - return ( -
handleChange("Blogs")} - style={{ - background: activepage === "Blogs" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: activepage === "Blogs" ? "text" : "initial", - backgroundClip: activepage === "Blogs" ? "text" : "initial", - color: activepage === "Blogs" ? "transparent" : "inherit", - }} - > - {" "} - Blogs -
- ); + return renderMenuItem({ + label: "Blogs", + to: "/profile/BlogForm", + page: "Blogs", + Icon: FaRegNewspaper, + }); }; const renderAdminMenu = () => ( <> -
handleChange("Events")} - style={{ - background: activepage === "Events" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: activepage === "Events" ? "text" : "initial", - backgroundClip: activepage === "Events" ? "text" : "initial", - color: activepage === "Events" ? "transparent" : "inherit", - }} - > - {" "} - Event -
- -
handleChange("Form")} - style={{ - background: activepage === "Form" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: activepage === "Form" ? "text" : "initial", - backgroundClip: activepage === "Form" ? "text" : "initial", - color: activepage === "Form" ? "transparent" : "inherit", - }} - > - {" "} - Form -
- -
handleChange("Members")} - style={{ - background: - activepage === "Members" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: activepage === "Members" ? "text" : "initial", - backgroundClip: activepage === "Members" ? "text" : "initial", - color: activepage === "Members" ? "transparent" : "inherit", - marginLeft: "-6px", - }} - > - {" "} - Members -
- -
handleChange("Attendance")} - style={{ - background: - activepage === "Attendance" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: - activepage === "Attendance" ? "text" : "initial", - backgroundClip: activepage === "Attendance" ? "text" : "initial", - color: activepage === "Attendance" ? "transparent" : "inherit", - marginLeft: "-6px", - }} - > - {" "} - Attendance -
+ {renderMenuItem({ + label: "Event", + to: "/profile/events", + page: "Events", + Icon: SlCalender, + activeKeys: ["events"], + })} + {renderMenuItem({ + label: "Form", + to: "/profile/Form", + page: "Form", + Icon: SiReacthookform, + })} + {renderMenuItem({ + label: "Members", + to: "/profile/members", + page: "Members", + Icon: TbUserEdit, + })} + {renderMenuItem({ + label: "Attendance", + to: "/profile/attendance", + page: "Attendance", + Icon: LuClipboardList, + })} ); // Render attendance-only menu - const renderAttendanceOnlyMenu = () => ( -
handleChange("Attendance")} - style={{ - background: activepage === "Attendance" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: activepage === "Attendance" ? "text" : "initial", - backgroundClip: activepage === "Attendance" ? "text" : "initial", - color: activepage === "Attendance" ? "transparent" : "inherit", - }} - > - {" "} - Attendance -
- ); + const renderAttendanceOnlyMenu = () => + renderMenuItem({ + label: "Attendance", + to: "/profile/attendance", + page: "Attendance", + Icon: LuClipboardList, + }); - const renderCertificateMenu = () => ( -
handleChange("Certificates")} - style={{ - background: activepage === "Certificates" ? "var(--primary)" : "transparent", - WebkitBackgroundClip: activepage === "Certificates" ? "text" : "initial", - backgroundClip: activepage === "Certificates" ? "text" : "initial", - color: activepage === "Certificates" ? "transparent" : "inherit", - }} - > - {" "} - Certificates -
- ); + const renderCertificateMenu = () => + renderMenuItem({ + label: "Certificates", + to: "/profile/certificates", + page: "Certificates", + Icon: FaCertificate, + }); + + const renderProfileDetailsMenu = () => + renderMenuItem({ + label: "Profile Details", + to: "/profile", + page: "Profile", + Icon: FiUser, + }); return ( <> -
+ ); }; -export default Sidebar; \ No newline at end of file +export default Sidebar; diff --git a/src/layouts/Profile/Sidebar/styles/Sidebar.module.scss b/src/layouts/Profile/Sidebar/styles/Sidebar.module.scss index 2f1f6fcd..7a5319ed 100644 --- a/src/layouts/Profile/Sidebar/styles/Sidebar.module.scss +++ b/src/layouts/Profile/Sidebar/styles/Sidebar.module.scss @@ -1,58 +1,77 @@ @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); .sidebar { display: flex; - height: 100vh; + height: 100%; flex-direction: column; - width: 34%; + width: 100%; position: relative; + padding: 1.4rem 1rem 1rem; + background: linear-gradient(180deg, rgba(18, 18, 22, 0.96), rgba(10, 10, 14, 0.95)); +} + +.cameraBtn { + border: none; + background: transparent; + padding: 0; + margin: 0; + cursor: pointer; } .profile { display: flex; - flex-direction: column; - justify-content: center; align-items: center; - margin-bottom: 10px; - margin-top: 40px; - // margin-bottom: auto; + gap: 0.9rem; + padding: 0.25rem 0.15rem 0.6rem; + margin: 0 0 0.75rem; } .divider { - height: 90%; - width: 1.6px; + height: calc(100% - 2.2rem); + width: 1px; position: absolute; right: 0; - top: 0; - bottom: 0; - margin: auto; - background-color: #fff !important; + top: 1.1rem; + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0.28) 0%, + rgba(255, 138, 0, 0.34) 40%, + rgba(255, 255, 255, 0.12) 100% + ); } .profilePhoto { - width: 148px; - height: 148px; - border-radius: 19px; + width: 72px; + height: 72px; + border-radius: 16px; object-fit: cover; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45); } .profileInfo { - text-align: center; - display: grid; - align-items: center; + text-align: left; + display: flex; + flex-direction: column; + align-items: flex-start; justify-content: center; + min-width: 0; } .name { color: rgb(255, 255, 255); - font-size: 25px; + font-size: 1.1rem; font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: 170px; } .role { font-family: "Poppins"; - font-size: 13px; + font-size: 0.76rem; opacity: 0.8; - margin-top: 6px; + margin-top: 0.25rem; color: rgba(255, 255, 255, 0.605); } @@ -60,156 +79,153 @@ width: 100%; display: flex; flex-direction: column; - align-items: center; - justify-content: center; + align-items: stretch; + justify-content: flex-start; + gap: 0.25rem; margin-bottom: auto; - // background-color: red; - padding-left: 18%; + padding: 0.4rem 0.35rem 0; } -.menu div { - width: 60%; +.menuItem { + width: 100%; font-family: "Poppins"; - // background-color: pink; font-weight: 500; cursor: pointer; - font-size: 20px; - gap: 4.06px; - padding: 12px 0px; - transition: background-color 0.3s; + font-size: 1rem; + gap: 0.65rem; + padding: 0.72rem 0.8rem; + border-radius: 12px; + transition: background-color 0.25s ease, transform 0.25s ease; display: flex; flex-direction: row; align-items: center; + border: 1px solid transparent; + letter-spacing: 0.01em; + overflow: hidden; + color: rgba(255, 255, 255, 0.92); + background: transparent; } -.menu div a{ - color: inherit; - cursor: pointer; - text-decoration: none; +.menuItem:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.08); + transform: none; } -.menuItem:hover { - background-color: #ffffff; +.menuItemActive { + background: transparent; + border-color: transparent; + color: #ffffff; } -.buttonContainer { +.menuLink { display: flex; - justify-content: left; - gap: 0.658rem; - margin-top: -49.82rem; - margin-left: 427px; + align-items: center; + width: 100%; + color: inherit; + cursor: pointer; + text-decoration: none; + gap: 0.6rem; } -.button { - padding: 10px 20px; - height: 46.2px; - width: 132.14px; - font-family: "Poppins"; - font-size: 0.943rem; - border-radius: 3.95px; - border: none; - cursor: pointer; - transition: background-color 0.3s ease, color 0.3s ease; +.menuIcon { + flex-shrink: 0; +} - &:hover { - opacity: 0.9; - } +.menuItemActive .menuIcon { + color: #ff8a00; } -.viewEvents { - background-color: transparent; - color: white; - border: 2px solid white; +.menuItemActive .menuLink span { + background: var( + --Primary, + linear-gradient( + 260deg, + rgba(255, 190, 11, 0.84) -29.7%, + rgba(244, 43, 3, 0.84) 128.34% + ) + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} - &:hover { - background-color: #333; +@media screen and (max-width: 768px) { + .sidebar { + position: fixed; + top: 74px; + left: 0; + width: min(84vw, 320px); + height: calc(100dvh - 74px); + padding: 1.15rem 0.8rem 0.9rem; + border-right: 1px solid rgba(255, 255, 255, 0.13); + transform: translateX(-105%); + transition: transform 0.22s ease; + z-index: 35; + box-shadow: 16px 0 36px rgba(0, 0, 0, 0.42); } - &:active { - background-color: #919191; - color: #ff8a00; + .mobileOpen { + transform: translateX(0); } -} - -.addEvent { - background-color: transparent; - color: white; - border: 2px solid white; - &:hover { - background-color: #cc7a00; + .profile { + margin-bottom: 1rem; + flex-direction: row; + text-align: left; } - &:active { - background-color: #919191; - color: #ff8a00; + .profileInfo { + align-items: flex-start; } -} -@media screen and (max-width: 768px) { - .sidebar { - width: 100%; - height: auto; - // background-color: rebeccapurple; - } .menu { - flex-direction: row; + flex-direction: column; width: 100%; - padding-left: 0px; - } - .menu div { - width: auto; - margin-right: 0.6em; - margin-left: 0.6em; - } - .menu div:nth-child(2) { - display: none; - } - .menu div:nth-child(3) { - display: none; + overflow-y: auto; + overflow-x: hidden; + padding-top: 0.2rem; + gap: 0.4rem; + padding-bottom: 0.4rem; } - .menu div:nth-child(4) { - display: none; + + .menuItem { + width: 100%; + min-width: 100%; + padding: 0.62rem 0.76rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + font-size: 0.92rem; } + .divider { - width: 30%; - height: 1px; - bottom: -10px; - left: 0; - right: 0; - margin: auto auto 0 auto; + display: none; } - } @media screen and (max-width: 1025px) { - - .sidebar{ - height: 61.5vh; + .profile { + flex-direction: column; + text-align: center; } -} -@media screen and (max-width: 916px) { - - .sidebar{ - height: 71vh; + .profileInfo { + align-items: center; } - -} -@media screen and (max-width: 801px) { - - .sidebar{ - height: 60vh; + .name { + max-width: 100%; } - } -@media screen and (max-width: 769px) { +@media screen and (max-width: 768px) { + .profile { + flex-direction: row; + text-align: left; + } - .sidebar{ - height: auto; + .profileInfo { + align-items: flex-start; } - -} \ No newline at end of file +} diff --git a/src/layouts/Profile/Topbar/Topbar.jsx b/src/layouts/Profile/Topbar/Topbar.jsx new file mode 100644 index 00000000..6372851e --- /dev/null +++ b/src/layouts/Profile/Topbar/Topbar.jsx @@ -0,0 +1,155 @@ +import React, { useContext, useEffect, useRef, useState } from "react"; +import { NavLink, useNavigate } from "react-router-dom"; +import { FiBell, FiChevronDown, FiLogOut, FiMenu, FiUser, FiX } from "react-icons/fi"; +import AuthContext from "../../../context/AuthContext"; +import styles from "./styles/Topbar.module.scss"; +import logo from "../../../assets/images/Logo/logo.svg"; +import defaultImg from "../../../assets/images/defaultImg.jpg"; + +const navLinks = [ + { label: "Home", to: "/" }, + { label: "Event", to: "/Events" }, + { label: "Social", to: "/Social" }, + { label: "Team", to: "/Team" }, + { label: "Blogs", to: "/Blog" }, +]; + +const Topbar = ({ + isSidebarOpen = false, + onToggleSidebar, + showSidebarToggle = false, +}) => { + const authCtx = useContext(AuthContext); + const navigate = useNavigate(); + const [isMenuOpen, setMenuOpen] = useState(false); + const [isMobileNavOpen, setMobileNavOpen] = useState(false); + const [animatingLink, setAnimatingLink] = useState(null); + const menuRef = useRef(null); + const navRef = useRef(null); + + useEffect(() => { + const onOutsideClick = (event) => { + if (menuRef.current && !menuRef.current.contains(event.target)) { + setMenuOpen(false); + } + if (navRef.current && !navRef.current.contains(event.target)) { + setMobileNavOpen(false); + } + }; + document.addEventListener("mousedown", onOutsideClick); + return () => document.removeEventListener("mousedown", onOutsideClick); + }, []); + + const handleLogout = () => { + setMenuOpen(false); + authCtx.logout(); + navigate("/"); + }; + + return ( +
+
+ + + FED logo + FED + +
+ + + +
+ {authCtx.isLoggedIn ? ( + <> + {/* */} +
+ + + {isMenuOpen && ( +
+ setMenuOpen(false)}> + + Profile + + +
+ )} +
+ + ) : ( + + Login + + )} +
+
+ ); +}; + +export default Topbar; diff --git a/src/layouts/Profile/Topbar/styles/Topbar.module.scss b/src/layouts/Profile/Topbar/styles/Topbar.module.scss new file mode 100644 index 00000000..5db164c3 --- /dev/null +++ b/src/layouts/Profile/Topbar/styles/Topbar.module.scss @@ -0,0 +1,353 @@ +.topbar { + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 1rem; + min-width: 0; + padding: 0.95rem 1.15rem 0.95rem 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background: linear-gradient(180deg, rgba(17, 17, 21, 0.94), rgba(12, 12, 16, 0.92)); + position: relative; + z-index: 40; +} + +:global(body.fed-modal-open) .topbar { + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.leftCluster { + display: inline-flex; + align-items: center; + gap: 0.6rem; + min-width: 0; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; +} + +.brandLogo { + width: 40px; + height: 40px; + border-radius: 50%; + + position: relative; + top: -5px; /* increase to -6px if needed */ +} +.brandText { + font-size: 2rem; + line-height: 1; + letter-spacing: 0.04em; + font-weight: 700; + color: #fff; + + position: relative; + top: -5px; /* adjust between -4px to -10px if needed */ +} +.navLinks { + justify-self: center; + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + max-width: 100%; + overflow-x: auto; + scrollbar-width: none; + gap: 0.5rem; + padding: 0.38rem; + border: 1px solid rgba(255, 255, 255, 0.13); + border-radius: 999px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)); +} + +.navLinks::-webkit-scrollbar { + display: none; +} + +.navLink { + color: rgba(255, 255, 255, 0.82); + text-decoration: none; + font-weight: 500; + font-size: 0.98rem; + border-radius: 999px; + padding: 0.47rem 0.85rem; + transition: all 0.2s ease; + overflow: hidden; + position: relative; +} + +.navLink:hover { + color: #fff; + background: rgba(255, 255, 255, 0.08); +} + +.navLabel { + display: inline-block; + transform-origin: center; +} + +.linkRoll .navLabel { + animation: rollInFromTop 0.42s cubic-bezier(0.2, 0.75, 0.2, 1); +} + +@keyframes rollInFromTop { + 0% { + transform: translateY(-130%) rotateX(85deg); + opacity: 0; + } + 55% { + transform: translateY(10%) rotateX(-10deg); + opacity: 1; + } + 100% { + transform: translateY(0) rotateX(0deg); + opacity: 1; + } +} + +.active { + color: #fff; + background: linear-gradient(135deg, rgba(255, 138, 0, 0.3), rgba(255, 99, 24, 0.24)); +} + +.actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 0.7rem; + margin-right: 1.25rem; +} + +.mobileMenuButton.iconButton { + display: none; +} + +.iconButton { + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.14); + color: rgba(255, 255, 255, 0.86); + background: rgba(255, 255, 255, 0.04); + cursor: pointer; +} + +.iconButton:hover { + border-color: rgba(255, 138, 0, 0.55); +} + +.userMenuWrap { + position: relative; +} + +.userMenu { + appearance: none; + border: none; + background: transparent; + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: rgba(255, 255, 255, 0.9); + cursor: pointer; +} + +.userAvatar { + width: 38px; + height: 38px; + border-radius: 50%; + object-fit: cover; + border: 1px solid rgba(255, 138, 0, 0.7); +} + +.dropdown { + position: absolute; + top: calc(100% + 0.45rem); + right: 0; + min-width: 140px; + padding: 0.38rem; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.13); + background: rgba(13, 13, 18, 0.96); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4); + z-index: 6; +} + +.dropdownItem { + appearance: none; + border: none; + background: transparent; + width: 100%; + color: rgba(255, 255, 255, 0.88); + text-decoration: none; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.55rem; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + text-align: left; +} + +.dropdownItem:hover { + background: rgba(255, 255, 255, 0.08); +} + +.authButton { + text-decoration: none; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + padding: 0.5rem 1rem; + border-radius: 999px; + border: 1px solid rgba(255, 138, 0, 0.5); + background: linear-gradient( + 260deg, + rgba(255, 190, 11, 0.84) -29.7%, + rgba(244, 43, 3, 0.84) 128.34% + ); +} + +@media screen and (max-width: 1024px) { + .topbar { + grid-template-columns: 1fr auto; + grid-template-areas: + "left actions" + "nav nav"; + row-gap: 0.85rem; + } + + .leftCluster { + grid-area: left; + } + + .actions { + grid-area: actions; + margin-right: 0.85rem; + } + + .navLinks { + grid-area: nav; + justify-self: stretch; + width: 100%; + overflow-x: auto; + justify-content: flex-start; + } +} + +@media screen and (max-width: 768px) { + .topbar { + grid-template-columns: 1fr auto; + grid-template-areas: "left actions"; + padding: 1rem 0.9rem 1rem 0.8rem; + gap: 0.8rem; + } + + .navLinks { + display: none; + position: absolute; + left: 0.7rem; + right: 0.7rem; + top: calc(100% + 0.55rem); + flex-direction: column; + align-items: stretch; + gap: 0.2rem; + padding: 0.5rem; + border-radius: 16px; + background: rgba(12, 12, 16, 0.98); + border: 1px solid rgba(255, 255, 255, 0.14); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.42); + z-index: 45; + } + + .mobileNavOpen { + display: flex; + } + + .mobileMenuButton.iconButton { + display: inline-flex; + width: 36px; + height: 36px; + /* Removed margin-top to keep it perfectly aligned with the logo and avatar */ + border-color: rgba(255, 138, 0, 0.45); + background: linear-gradient(180deg, rgba(255, 138, 0, 0.2), rgba(255, 99, 24, 0.16)); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.28); + } + + .leftCluster { + gap: 0.78rem; + } + + .brandLogo { + width: 38px; + height: 38px; + } + + .brandText { + font-size: 1.85rem; + } + + .actions { + gap: 0.55rem; + margin-right: 0; + } + + .authButton { + font-size: 0.85rem; + padding: 0.42rem 0.82rem; + } + + .navLink { + width: 100%; + border-radius: 10px; + padding: 0.62rem 0.75rem; + font-size: 0.94rem; + } +} + +@media screen and (max-width: 640px) { + .topbar { + padding: 0.95rem 0.85rem 0.86rem 0.72rem; + } + + .navLink { + font-size: 0.88rem; + padding: 0.42rem 0.68rem; + } + + .actions { + gap: 0.45rem; + margin-right: 0; + } + + .iconButton { + width: 32px; + height: 32px; + } + + .userAvatar { + width: 34px; + height: 34px; + } + + .dropdown { + min-width: 132px; + right: 0; + } + + .mobileMenuButton.iconButton { + display: inline-flex; + width: 34px; + height: 34px; + /* Removed margin-top here as well */ + } +} diff --git a/src/layouts/Profile/index.jsx b/src/layouts/Profile/index.jsx index 2e4f69fc..9dd5456e 100644 --- a/src/layouts/Profile/index.jsx +++ b/src/layouts/Profile/index.jsx @@ -1,3 +1,4 @@ //Profile Layout export { default as ProfileLayout } from './ProfileLayout/ProfileLayout'; -export { default as Sidebar } from './Sidebar/Sidebar'; \ No newline at end of file +export { default as Sidebar } from './Sidebar/Sidebar'; +export { default as ProfileTopbar } from './Topbar/Topbar'; diff --git a/src/pages/AttendancePage/AttendancePage.jsx b/src/pages/AttendancePage/AttendancePage.jsx index 06486c43..e83c5f03 100644 --- a/src/pages/AttendancePage/AttendancePage.jsx +++ b/src/pages/AttendancePage/AttendancePage.jsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect, useContext } from "react"; -import { EventCard } from "../../components"; +import React, { useState, useEffect, useContext, useRef } from "react"; +import { Dialog, EventCard } from "../../components"; +import modalCard from "../../components/ui/ModalCard.module.scss"; import { Button } from "../../components/Core"; import AuthContext from "../../context/AuthContext"; import { api } from "../../services"; @@ -7,7 +8,7 @@ import styles from "./styles/AttendancePage.module.scss"; import { IoClose } from "react-icons/io5"; import { FaDownload } from "react-icons/fa"; import { Alert, ComponentLoading } from "../../microInteraction"; -import { Html5QrcodeScanner, Html5QrcodeScanType } from "html5-qrcode"; +import BarcodeScanner from "react-qr-barcode-scanner"; const AttendancePage = () => { const [ongoingEvents, setOngoingEvents] = useState([]); @@ -16,12 +17,14 @@ const AttendancePage = () => { const [error, setError] = useState(null); const [showScanner, setShowScanner] = useState(false); const [selectedEventId, setSelectedEventId] = useState(null); - const [scanner, setScanner] = useState(null); const [isScanning, setIsScanning] = useState(false); const [showSuccessModal, setShowSuccessModal] = useState(false); const [attendedUser, setAttendedUser] = useState(null); const [hasShownAlert, setHasShownAlert] = useState(false); const [isSuccess, setIsSuccess] = useState(false); + const isProcessingScanRef = useRef(false); + const lastScanRef = useRef({ text: "", time: 0 }); + const SCAN_DEBOUNCE_MS = 1500; const authCtx = useContext(AuthContext); useEffect(() => { @@ -65,39 +68,12 @@ const AttendancePage = () => { fetchEvents(); }, []); - const initializeScanner = () => { - try { - const qrScanner = new Html5QrcodeScanner( - "qr-reader", - { - fps: 10, - qrbox: { width: 250, height: 250 }, - aspectRatio: 1, - supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA] - }, - false - ); - - qrScanner.render(onScanSuccess, onScanFailure); - setScanner(qrScanner); - } catch (error) { - console.error("Error initializing scanner:", error); - if (!hasShownAlert) { - Alert({ - type: "error", - message: "Failed to initialize QR scanner", - position: "top-right", - }); - setHasShownAlert(true); - } - setShowScanner(false); - } - }; - const onScanSuccess = async (decodedText) => { + if (isProcessingScanRef.current) return; + if (!selectedEventId) return; + + isProcessingScanRef.current = true; setIsScanning(true); - console.log("QR Code scanned successfully:", decodedText); - console.log("Selected Event ID:", selectedEventId); try { // jwt token from qr code @@ -115,23 +91,16 @@ const AttendancePage = () => { ); if (response.status === 200) { - // store user details setAttendedUser(response.data.user || response.data); setIsSuccess(true); - if (scanner) { - scanner.clear(); - } + setShowScanner(false); setShowSuccessModal(true); - setIsScanning(false); - - // show success alert + Alert({ type: "success", message: "Attendance marked successfully!", position: "top-right", }); - - return; // exit early } } catch (error) { console.error("Error marking attendance:", error); @@ -160,28 +129,63 @@ const AttendancePage = () => { } } finally { setIsScanning(false); + isProcessingScanRef.current = false; } }; - const onScanFailure = (error) => { - console.warn(`QR Code scanning failed: ${error}`); + const onScannerUpdate = (error, result) => { + if (error) { + const errorText = error?.message || ""; + const errorName = error?.name || ""; + const isPermissionIssue = + errorName === "NotAllowedError" || + /permission|denied|notallowed/i.test(errorText); + + if (isPermissionIssue && !hasShownAlert) { + Alert({ + type: "error", + message: "Camera permission denied. Please allow camera access.", + position: "top-right", + }); + setHasShownAlert(true); + } + return; + } + + const decodedText = result?.text || result?.getText?.(); + if (!decodedText) return; + + const now = Date.now(); + if (isProcessingScanRef.current) return; + if ( + lastScanRef.current.text === decodedText && + now - lastScanRef.current.time < SCAN_DEBOUNCE_MS + ) { + return; + } + + lastScanRef.current = { text: decodedText, time: now }; + onScanSuccess(decodedText); }; const handleScanQR = (eventId) => { setSelectedEventId(eventId); setShowScanner(true); - setHasShownAlert(false); // reset alert state - setIsSuccess(false); // reset success state + setHasShownAlert(false); + setIsSuccess(false); + isProcessingScanRef.current = false; + lastScanRef.current = { text: "", time: 0 }; }; const handleCloseSuccessModal = () => { setShowSuccessModal(false); setAttendedUser(null); - // auto open scanner for next scan setTimeout(() => { setShowScanner(true); - setHasShownAlert(false); // reset alert state - setIsSuccess(false); // reset success state + setHasShownAlert(false); + setIsSuccess(false); + isProcessingScanRef.current = false; + lastScanRef.current = { text: "", time: 0 }; }, 100); }; @@ -269,18 +273,10 @@ const AttendancePage = () => { ); useEffect(() => { - if (showScanner) { - initializeScanner(); + if (!showScanner) { + isProcessingScanRef.current = false; + lastScanRef.current = { text: "", time: 0 }; } - return () => { - if (scanner) { - try { - scanner.clear(); - } catch (error) { - console.error("Error clearing scanner in cleanup:", error); - } - } - }; }, [showScanner]); if (isLoading) { @@ -313,15 +309,9 @@ const AttendancePage = () => {
)} -
+
+ +
@@ -341,8 +337,20 @@ const AttendancePage = () => { {/* Success Modal */} {showSuccessModal && attendedUser && ( -
-
+ { + if (!next) handleCloseSuccessModal(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", + }} + > +
-
+ )} {/* Ongoing Events */} @@ -426,4 +434,4 @@ const AttendancePage = () => { ); }; -export default AttendancePage; \ No newline at end of file +export default AttendancePage; diff --git a/src/pages/AttendancePage/styles/AttendancePage.module.scss b/src/pages/AttendancePage/styles/AttendancePage.module.scss index d8031fb0..d3c114bf 100644 --- a/src/pages/AttendancePage/styles/AttendancePage.module.scss +++ b/src/pages/AttendancePage/styles/AttendancePage.module.scss @@ -53,29 +53,26 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.85); display: flex; justify-content: center; align-items: center; - z-index: 1000; + z-index: 2000; } .scannerContent { - background: #1a1a1a; padding: 2rem; - border-radius: 10px; width: 90%; - max-width: 500px; + max-width: 520px; position: relative; .closeButton { position: absolute; - top: 4px; - right: 6px; - border: none; - border-radius: 50%; - width: 34px; - height: 34px; + top: 10px; + right: 10px; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + width: 36px; + height: 36px; display: flex; align-items: center; justify-content: center; @@ -103,28 +100,19 @@ border-radius: 8px; overflow: hidden; margin: 1rem 0; - - #qr-reader { - width: 100%; - - video { - width: 100% !important; - border-radius: 6px; - } - - button { - padding: 8px 16px; - background: var(--primary); - color: white; - border: none; - border-radius: 4px; - margin: 8px; - cursor: pointer; - - &:hover { - opacity: 0.9; - } - } + position: relative; + } + + .scannerViewport { + width: 100%; + min-height: 300px; + background: #000; + + :global(video) { + width: 100% !important; + height: 300px !important; + object-fit: cover; + border-radius: 6px; } } } diff --git a/src/pages/Authentication/Login/Login.jsx b/src/pages/Authentication/Login/Login.jsx index fc2eae8e..065295d2 100644 --- a/src/pages/Authentication/Login/Login.jsx +++ b/src/pages/Authentication/Login/Login.jsx @@ -1,9 +1,7 @@ import Login from '../../../authentication/Login/Login'; const LoginMain = () => { - return ( -
- ) -} + return ; +}; -export default LoginMain \ No newline at end of file +export default LoginMain; diff --git a/src/pages/Blog/FullBlog.jsx b/src/pages/Blog/FullBlog.jsx index 175695e7..e4579bbf 100644 --- a/src/pages/Blog/FullBlog.jsx +++ b/src/pages/Blog/FullBlog.jsx @@ -80,7 +80,7 @@ const FullBlog = () => { setBlogs(processedBlogs); - // ✅ Extract departments dynamically + // Extract departments dynamically const uniqueDepts = new Set(); processedBlogs.forEach((blog) => { try { diff --git a/src/pages/Event/EventDetailPage.jsx b/src/pages/Event/EventDetailPage.jsx new file mode 100644 index 00000000..f9b797ec --- /dev/null +++ b/src/pages/Event/EventDetailPage.jsx @@ -0,0 +1,691 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable no-unused-vars */ +import React, { useState, useEffect, useContext } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import shareOutline from "../../assets/images/shareOutline.svg"; +import Share from "../../features/Modals/Event/ShareModal/ShareModal"; +import { MdGroups, MdArrowBackIos, MdLocationOn, MdCalendarToday, MdAccessTime } from "react-icons/md"; +import { FaUser, FaRupeeSign } from "react-icons/fa"; +import { PiClockCountdownDuotone } from "react-icons/pi"; +import { IoIosLock } from "react-icons/io"; +import AuthContext from "../../context/AuthContext"; +import { Blurhash } from "react-blurhash"; +import { Alert } from "../../microInteraction"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import { api } from "../../services"; +import { parse, differenceInMilliseconds } from "date-fns"; + +const EventDetailPage = () => { + useEffect(() => { window.scrollTo(0, 0); }, []); + + const navigate = useNavigate(); + const [remainingTime, setRemainingTime] = useState(""); + const [btnTxt, setBtnTxt] = useState("Register Now"); + const authCtx = useContext(AuthContext); + const [alert, setAlert] = useState(null); + const { eventId } = useParams(); + const [isLoading, setIsLoading] = useState(true); + const [info, setInfo] = useState({}); + const [data, setData] = useState({}); + const [isRegisteredInRelatedEvents, setIsRegisteredInRelatedEvents] = useState(false); + const [pastEvents, setPastEvents] = useState([]); + const [ongoingEvents, setOngoingEvents] = useState([]); + const [imageLoaded, setImageLoaded] = useState(false); + const [isShareOpen, setShareOpen] = useState(false); + + // Fetch event data + useEffect(() => { + const fetchEvent = async () => { + try { + const response = await api.get("/api/form/getAllForms"); + if (response.status === 200) { + const fetchedEvents = response.data.events; + const ongoing = fetchedEvents.filter((event) => !event.info.isEventPast); + const past = fetchedEvents.filter((event) => event.info.isEventPast); + setOngoingEvents(ongoing); + setPastEvents(past); + const eventData = fetchedEvents.find((e) => e.id === eventId); + setData(eventData); + setInfo(eventData?.info || {}); + } else { + setAlert({ type: "error", message: "Error fetching event details.", position: "bottom-right", duration: 3000 }); + } + } catch (error) { + console.error("Error fetching event:", error); + setAlert({ type: "error", message: "Error fetching event details.", position: "bottom-right", duration: 3000 }); + } finally { + setIsLoading(false); + } + }; + fetchEvent(); + }, [eventId]); + + useEffect(() => { + if (alert) { + const { type, message, position, duration } = alert; + Alert({ type, message, position, duration }); + setAlert(null); + } + }, [alert]); + + useEffect(() => { + if (info.regDateAndTime) { + calculateRemainingTime(); + const intervalId = setInterval(calculateRemainingTime, 1000); + return () => clearInterval(intervalId); + } + }, [info.regDateAndTime]); + + const dateStr = info.eventDate; + const date = new Date(dateStr); + const day = date.getDate(); + const getOrdinalSuffix = (d) => { + if (d > 3 && d < 21) return "th"; + switch (d % 10) { case 1: return "st"; case 2: return "nd"; case 3: return "rd"; default: return "th"; } + }; + const formattedDate = `${day}${getOrdinalSuffix(day)} ${date.toLocaleDateString("en-GB", { month: "long" })} ${date.getFullYear()}`; + + const calculateRemainingTime = () => { + const regStartDate = parse(info.regDateAndTime, "MMMM do yyyy, h:mm:ss a", new Date()); + const timeDifference = differenceInMilliseconds(regStartDate, new Date()); + if (timeDifference <= 0) { setRemainingTime(null); return; } + const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + const hours = Math.floor((timeDifference / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((timeDifference / (1000 * 60)) % 60); + const seconds = Math.floor((timeDifference / 1000) % 60); + setRemainingTime(days > 0 ? `${days} day${days > 1 ? "s" : ""} left` : [hours > 0 ? `${hours}h ` : "", minutes > 0 ? `${minutes}m ` : "", seconds > 0 ? `${seconds}s` : ""].join("").trim()); + }; + + useEffect(() => { + if (info.isRegistrationClosed || info.isEventPast) { setBtnTxt("Closed"); } + else if (remainingTime) { setBtnTxt(authCtx.user.access === "USER" ? "Locked" : remainingTime); } + else { setBtnTxt("Register Now"); } + }, [info.isRegistrationClosed, remainingTime]); + + useEffect(() => { + const registeredEventIds = authCtx.user.regForm || []; + const relatedEventIds = ongoingEvents.map((e) => e.info.relatedEvent).filter((id) => id && id !== "null").filter((id, i, s) => s.indexOf(id) === i); + if (registeredEventIds.length > 0 && relatedEventIds.length > 0) { + if (relatedEventIds.some((id) => registeredEventIds.includes(id))) setIsRegisteredInRelatedEvents(true); + } + }, [ongoingEvents, authCtx.user.regForm]); + + useEffect(() => { + if (authCtx.isLoggedIn && authCtx.user.regForm) { + if (info.isRegistrationClosed) { setBtnTxt("Closed"); return; } + if (authCtx.user.regForm.includes(data?.id)) { setBtnTxt("Already Registered"); return; } + if (data?.info?.relatedEvent !== "null" && authCtx.user.access === "USER") { + setBtnTxt(data?.info?.isRegistrationClosed ? "Closed" : "Locked"); return; + } + setBtnTxt(remainingTime ? remainingTime : data?.info?.isRegistrationClosed ? "Closed" : "Register Now"); + } + }, [authCtx.isLoggedIn, authCtx.user.regForm, data, info.isRegistrationClosed, isRegisteredInRelatedEvents, remainingTime]); + + const handleForm = () => { + if (authCtx.isLoggedIn) { + if (authCtx.user.access !== "USER" && authCtx.user.access !== "ADMIN") { + setBtnTxt("Already Member"); + setAlert({ type: "info", message: "Team Members are not allowed to register for the Event", position: "bottom-right", duration: 3000 }); + } else { + navigate("/Events/" + data?.id + "/Register"); + } + } else { + sessionStorage.setItem("prevPage", window.location.pathname); + navigate("/login"); + } + }; + + // Parse schedule from description if it contains schedule data + const parseSchedule = () => { + if (!info.schedule) return []; + try { return typeof info.schedule === "string" ? JSON.parse(info.schedule) : info.schedule; } + catch { return []; } + }; + const schedule = parseSchedule(); + + const url = window.location.href; + const isDisabled = ["Closed", "Already Registered", "Already Member", "Locked"].includes(btnTxt) || btnTxt === remainingTime; + + return ( + <> +
+
+ + + {isLoading ? ( + +
+
+
+ +
+
+ +
+ + +
+
+
+ + + +
+
+ + + +
+
+
+
+ + +
+ + + +
+ + +
+
+
+
+ ) : !data ? ( +
Event not found.
+ ) : ( +
+ {/* LEFT COLUMN */} +
+ {/* Banner */} +
+ {!imageLoaded && ( + + )} + {info.eventTitle} setImageLoaded(true)} + /> +
+
{formattedDate}
+ {info.ongoingEvent && ( +
setShareOpen(p => !p)}> + Share +
+ )} +
+ + {/* Event Title Row */} +
+

{info.eventTitle}

+
+ {info.participationType === "Team" ? ( + Team: {info.minTeamSize}-{info.maxTeamSize} + ) : ( + Individual + )} + + {info.eventAmount ? <>{info.eventAmount} : Free Event} + +
+
+ + {/* About Section */} +
+
+

About the Event

+
+
+

+ {info.eventdescription + ? info.eventdescription.split("\n").map((line, i) => {line}
) + : "Detailed description for this event will be updated shortly."} +

+
+ + {/* Schedule Section */} + {schedule.length > 0 && ( +
+
+

Schedule & Agenda

+
+
+
+ {schedule.map((item, i) => ( +
+ {item.time} +
+ {item.title || item.event} +
+ ))} +
+
+ )} + + {/* Location Section */} + {info.eventLocation && ( +
+
+

Location

+
+
+
+
+ {info.eventLocation} +
+
+ )} + + {/* Date & Time Section */} + {info.eventTime && ( +
+
+

Date & Time

+
+
+
+
+
+ {formattedDate} +
+
+
+ {info.eventTime} +
+
+
+ )} +
+ + {/* RIGHT COLUMN — Registration Panel */} +
+
+

Registration

+

+ {btnTxt === "Already Registered" + ? "You've already registered for this event!" + : btnTxt === "Closed" + ? "Registration for this event is now closed." + : btnTxt === "Locked" + ? "Complete prerequisites to unlock registration." + : remainingTime + ? `Registration opens in ${remainingTime}` + : "Don't miss out on this event. Secure your spot now!"} +

+ +
+
+ + {formattedDate} +
+ {info.eventTime && ( +
+ + {info.eventTime} +
+ )} + {info.eventLocation && ( +
+ + {info.eventLocation} +
+ )} +
+ +
+ + + + {/* Participation type badge */} +
+ {info.participationType === "Team" ? ( + <>Team Event ({info.minTeamSize}-{info.maxTeamSize} members) + ) : ( + <>Individual Event + )} +
+
+
+
+ )} + + {isShareOpen && setShareOpen(p => !p)} urlpath={url} />} + +
+
+ + ); +}; + +// ─── Inline Styles ──────────────────────────────────────────────────────────── + +const pageStyle = { + minHeight: "100vh", + backgroundColor: "#0c0c0c", + padding: "clamp(20px, 3vw, 40px) 5%", + fontFamily: "'Segoe UI', Roboto, Helvetica, sans-serif", + color: "#e8e8e8", +}; + +const innerPageWrapperStyle = { + maxWidth: "1440px", + margin: "0 auto", + width: "100%", +}; + +const backBtnStyle = { + display: "inline-flex", + alignItems: "center", + gap: "8px", + background: "#161616", + border: "1px solid #2a2a2a", + color: "#c9c9c9", + padding: "10px 20px", + borderRadius: "12px", + cursor: "pointer", + fontSize: "0.95rem", + fontWeight: "500", + marginBottom: "clamp(20px, 4vw, 32px)", + transition: "all 0.2s ease", +}; + +const containerStyle = { + display: "flex", + gap: "clamp(24px, 4vw, 40px)", + width: "100%", + alignItems: "flex-start", + flexWrap: "wrap", +}; + +const leftColStyle = { + flex: "1 1 60%", + minWidth: "280px", // Allows mobile screens to shrink naturally while wrapping + display: "flex", + flexDirection: "column", + gap: "clamp(24px, 4vw, 32px)", +}; + +const rightColStyle = { + flex: "1 1 320px", + position: "sticky", + top: "32px", +}; + +const bannerWrapStyle = { + position: "relative", + borderRadius: "20px", + overflow: "hidden", + background: "#1a1a1a", + width: "100%", + height: "clamp(250px, 40vw, 450px)", // Fluid height based on screen size + border: "1px solid #2a2a2a", +}; + +const bannerImgStyle = { + width: "100%", + height: "100%", + objectFit: "cover", + display: "block", + transition: "opacity 0.4s ease-in-out", +}; + +const bannerOverlayStyle = { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + height: "40%", + background: "linear-gradient(to top, rgba(12,12,12,0.9) 0%, transparent 100%)", + pointerEvents: "none", +}; + +const dateBadgeStyle = { + position: "absolute", + top: "16px", + left: "16px", + background: "rgba(10, 10, 10, 0.85)", + backdropFilter: "blur(10px)", + color: "#fff", + padding: "8px 16px", + borderRadius: "24px", + fontSize: "clamp(0.8rem, 2vw, 0.9rem)", + fontWeight: "600", + border: "1px solid rgba(255,255,255,0.15)", + zIndex: 2, +}; + +const shareBtnStyle = { + position: "absolute", + top: "16px", + right: "16px", + background: "rgba(10, 10, 10, 0.85)", + backdropFilter: "blur(10px)", + padding: "10px", + borderRadius: "50%", + cursor: "pointer", + border: "1px solid rgba(255,255,255,0.15)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 2, + transition: "background 0.2s ease", +}; + +const titleContainerStyle = { + display: "flex", + flexDirection: "column", + gap: "16px", + padding: "0 8px", +}; + +const titleStyle = { + color: "#ffffff", + fontSize: "clamp(1.75rem, 5vw, 2.5rem)", // Fluid typography + fontWeight: "800", + lineHeight: "1.2", + margin: 0, + letterSpacing: "-0.5px", +}; + +const metaRowStyle = { + display: "flex", + gap: "12px", + flexWrap: "wrap", +}; + +const metaChipStyle = { + display: "flex", + alignItems: "center", + gap: "8px", + background: "#161616", + border: "1px solid #2a2a2a", + color: "#e8e8e8", + fontSize: "clamp(0.85rem, 2vw, 0.95rem)", + fontWeight: "500", + padding: "8px 16px", + borderRadius: "24px", +}; + +const sectionStyle = { + background: "#121212", + border: "1px solid #2a2a2a", + borderRadius: "20px", + padding: "clamp(20px, 4vw, 32px)", // Fluid padding +}; + +const sectionHeaderStyle = { + display: "flex", + alignItems: "center", + marginBottom: "16px", +}; + +const sectionTitleStyle = { + color: "#ffffff", + fontSize: "clamp(1.2rem, 3vw, 1.4rem)", + fontWeight: "700", + margin: 0, +}; + +const dividerStyle = { + height: "2px", + background: "linear-gradient(90deg, #f97507 0%, rgba(249, 117, 7, 0.1) 50%, transparent 100%)", + marginBottom: "24px", + borderRadius: "2px", + width: "60%", +}; + +const descStyle = { + color: "#cfcfcf", + fontSize: "1.05rem", + lineHeight: "1.8", + margin: 0, +}; + +const scheduleItemStyle = { + display: "flex", + alignItems: "center", + gap: "16px", + borderRadius: "12px", + padding: "16px 20px", + background: "#181818", + border: "1px solid #2a2a2a", + flexWrap: "wrap", // Helps text not overflow on very small screens +}; + +const scheduleTimeStyle = { + color: "#f97507", + fontWeight: "700", + fontSize: "0.95rem", + minWidth: "90px", +}; + +const scheduleItemDividerStyle = { + width: "1px", + height: "24px", + background: "#333", + display: "block", +}; + +const locationStyle = { + display: "flex", + alignItems: "center", + gap: "16px", +}; + +const iconCircleStyle = { + background: "#1c140d", + border: "1px solid rgba(249, 117, 7, 0.2)", + padding: "10px", + borderRadius: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "44px", +}; + +// Registration Panel +const regPanelStyle = { + background: "#121212", + borderRadius: "20px", + padding: "clamp(20px, 4vw, 32px)", + border: "1px solid #2a2a2a", + display: "flex", + flexDirection: "column", + gap: "20px", +}; + +const regPanelTitleStyle = { + color: "#ffffff", + fontSize: "1.6rem", + fontWeight: "800", + margin: 0, +}; + +const regPanelSubStyle = { + color: "#a0a0a0", + fontSize: "0.95rem", + lineHeight: "1.6", + margin: 0, +}; + +const regInfoWrapStyle = { + display: "flex", + flexDirection: "column", + gap: "12px", + background: "#181818", + padding: "16px", + borderRadius: "12px", + border: "1px solid #2a2a2a", +}; + +const regInfoRowStyle = { + display: "flex", + alignItems: "center", + gap: "12px", +}; + +const regDividerStyle = { + height: "1px", + background: "#2a2a2a", + margin: "8px 0", +}; + +const regBtnStyle = { + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "10px", + width: "100%", + padding: "16px", + background: "linear-gradient(135deg, #f97507 0%, #d86200 100%)", + color: "white", + border: "none", + borderRadius: "12px", + fontSize: "1.1rem", + fontWeight: "700", + transition: "all 0.3s ease", + boxShadow: "none", // Removed glow animation shadow here +}; + +const participationBadgeStyle = { + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "10px", + color: "#a0a0a0", + fontSize: "0.9rem", + fontWeight: "500", + padding: "14px", + background: "#161616", + borderRadius: "12px", + border: "1px dashed #333", + marginTop: "8px", +}; + +export default EventDetailPage; diff --git a/src/pages/Event/EventForm.jsx b/src/pages/Event/EventForm.jsx index d8798e5f..c14219b3 100644 --- a/src/pages/Event/EventForm.jsx +++ b/src/pages/Event/EventForm.jsx @@ -1,9 +1,10 @@ -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; +import { useState, useEffect, useContext, useCallback } from "react"; +import { useParams, useSearchParams, useNavigate } from "react-router-dom"; import FormData from "../../data/FormData.json"; import PreviewForm from "../../features/Modals/Profile/Admin/PreviewForm"; +import AuthContext from "../../context/AuthContext"; import { api } from "../../services"; -import { Alert } from "../../microInteraction"; +import { Alert, ComponentLoading } from "../../microInteraction"; const EventForm = () => { const [showPreview, setShowPreview] = useState(true); @@ -11,27 +12,72 @@ const EventForm = () => { const [isLoading, setIsLoading] = useState(true); const [alert, setAlert] = useState(null); const { eventId } = useParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const authCtx = useContext(AuthContext); + + // [v2] Extract teamCode from invite link + const teamCode = searchParams.get("teamCode"); // Ensure eventId is correctly parsed const id = eventId; - // console.log("event id in eventForm is :",id); useEffect(() => { if (alert) { const { type, message, position, duration } = alert; Alert({ type, message, position, duration }); - setAlert(null); // Reset alert after displaying it + setAlert(null); } }, [alert]); + // [v2] If user is already registered and has a teamCode, auto-join the team + const handleAutoJoin = useCallback(async (formId, code) => { + try { + const response = await api.post("/api/form/joinTeam", { + formId, + teamCode: code, + }); + if (response.data?.success) { + Alert({ + type: "success", + message: response.data.message || "Successfully joined the team!", + position: "bottom-right", + duration: 3000, + }); + navigate(`/Events/${formId}/team`, { replace: true }); + return true; + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to join team"; + Alert({ + type: "error", + message: msg, + position: "bottom-right", + duration: 3000, + }); + // If already on another team or error, redirect to team management + navigate(`/Events/${formId}/team`, { replace: true }); + return false; + } + }, [navigate]); + useEffect(() => { const fetchEvent = async () => { try { const response = await api.get(`/api/form/getAllForms?id=${eventId}`); - // console.log("registerForm",response.data) if (response.status === 200) { - setEventData(response.data.events); - + const fetchedEventData = response.data.events; + setEventData(fetchedEventData); + + // [v2] Check if user is already registered for this form with a teamCode in URL + if (teamCode && authCtx.isLoggedIn) { + const isRegistered = authCtx.user?.regForm?.includes(fetchedEventData?.id || eventId); + if (isRegistered) { + // User is already registered — auto-join the team via invite link + await handleAutoJoin(fetchedEventData?.id || eventId, teamCode); + return; // Don't show the form + } + } } else { setAlert({ type: "error", @@ -43,18 +89,13 @@ const EventForm = () => { } } catch (error) { console.error("Error fetching event:", error); - - // const { events } = FormData; - // const localEventData = events.find((event) => event.id === id); - // setEventData(localEventData); } finally { setIsLoading(false); } }; fetchEvent(); - }, [id]); - // console.log("eventData",eventData); + }, [id, teamCode, authCtx.isLoggedIn]); // Add scroll lock effect when form opens useEffect(() => { @@ -72,11 +113,12 @@ const EventForm = () => { setShowPreview(false)} - eventId = {eventData?.id} + eventId={eventData?.id} sections={eventData?.sections || []} eventData={eventData?.info || {}} form={eventData || {}} showCloseBtn={true} + teamCode={teamCode} // [v2] Pass teamCode to PreviewForm /> )} @@ -85,3 +127,4 @@ const EventForm = () => { }; export default EventForm; + diff --git a/src/pages/Event/EventPage.jsx b/src/pages/Event/EventPage.jsx new file mode 100644 index 00000000..be026b31 --- /dev/null +++ b/src/pages/Event/EventPage.jsx @@ -0,0 +1,627 @@ +import { useState, useEffect, useContext, useCallback } from "react"; +import styles from "./styles/EventPage.module.scss"; +import { useNavigate, useParams } from "react-router-dom"; +import { MdGroups } from "react-icons/md"; +import { IoIosLock, IoMdInformationCircleOutline } from "react-icons/io"; +import { PiClockCountdownDuotone, PiCalendarCheckDuotone, PiMapPinAreaDuotone, PiQuestionDuotone } from "react-icons/pi"; +import { FaUser, FaRupeeSign, FaMicrophoneAlt } from "react-icons/fa"; +import AuthContext from "../../context/AuthContext"; +import "react-loading-skeleton/dist/skeleton.css"; +import { Blurhash } from "react-blurhash"; +import { + MicroLoading, + Alert, + ComponentLoading, +} from "../../microInteraction"; +import { api } from "../../services"; +import { parse, differenceInMilliseconds } from "date-fns"; +import shareOutline from "../../assets/images/shareOutline.svg"; +import Share from "../../features/Modals/Event/ShareModal/ShareModal"; +import ReactMarkdown from "react-markdown"; + +const EventPage = () => { + const navigate = useNavigate(); + const { eventId } = useParams(); + const authCtx = useContext(AuthContext); + + const [remainingTime, setRemainingTime] = useState(""); + const [btnTxt, setBtnTxt] = useState("Register Now"); + const [isMicroLoading, setIsMicroLoading] = useState(false); + const [alert, setAlert] = useState(null); + const [shouldNavigate, setShouldNavigate] = useState(false); + const [navigatePath, setNavigatePath] = useState("/"); + const [isLoading, setIsLoading] = useState(true); + const [info, setInfo] = useState({}); + const [data, setData] = useState({}); + const [isRegisteredInRelatedEvents, setIsRegisteredInRelatedEvents] = + useState(false); + const [ongoingEvents, setOngoingEvents] = useState([]); + const [imageLoaded, setImageLoaded] = useState(false); + const [isOpenShare, setIsOpenShare] = useState(false); + + useEffect(() => { + const fetchEvent = async () => { + try { + const response = await api.get("/api/form/getAllForms"); + if (response.status === 200) { + const fetchedEvents = response.data.events; + const ongoing = fetchedEvents.filter( + (event) => !event.info.isEventPast, + ); + setOngoingEvents(ongoing); + + const eventData = response.data?.events.find((e) => e.id === eventId); + setData(eventData); + setInfo(eventData?.info || {}); + + if (eventData?.info) { + updateMetaTags(eventData.info); + } + } else { + setAlert({ + type: "error", + message: + "There was an error fetching event details. Please try again.", + position: "bottom-right", + duration: 3000, + }); + } + } catch (error) { + console.error("Error fetching event:", error); + setAlert({ + type: "error", + message: "There was an error fetching event form. Please try again.", + position: "bottom-right", + duration: 3000, + }); + } finally { + setIsLoading(false); + } + }; + + fetchEvent(); + }, [eventId]); + + useEffect(() => { + if (shouldNavigate) { + navigate(navigatePath); + setShouldNavigate(false); + } + }, [shouldNavigate, navigatePath, navigate]); + + useEffect(() => { + if (alert) { + const { type, message, position, duration } = alert; + Alert({ type, message, position, duration }); + setAlert(null); + } + }, [alert]); + + const calculateRemainingTime = useCallback(() => { + const regStartDate = parse( + info.regDateAndTime, + "MMMM do yyyy, h:mm:ss a", + new Date(), + ); + const now = new Date(); + const timeDifference = differenceInMilliseconds(regStartDate, now); + + if (timeDifference <= 0) { + setRemainingTime(null); + return; + } + + const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + const hours = Math.floor((timeDifference / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((timeDifference / (1000 * 60)) % 60); + const seconds = Math.floor((timeDifference / 1000) % 60); + + let remaining; + if (days > 0) { + remaining = `${days} day${days > 1 ? "s" : ""} left`; + } else { + remaining = [ + hours > 0 ? `${hours}h ` : "", + minutes > 0 ? `${minutes}m ` : "", + seconds > 0 ? `${seconds}s` : "", + ] + .join("") + .trim(); + } + setRemainingTime(remaining); + }, [info.regDateAndTime]); + + useEffect(() => { + if (info.regDateAndTime) { + calculateRemainingTime(); + const intervalId = setInterval(calculateRemainingTime, 1000); + return () => clearInterval(intervalId); + } + }, [info.regDateAndTime, calculateRemainingTime]); + + useEffect(() => { + if (info.isRegistrationClosed || info.isEventPast) { + setBtnTxt("Closed"); + } else if (remainingTime) { + if (authCtx.user?.access === "USER") { + setBtnTxt("Locked"); + } + setBtnTxt(remainingTime); + } else { + setBtnTxt("Register Now"); + } + }, [ + info.isRegistrationClosed, + info.isEventPast, + remainingTime, + authCtx.user?.access, + ]); + + useEffect(() => { + const registeredEventIds = authCtx.user?.regForm || []; + const relatedEventIds = ongoingEvents + .map((event) => event.info.relatedEvent) + .filter((id) => id !== null && id !== undefined && id !== "null") + .filter((id, index, self) => self.indexOf(id) === index); + + let isRegRelated = false; + if (registeredEventIds.length > 0 && relatedEventIds.length > 0) { + isRegRelated = relatedEventIds.some((relatedEventId) => + registeredEventIds.includes(relatedEventId), + ); + } + if (isRegRelated) { + setIsRegisteredInRelatedEvents(true); + } + }, [ongoingEvents, authCtx.user?.regForm]); + + useEffect(() => { + if (authCtx.isLoggedIn && authCtx.user?.regForm) { + if (info.isRegistrationClosed) { + setBtnTxt("Closed"); + } + if (isRegisteredInRelatedEvents) { + if (data?.info?.relatedEvent === "null") { + if (authCtx.user.regForm.includes(data.id)) + setBtnTxt("Already Registered"); + } else { + if (authCtx.user.regForm.includes(data?.id)) { + setBtnTxt("Already Registered"); + } else { + if (remainingTime) setBtnTxt(remainingTime); + else if (data?.info?.isRegistrationClosed) setBtnTxt("Closed"); + else setBtnTxt("Register Now"); + } + } + } else { + if (data?.info?.relatedEvent === "null") { + if (authCtx.user.regForm.includes(data.id)) + setBtnTxt("Already Registered"); + else { + if (remainingTime) setBtnTxt(remainingTime); + else if (data?.info?.isRegistrationClosed) setBtnTxt("Closed"); + else setBtnTxt("Register Now"); + } + } else { + if (authCtx.user.access === "USER") { + if (data?.info?.isRegistrationClosed) setBtnTxt("Closed"); + else setBtnTxt("Locked"); + } + } + } + } + }, [ + authCtx.isLoggedIn, + authCtx.user?.regForm, + authCtx.user?.access, + data, + info.isRegistrationClosed, + info.isEventPast, + isRegisteredInRelatedEvents, + remainingTime, + ]); + + const handleShare = () => setIsOpenShare(!isOpenShare); + + const handleForm = () => { + if (authCtx.isLoggedIn) { + setIsMicroLoading(true); + if (authCtx.user.access !== "USER" && authCtx.user.access !== "ADMIN") { + setTimeout(() => { + setIsMicroLoading(false); + setBtnTxt("Already Member"); + }, 1000); + setAlert({ + type: "info", + message: "Team Members are not allowed to register for the Event", + position: "bottom-right", + duration: 3000, + }); + } else { + setNavigatePath("/Events/" + data?.id + "/Form"); + setTimeout(() => setShouldNavigate(true), 3000); + setTimeout(() => setIsMicroLoading(false), 3000); + setAlert({ + type: "info", + message: "Opening Event Registration Form", + position: "bottom-right", + duration: 3000, + }); + } + } else { + setIsMicroLoading(true); + sessionStorage.setItem("prevPage", window.location.pathname); + setNavigatePath("/login"); + setTimeout(() => setShouldNavigate(true), 3000); + setTimeout(() => setIsMicroLoading(false), 3000); + } + }; + + const getFormattedDate = () => { + if (!info.eventDate) return ""; + const date = new Date(info.eventDate); + const day = date.getDate(); + const getOrdinalSuffix = (day) => { + if (day > 3 && day < 21) return "th"; + switch (day % 10) { + case 1: + return "st"; + case 2: + return "nd"; + case 3: + return "rd"; + default: + return "th"; + } + }; + return `${day}${getOrdinalSuffix(day)} ${date.toLocaleDateString("en-GB", { month: "long" })} ${date.getFullYear()}`; + }; + + const updateMetaTags = (eventInfo) => { + document.title = `${eventInfo.eventTitle || "Event"} | FED KIIT`; + const MetaSet = (name, content) => { + let element = + document.head.querySelector(`meta[property="${name}"]`) || + document.head.querySelector(`meta[name="${name}"]`); + if (!element) { + element = document.createElement("meta"); + if (name.startsWith("og:")) element.setAttribute("property", name); + else element.setAttribute("name", name); + document.head.appendChild(element); + } + element.setAttribute("content", content); + }; + MetaSet( + "description", + eventInfo.eventdescription?.slice(0, 160) || "Event at FED KIIT", + ); + MetaSet("og:title", eventInfo.eventTitle); + MetaSet("og:description", eventInfo.eventdescription?.slice(0, 160)); + MetaSet("og:image", eventInfo.eventImg); + MetaSet("og:url", window.location.href); + MetaSet("og:type", "website"); + + // Structured JSON-LD Scheme + let scriptEle = document.querySelector("#event-schema"); + if (!scriptEle) { + scriptEle = document.createElement("script"); + scriptEle.id = "event-schema"; + scriptEle.type = "application/ld+json"; + document.head.appendChild(scriptEle); + } + scriptEle.textContent = JSON.stringify({ + "@context": "https://schema.org", + "@type": "Event", + name: eventInfo.eventTitle, + startDate: eventInfo.eventDate, + description: eventInfo.eventdescription, + image: eventInfo.eventImg, + location: { + "@type": "Place", + name: "KIIT University", + }, + }); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!data) return
Event not found
; + + return ( +
+
+
+ {!imageLoaded && ( + + )} + {info.eventImg ? ( + {info.eventTitle} setImageLoaded(true)} + /> + ) : ( +
+ +

Event Visuals Coming Soon

+
+ )} +
+
{getFormattedDate()}
+ {info.ongoingEvent && ( +
+ Share +
+ )} +
+
+
+ +
+
+
+

{info.eventTitle}

+
+ {info.participationType === "Team" ? ( + + + + Team size: {info.minTeamSize} - {info.maxTeamSize} + + + ) : ( + + + Individual + + )} + | + + + {info.eventAmount || "Free"} + +
+
+ +
+

+ About the Event +

+
+ {info.eventdescription ? ( + {info.eventdescription} + ) : ( + <> +

Welcome to our flagship event! Detailed descriptions and the official timeline for this session will be updated shortly by the organizing team.

+

In the meantime, gear up for an immersive experience filled with keynotes from industry leaders, hands-on technical workshops, and exclusive networking opportunities. This event is designed to push the boundaries of innovation and bring together the brightest minds under one roof. Stay tuned for more major announcements regarding our speaker lineup and interactive sessions!

+ + )} +
+
+ +
+

+ Schedule & Agenda +

+
+
+
10:00 AM
+
Opening Ceremony
+
+
+
11:30 AM
+
Keynote Session
+
+
+
01:00 PM
+
Networking Lunch
+
+
+
02:30 PM
+
Hands-on Workshop
+
+
+
+ +
+

+ Speakers & Hosts +

+
+
+
+ +
+
+

To be announced

+

Expert Speaker

+
+
+
+
+ +
+
+

FED Team

+

Event Hosts

+
+
+
+
+ +
+

+ Frequently Asked Questions +

+
+
+ How can I register for this event? +

You can register by clicking the "Register Now" button in the sidebar. Make sure you are logged in to your FED account to proceed with the booking.

+
+
+ Is there any registration fee? +

{info.eventAmount ? `The registration fee for this event is ₹${info.eventAmount}. Payments can be made securely through our integrated gateway.` : "This event is completely free of cost for all KIIT students and registered club members."}

+
+
+ Will I receive a certificate of participation? +

Yes, all attendees verified at the venue will receive an official e-certificate sent to their registered email address within 7 working days after the event concludes.

+
+
+ Do I need to carry my laptop? +

For technical workshops and hackathons, carrying a fully charged laptop is highly recommended. Power outlets will be stationed across the venue.

+
+
+ Are food and refreshments provided? +

Yes, light snacks and beverages will be provided during the scheduled networking breaks. Attendees are also welcome to utilize the campus cafeterias during lunch hours.

+
+
+
+
+ +
+
+
+

Registration

+

Secure your spot now before it's too late!

+
+ +
+
+ +
+ Date + {getFormattedDate()} +
+
+
+ +
+ Location + KIIT University +
+
+
+ +
+ Ticket Price + {info.eventAmount || "Free"} +
+
+
+ + + + {info.ongoingEvent && ( + + )} +
+ +
+
+ Venue +

{info.venue || "KIIT University, Bhubaneswar"}

+
+
+ Address +

{info.address || "Patia, Bhubaneswar, Odisha 751024"}

+
+
+ +
+

Organized By

+
+
+ FED Logo +
+
+ FED KIIT + Federation of Entrepreneurship Development +
+
+
+ +
+

Important Notes

+
    +
  • Please carry your KIIT Student ID card for entry.
  • +
  • Arrive at least 15 minutes before the scheduled time.
  • +
  • Registration is mandatory for all attendees.
  • +
+
+
+
+ + {isOpenShare && ( + + )} + +
+ ); +}; + +export default EventPage; diff --git a/src/pages/Event/EventRegisterPage.jsx b/src/pages/Event/EventRegisterPage.jsx new file mode 100644 index 00000000..568dd449 --- /dev/null +++ b/src/pages/Event/EventRegisterPage.jsx @@ -0,0 +1,105 @@ +/* eslint-disable no-unused-vars */ +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { api } from "../../services"; +import { Alert, ComponentLoading } from "../../microInteraction"; +import PreviewForm from "../../features/Modals/Profile/Admin/PreviewForm"; +import { MdArrowBackIos } from "react-icons/md"; +import style from "./styles/EventRegisterPage.module.scss"; + +const EventRegisterPage = () => { + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + const { eventId } = useParams(); + const navigate = useNavigate(); + const [eventData, setEventData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [alert, setAlert] = useState(null); + + useEffect(() => { + if (alert) { + const { type, message, position, duration } = alert; + Alert({ type, message, position, duration }); + setAlert(null); + } + }, [alert]); + + useEffect(() => { + const fetchEvent = async () => { + try { + const response = await api.get(`/api/form/getAllForms?id=${eventId}`); + if (response.status === 200) { + setEventData(response.data.events); + } else { + setAlert({ + type: "error", + message: "There was an error fetching event form. Please try again.", + position: "bottom-right", + duration: 3000, + }); + } + } catch (error) { + console.error("Error fetching event form:", error); + setAlert({ + type: "error", + message: "There was an error fetching event form. Please try again.", + position: "bottom-right", + duration: 3000, + }); + } finally { + setIsLoading(false); + } + }; + + fetchEvent(); + }, [eventId]); + + const handleClose = () => { + navigate(`/Events/${eventId}/details`); + }; + + return ( +
+ {/* Back Button */} + + + {isLoading ? ( + + ) : eventData ? ( +
+ +
+ ) : ( +
Event form not found.
+ )} + + +
+ ); +}; + +export default EventRegisterPage; + + diff --git a/src/pages/Event/styles/EventDetailPage.module.scss b/src/pages/Event/styles/EventDetailPage.module.scss new file mode 100644 index 00000000..a86a44cc --- /dev/null +++ b/src/pages/Event/styles/EventDetailPage.module.scss @@ -0,0 +1,250 @@ +@import "/src/assets/styles/Global.scss"; + +// ── Page wrapper ──────────────────────────────────── +.page { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 1rem 4rem; + position: relative; +} + +// ── Back button ───────────────────────────────────── +.backBtn { + display: flex; + align-items: center; + gap: 0.3rem; + background: none; + border: none; + color: white; + font-size: 0.95rem; + cursor: pointer; + align-self: flex-start; + margin-bottom: 1.5rem; + padding: 0.4rem 0.8rem; + border-radius: 8px; + transition: background 0.2s ease, color 0.2s ease; + + &:hover { + background: rgba(255, 255, 255, 0.08); + color: #f97507; + } +} + +// ── Card (full-width, centered) ────────────────────── +.card { + width: 100%; + max-width: 680px; + background: linear-gradient(90deg, + rgba(32, 32, 32, 0.95) -11.52%, + rgba(37, 37, 37, 0.95) 106.29%); + font-size: 0.75rem; + color: white; + text-align: center; + border-radius: 1.26863rem; + border: 1.2px solid rgba(214, 214, 214, 0.81); + overflow: hidden; +} + +.card a { + color: #fff; +} + +// ── Banner image area ──────────────────────────────── +.backimg { + width: 90%; + height: 18rem; + object-fit: cover; + margin-inline: auto; + margin-top: 6%; + position: relative; +} + +.img { + height: 100%; + width: 98%; + object-fit: cover; + object-position: center; + border-radius: 20px; + border: 0.973px solid #fff; + box-shadow: 1.945px 1.945px 0px 0px; +} + +// ── Date badge ─────────────────────────────────────── +.date { + background: rgba(15, 15, 15, 0.5); + backdrop-filter: blur(2px); + border: 1px solid rgb(143, 139, 139); + color: white; + position: absolute; + left: 1rem; + border-radius: 10px; + font-size: 1rem; + padding: 5px 10px; + top: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + +// ── Share button ───────────────────────────────────── +.share { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgb(143, 139, 139); + color: #f97507; + position: absolute; + top: 0.8rem; + right: 2.8rem; + text-align: center; + border-radius: 7px; + padding: 0.5rem; + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; +} + +.shareIcon { + height: 1rem; + width: 1rem; +} + +// ── Info row (name + register btn) ─────────────────── +.backbtn { + width: 87%; + display: flex; + justify-content: space-between; + margin-top: 0.8rem; + align-items: center; + margin-inline: auto; + margin-bottom: 0.5rem; +} + +.eventname { + font-size: 1.5rem; + width: 65%; + font-weight: 600; + text-align: start; + background: var(--Primary, + linear-gradient(260deg, + rgba(255, 190, 11, 0.84) -29.7%, + rgba(244, 43, 3, 0.84) 128.34%)); + color: transparent; + background-clip: text; + -webkit-background-clip: text; +} + +.eventname p { + font-size: 0.9rem; + display: flex; + align-items: center; + margin-top: 0.1rem; + color: #fff; + flex-wrap: wrap; + gap: 4px; +} + +.price { + display: flex; + align-items: center; + + p { + display: flex; + align-items: center; + gap: 2px; + color: white; + margin: 0; + } +} + +// ── Register button ────────────────────────────────── +.registerbtn { + button { + border-radius: 0.35788rem; + background: var(--Primary, + linear-gradient(260deg, + rgba(255, 190, 11, 0.84) -29.7%, + rgba(244, 43, 3, 0.84) 128.34%)); + display: inline-flex; + color: #fff; + padding: 0.6rem 1rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + outline: none; + border: none; + cursor: pointer; + font-size: 0.9rem; + min-width: 9rem; + transition: opacity 0.2s ease; + + &:disabled { + opacity: 0.7; + } + } +} + +// ── Description text ───────────────────────────────── +.backtxt { + width: 87%; + height: auto; + text-align: start; + font-size: 0.85rem; + line-height: 1.6; + margin-inline: auto; + margin-top: 0.8rem; + margin-bottom: 5%; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + color: #ddd; +} + +// ── Error state ────────────────────────────────────── +.error { + color: white; + opacity: 0.7; + font-size: 1rem; + margin-top: 3rem; +} + +// ── Responsive ─────────────────────────────────────── +@media (max-width: 600px) { + .backimg { + height: auto; + min-height: 12rem; + } + + .img { + height: auto; + } + + .eventname { + font-size: 1.1rem; + } + + .card { + border-radius: 0.8rem; + } + + .date { + font-size: 0.75rem; + } + + .registerbtn button { + min-width: 7rem; + font-size: 0.78rem; + padding: 0.5rem 0.6rem; + } +} + +@media (max-width: 380px) { + .backbtn { + flex-direction: column; + align-items: flex-start; + gap: 0.8rem; + } + + .eventname { + width: 100%; + } +} \ No newline at end of file diff --git a/src/pages/Event/styles/EventPage.module.scss b/src/pages/Event/styles/EventPage.module.scss new file mode 100644 index 00000000..5fe278fd --- /dev/null +++ b/src/pages/Event/styles/EventPage.module.scss @@ -0,0 +1,660 @@ +@import "/src/assets/styles/Global.scss"; + +.pageContainer { + background: #110a09; + min-height: 100vh; + color: white; + padding: 2rem 5% 5rem; + font-family: "Inter", "Segoe UI", sans-serif; + box-sizing: border-box; +} + +.heroSection { + width: 100%; + margin-bottom: 4rem; + border-radius: 24px; + overflow: hidden; + position: relative; + background: #110a09; + backdrop-filter: blur(15px); + border: 4px solid #110a09; + box-shadow: 0 25px 60px rgba(17, 10, 9, 0.8); + transition: all 0.3s ease; + + &:hover { + border-color: rgba(244, 43, 3, 0.3); + } +} + +.heroImageWrapper { + position: relative; + width: 100%; + padding: 1.5rem; + background: rgba(255, 255, 255, 0.02); + border-radius: 24px; + overflow: hidden; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + min-height: 400px; + + .heroImage { + width: 100%; + max-height: 500px; + object-fit: contain; + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1); + } + + .imagePlaceholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + color: rgba(255, 255, 255, 0.2); + font-weight: 500; + + p { + margin: 0; + font-size: 1.1rem; + } + } + + &:hover .heroImage { + transform: scale(1.02); + } +} + +.heroBadges { + position: absolute; + top: 2rem; + right: 2rem; + display: flex; + flex-direction: column; + gap: 1.2rem; + align-items: flex-end; + z-index: 10; +} + +.dateBadge { + background: rgba(17, 10, 9, 0.85); + backdrop-filter: blur(15px); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + border-radius: 50px; + text-align: center; + font-size: 0.95rem; + font-weight: 600; + padding: 0.8rem 1.5rem; + box-shadow: 0 8px 32px rgba(17, 10, 9, 0.4); + letter-spacing: 0.5px; +} + +.shareBadge { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05)); + backdrop-filter: blur(15px); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #fff; + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + box-shadow: 0 8px 32px rgba(17, 10, 9, 0.4); + + &:hover { + background: rgba(244, 43, 3, 0.2); + border-color: rgba(244, 43, 3, 0.5); + transform: scale(1.15) rotate(15deg); + } + + img { + height: 1.4rem; + width: 1.4rem; + filter: brightness(0) invert(1); + } +} + +.contentGrid { + display: flex; + flex-direction: column; + gap: 3rem; + max-width: 1400px; + margin: 0 auto; + + @media (min-width: 1100px) { + flex-direction: row; + align-items: flex-start; + justify-content: center; + gap: 5rem; + } +} + +.mainContent { + flex: 1; + display: flex; + flex-direction: column; + gap: 4rem; + max-width: 800px; + margin: 0 auto; +} + +.headerArea { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.eventTitle { + font-size: clamp(2.5rem, 5vw, 4rem); + font-weight: 800; + margin: 0; + line-height: 1.1; + background: linear-gradient( + 135deg, + #fff 0%, + rgba(255, 255, 255, 0.7) 100% + ); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -1px; +} + +.eventMeta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1.5rem; + font-size: 1.1rem; + color: rgba(255, 255, 255, 0.6); +} + +.metaItem { + display: flex; + align-items: center; + gap: 0.6rem; + background: rgba(19, 10, 8, 0.8); + padding: 0.5rem 1.2rem; + border-radius: 50px; + border: 1px solid rgba(244, 43, 3, 0.2); + + span { + color: #eee; + } +} + +.section { + display: flex; + flex-direction: column; + gap: 2rem; + + h2 { + font-size: 2rem; + font-weight: 700; + margin: 0; + color: #fff; + display: flex; + align-items: center; + gap: 1rem; + + &::after { + content: ""; + height: 2px; + flex: 1; + background: linear-gradient(90deg, rgba(244, 43, 3, 0.5), transparent); + } + } +} + +.richText { + font-size: 1.15rem; + line-height: 1.8; + color: rgba(255, 255, 255, 0.8); + font-weight: 400; + + p { + margin-bottom: 1.5rem; + } +} + +.agendaGrid { + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.agendaItem { + display: flex; + align-items: center; + gap: 2.5rem; + padding: 1.5rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.3s ease; + + &:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(244, 43, 3, 0.4); + transform: translateX(10px); + } + + .time { + font-weight: 700; + color: #f42b03; + font-size: 1.1rem; + min-width: 100px; + text-shadow: 0 0 20px rgba(244, 43, 3, 0.3); + } + + .event { + color: #fff; + font-size: 1.2rem; + font-weight: 500; + } +} + +.speakersGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.speakerCard { + background: rgba(17, 10, 9, 0.4); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1.5rem; + transition: all 0.3s ease; + width: 100%; + + &:hover { + transform: translateY(-5px); + border-color: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.08); + } + + .speakerAvatar { + width: 70px; + height: 70px; + border-radius: 18px; + background: linear-gradient(135deg, #f97507, #f42b03); + border: none; + display: flex; + align-items: center; + justify-content: center; + color: white; + box-shadow: 0 8px 16px rgba(17, 10, 9, 0.4); + flex-shrink: 0; + } + + .speakerInfo { + h3 { + font-size: 1.2rem; + margin: 0; + color: #fff; + border: none; + padding: 0; + font-weight: 600; + } + + p { + margin: 0.3rem 0 0; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.5); + } + } +} + +.sidebar { + width: 100%; + max-width: 420px; + display: flex; + flex-direction: column; + gap: 2rem; + + @media (min-width: 1100px) { + position: sticky; + top: 5rem; + height: max-content; + } +} + +.stickyCTA { + background: linear-gradient( + 145deg, + rgba(25, 14, 11, 0.95), + rgba(15, 8, 7, 0.95) + ); + border: 1px solid rgba(244, 43, 3, 0.3); + backdrop-filter: blur(20px); + border-radius: 30px; + padding: 2.5rem; + display: flex; + flex-direction: column; + gap: 2rem; + box-shadow: 0 25px 50px -12px rgba(17, 10, 9, 0.7); + + .ctaHeader { + h3 { + margin: 0 0 0.5rem; + font-size: 1.8rem; + font-weight: 700; + color: white; + } + + p { + margin: 0; + color: rgba(255, 255, 255, 0.6); + font-size: 1.05rem; + line-height: 1.4; + } + } + + .eventQuickDetails { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1.5rem 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + .detailItem { + display: flex; + align-items: center; + gap: 1.2rem; + + div { + display: flex; + flex-direction: column; + + strong { + color: white; + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 0.2rem; + } + + span { + color: rgba(255, 255, 255, 0.5); + font-size: 0.9rem; + } + } + } + } + + .registerBtn { + border-radius: 0.35788rem; + background: linear-gradient( + 260deg, + rgba(255, 190, 11, 0.84) -29.7%, + rgba(244, 43, 3, 0.84) 128.34% + ); + color: #fff; + padding: 1rem 1.5rem; + display: flex; + justify-content: center; + align-items: center; + gap: 0.71575rem; + outline: none; + border: none; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: opacity 0.2s ease; + + &.activeBtn { + &:hover { + opacity: 0.9; + } + &:active { + transform: translateY(1px); + } + } + + &.disabledBtn { + cursor: not-allowed; + opacity: 0.6; + background: #333; + } + } + + .secondaryShareBtn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 0.8rem; + color: white; + display: flex; + justify-content: center; + align-items: center; + gap: 0.8rem; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s ease; + + img { + width: 16px; + height: 16px; + filter: brightness(0) invert(1); + } + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.3); + } + } +} + +.hostedByCard { + background: rgba(17, 10, 9, 0.4); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 1.5rem 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + + .hostedTitle { + margin: 0; + color: rgba(255, 255, 255, 0.5); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.05rem; + font-weight: 600; + } + + .hostProfile { + display: flex; + align-items: center; + gap: 1.2rem; + + .hostAvatar { + width: 50px; + height: 50px; + background: #fff; + border-radius: 50%; + padding: 0.2rem; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + .hostDetails { + display: flex; + flex-direction: column; + + strong { + color: white; + font-size: 1.1rem; + margin-bottom: 0.2rem; + } + + span { + color: rgba(255, 255, 255, 0.4); + font-size: 0.8rem; + line-height: 1.2; + } + } + } +} + +.guidelinesCard { + background: rgba(244, 43, 3, 0.03); + border: 1px dashed rgba(244, 43, 3, 0.3); + border-radius: 20px; + padding: 1.5rem 2rem; + display: flex; + flex-direction: column; + gap: 1rem; + + h4 { + margin: 0; + color: white; + font-size: 1.1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + } + + ul { + margin: 0; + padding-left: 1.2rem; + color: rgba(255, 255, 255, 0.6); + font-size: 0.95rem; + line-height: 1.6; + + li { + margin-bottom: 0.5rem; + + &::marker { + color: #f42b03; + } + } + } +} + +.locationInfo { + background: rgba(17, 10, 9, 0.4); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + padding: 1.5rem 2rem; + display: flex; + flex-direction: column; + gap: 1.2rem; + width: 100%; + + .locationItem { + display: flex; + flex-direction: column; + gap: 0.3rem; + + strong { + color: #f42b03; + font-weight: 700; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05rem; + } + + p { + margin: 0; + color: #fff; + font-size: 1.05rem; + line-height: 1.4; + font-weight: 500; + } + } +} + + + + +.faqList { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.faqItem { + background: rgba(17, 10, 9, 0.4); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + overflow: hidden; + transition: all 0.3s ease; + + &[open] { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(244, 43, 3, 0.3); + } + + summary { + padding: 2.5rem 3rem; + font-weight: 600; + cursor: pointer; + color: #fff; + font-size: 1.35rem; + list-style: none; + display: flex; + justify-content: space-between; + align-items: center; + + &::-webkit-details-marker { + display: none; + } + + &::after { + content: "+"; + font-size: 2rem; + color: #f42b03; + transition: transform 0.3s ease; + } + } + + &[open] summary::after { + transform: rotate(45deg); + } + + p { + margin: 0; + padding: 0 3rem 2.5rem; + color: rgba(255, 255, 255, 0.7); + line-height: 1.8; + font-size: 1.25rem; + } +} + +.loadingContainer, .notFound { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 80vh; + gap: 2rem; + + h2 { + font-size: 2.5rem; + background: linear-gradient(135deg, #fff, #888); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } +} diff --git a/src/pages/Event/styles/EventRegisterPage.module.scss b/src/pages/Event/styles/EventRegisterPage.module.scss new file mode 100644 index 00000000..187a5e2a --- /dev/null +++ b/src/pages/Event/styles/EventRegisterPage.module.scss @@ -0,0 +1,44 @@ +@import "/src/assets/styles/Global.scss"; + +.page { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 1rem 4rem; +} + +.backBtn { + display: flex; + align-items: center; + gap: 0.3rem; + background: none; + border: none; + color: white; + font-size: 0.95rem; + cursor: pointer; + align-self: flex-start; + margin-bottom: 1.5rem; + padding: 0.4rem 0.8rem; + border-radius: 8px; + transition: background 0.2s ease, color 0.2s ease; + + &:hover { + background: rgba(255, 255, 255, 0.08); + color: #f97507; + } +} + +// Let PreviewForm's own styles handle the form container +// .formWrapper just provides the max-width centering +.formWrapper { + width: 100%; + max-width: 1100px; +} + +.error { + color: white; + opacity: 0.7; + font-size: 1rem; + margin-top: 3rem; +} diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index 164b6430..640c0032 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -1,11 +1,12 @@ /* eslint-disable no-unused-vars */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Hero, About, Sponser, Feedback, Contact } from "../../sections"; import { LiveEventPopup } from "../../features"; const Home = () => { - - window.scrollTo(0, 0); + useEffect(() => { + window.scrollTo(0, 0); + }, []); return ( <> diff --git a/src/pages/Home/Home.module.scss b/src/pages/Home/Home.module.scss new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/pages/Home/Home.module.scss @@ -0,0 +1 @@ + diff --git a/src/pages/Profile/Profile.jsx b/src/pages/Profile/Profile.jsx index 9c814f9d..544ab934 100644 --- a/src/pages/Profile/Profile.jsx +++ b/src/pages/Profile/Profile.jsx @@ -1,25 +1,16 @@ import { useState, useContext, useEffect } from "react"; -import { ProfileLayout, Sidebar } from "../../layouts"; -import { - ProfileView, - EventsView, - NewForm, - ViewMember, - ViewEvent, -} from "../../sections"; +import { ProfileLayout, ProfileTopbar, Sidebar } from "../../layouts"; import AuthContext from "../../context/AuthContext"; import { api } from "../../services"; import style from "./styles/Profile.module.scss"; import { Loading } from "../../microInteraction"; -import { Outlet, useNavigate } from "react-router-dom"; -import {Navbar,Footer} from "../../layouts"; +import { Outlet } from "react-router-dom"; const Profile = () => { const [activePage, setActivePage] = useState("Profile"); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); const authCtx = useContext(AuthContext); - const [designation, setDesignation] = useState("Admin"); const [isLoading, setLoading] = useState(true); - const navigate = useNavigate(); useEffect(() => { if (authCtx.isLoggedIn && window.localStorage.getItem("token")) { @@ -73,32 +64,40 @@ const Profile = () => { } }; - useEffect(() => { - const access = authCtx.user.access; - if (access === "ADMIN") { - setDesignation("Admin"); - } else if (access === "ALUMNI") { - setDesignation("Alumni"); - } else if (access === "USER") { - setDesignation("User"); - } else { - setDesignation("Member"); - } - }, [authCtx.user.access]); - - - return ( -
- { - setActivePage(page); - authCtx.eventData = null; - }} +
+ setIsSidebarOpen((prev) => !prev)} + showSidebarToggle /> - {isLoading ? :
} +
+ setIsSidebarOpen(false)} + handleChange={(page) => { + setActivePage(page); + authCtx.eventData = null; + }} + /> + {isSidebarOpen && ( +
); diff --git a/src/pages/Profile/styles/Profile.module.scss b/src/pages/Profile/styles/Profile.module.scss index 76a3fdd4..773f37e5 100644 --- a/src/pages/Profile/styles/Profile.module.scss +++ b/src/pages/Profile/styles/Profile.module.scss @@ -1,17 +1,102 @@ +.profileShell { + width: min(1460px, calc(100% - 2rem)); + height: calc(100vh - 2rem); + margin: 0 auto; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 28px; + overflow: hidden; + background: linear-gradient(140deg, rgba(21, 20, 24, 0.94), rgba(10, 10, 14, 0.97)); + backdrop-filter: blur(10px); + box-shadow: + 0 24px 56px rgba(0, 0, 0, 0.56), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + position: relative; +} + +.profileShell::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: + radial-gradient(circle at 91% 78%, rgba(255, 114, 14, 0.22), transparent 35%), + radial-gradient(circle at 3% 96%, rgba(255, 94, 0, 0.18), transparent 34%); + pointer-events: none; +} + .profile { - display: flex; - flex-direction: row; - } - + display: grid; + grid-template-columns: minmax(250px, 300px) minmax(0, 1fr); + height: calc(100% - 74px); + min-width: 0; +} + .profile__content { - width: 100%; + box-sizing: border-box; + width: 100%; + min-width: 0; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + position: relative; + z-index: 2; + padding: 1.1rem 1.2rem 1.2rem; +} + +.sidebarBackdrop { + display: none; +} + +.profile__content::-webkit-scrollbar { + width: 8px; +} + +.profile__content::-webkit-scrollbar-thumb { + border-radius: 12px; + background: linear-gradient(180deg, rgba(255, 146, 38, 0.75), rgba(231, 77, 0, 0.75)); } + +.profile__content::-webkit-scrollbar-track { + background: transparent; +} + @media screen and (max-width: 768px) { - .profile { - flex-direction: column; - } - .profile__content { - margin-left: 0; - } -} - \ No newline at end of file + .profileShell { + width: calc(100% - 0.5rem); + height: calc(100dvh - 0.5rem); + min-height: 0; + border-radius: 20px; + } + + .profile { + grid-template-columns: 1fr; + height: calc(100% - 74px); + min-height: 0; + } + + .profile__content { + height: 100%; + max-height: 100%; + overflow-y: auto; + margin-left: 0; + padding: 0.9rem 0.8rem 1rem; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + } + + .sidebarBackdrop { + display: block; + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 74px; + border: none; + background: rgba(0, 0, 0, 0.52); + backdrop-filter: blur(2px); + z-index: 32; + cursor: pointer; + } +} + diff --git a/src/pages/TeamManagement/TeamManagement.jsx b/src/pages/TeamManagement/TeamManagement.jsx new file mode 100644 index 00000000..9dc61093 --- /dev/null +++ b/src/pages/TeamManagement/TeamManagement.jsx @@ -0,0 +1,447 @@ +import React, { useState, useEffect, useContext, useCallback } from "react"; +import { useParams, useNavigate, useSearchParams } from "react-router-dom"; +import AuthContext from "../../context/AuthContext"; +import { api } from "../../services"; +import { Alert, ComponentLoading } from "../../microInteraction"; +import { Button } from "../../components/Core"; +import MemberCard from "./components/MemberCard"; +import InviteSection from "./components/InviteSection"; +import ConfirmDialog from "./components/ConfirmDialog"; +import TeamlessState from "./components/TeamlessState"; +import styles from "./styles/TeamManagement.module.scss"; +import { IoArrowBack, IoCopyOutline, IoCheckmark } from "react-icons/io5"; +import { FiEdit2, FiCheck, FiX } from "react-icons/fi"; + +const TeamManagement = () => { + const { eventId: formId } = useParams(); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const authCtx = useContext(AuthContext); + + const [teamData, setTeamData] = useState(null); + const [isTeamless, setIsTeamless] = useState(false); + const [teamlessInfo, setTeamlessInfo] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Rename state + const [isEditing, setIsEditing] = useState(false); + const [editName, setEditName] = useState(""); + const [renameLoading, setRenameLoading] = useState(false); + + // Copy state + const [codeCopied, setCodeCopied] = useState(false); + + // Confirm dialog state + const [confirmDialog, setConfirmDialog] = useState({ + isOpen: false, + title: "", + message: "", + confirmText: "", + onConfirm: () => { }, + }); + + const isLeader = teamData?.leaderEmail === authCtx.user?.email; + const isRegistrationOpen = !teamData?.isRegistrationClosed && !teamData?.isEventPast; + const spotsRemaining = teamData ? teamData.maxTeamSize - teamData.teamSize : 0; + + // [v2] Handle toast params from email action redirects + useEffect(() => { + const toast = searchParams.get("toast"); + const name = searchParams.get("name"); + if (toast) { + const toastMessages = { + joined: { type: "success", message: `${name || "User"} has been added to the team! 🎉` }, + rejected: { type: "info", message: `${name || "User"}'s join request was declined.` }, + expired: { type: "warning", message: "This request has expired." }, + already_accepted: { type: "info", message: "This request was already accepted." }, + already_rejected: { type: "info", message: "This request was already declined." }, + already_joined: { type: "info", message: `${name || "This user"} has already joined another team.` }, + team_full: { type: "warning", message: `Team is full. ${name || "The user"} could not be added.` }, + invalid: { type: "error", message: "Invalid request." }, + }; + const t = toastMessages[toast]; + if (t) { + Alert({ type: t.type, message: t.message, position: "top-right" }); + } + // Clean URL params after showing toast + searchParams.delete("toast"); + searchParams.delete("name"); + setSearchParams(searchParams, { replace: true }); + } + }, []); // Run once on mount + + const fetchTeamDetails = useCallback(async () => { + try { + setIsLoading(true); + const response = await api.get(`/api/form/teamDetails/${formId}`); + if (response.data?.success) { + const data = response.data.data; + // [v2] Check if teamless + if (data.isTeamless) { + setIsTeamless(true); + setTeamlessInfo(data); + setTeamData(null); + } else { + setIsTeamless(false); + setTeamlessInfo(null); + setTeamData(data); + setEditName(data.teamName); + } + } + } catch (err) { + console.error("Error fetching team details:", err); + const msg = err.response?.data?.message || "Failed to load team details"; + setError(msg); + } finally { + setIsLoading(false); + } + }, [formId]); + + useEffect(() => { + fetchTeamDetails(); + }, [fetchTeamDetails]); + + // Copy team code + const handleCopyCode = async () => { + try { + await navigator.clipboard.writeText(teamData.teamCode); + setCodeCopied(true); + setTimeout(() => setCodeCopied(false), 2000); + } catch { + Alert({ type: "error", message: "Failed to copy", position: "top-right" }); + } + }; + + // Rename team + const handleRenameSubmit = async () => { + const trimmed = editName.trim(); + if (!trimmed) { + Alert({ type: "error", message: "Team name cannot be empty", position: "top-right" }); + return; + } + if (trimmed.toUpperCase() === teamData.teamName) { + setIsEditing(false); + return; + } + + setRenameLoading(true); + try { + const response = await api.patch("/api/form/renameTeam", { + formId, + newTeamName: trimmed, + }); + if (response.data?.success) { + Alert({ type: "success", message: `Team renamed to "${response.data.data.teamName}"`, position: "top-right" }); + setIsEditing(false); + fetchTeamDetails(); + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to rename team"; + Alert({ type: "error", message: msg, position: "top-right" }); + } finally { + setRenameLoading(false); + } + }; + + const handleRenameCancel = () => { + setEditName(teamData.teamName); + setIsEditing(false); + }; + + // Leave team + const handleLeaveTeam = () => { + const isSoloLeader = isLeader && teamData.teamSize === 1; + + setConfirmDialog({ + isOpen: true, + title: isSoloLeader ? "Dissolve Team" : "Leave Team", + message: isSoloLeader + ? `Dissolve "${teamData.teamName}"? You'll remain registered but can create or join another team.` + : `Leave "${teamData.teamName}"? You'll remain registered and can create or join another team.`, + confirmText: isSoloLeader ? "Dissolve Team" : "Leave Team", + onConfirm: async () => { + try { + const response = await api.post("/api/form/leaveTeam", { formId }); + if (response.data?.success) { + Alert({ type: "success", message: response.data.message, position: "top-right" }); + // Refresh to show TeamlessState + fetchTeamDetails(); + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to leave team"; + Alert({ type: "error", message: msg, position: "top-right" }); + } + setConfirmDialog((prev) => ({ ...prev, isOpen: false })); + }, + }); + }; + + // Remove member + const handleRemoveMember = (memberEmail, memberName) => { + setConfirmDialog({ + isOpen: true, + title: "Remove Team Member", + message: `Remove ${memberName} from the team?`, + confirmText: "Remove Member", + onConfirm: async () => { + try { + const response = await api.post("/api/form/removeTeamMember", { formId, memberEmail }); + if (response.data?.success) { + Alert({ type: "success", message: response.data.message, position: "top-right" }); + fetchTeamDetails(); + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to remove member"; + Alert({ type: "error", message: msg, position: "top-right" }); + } + setConfirmDialog((prev) => ({ ...prev, isOpen: false })); + }, + }); + }; + + // Invite email + const handleInviteEmail = async (inviteeEmail) => { + try { + const response = await api.post("/api/form/inviteTeamMember", { + formId, + inviteeEmail, + }); + if (response.data?.success) { + Alert({ type: "success", message: response.data.message, position: "top-right" }); + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to send invitation"; + Alert({ type: "error", message: msg, position: "top-right" }); + throw err; // Re-throw to let InviteSection handle UI updates + } + }; + + // Get invite link + const handleGetInviteLink = async () => { + try { + const response = await api.get(`/api/form/inviteLink/${formId}`); + return response.data?.data; + } catch (err) { + const msg = err.response?.data?.message || "Failed to get invite link"; + Alert({ type: "error", message: msg, position: "top-right" }); + return null; + } + }; + + if (isLoading) { + return ( + + ); + } + + if (error) { + return ( +
+
+

Unable to load team

+

{error}

+ +
+
+ ); + } + + if (!teamData && !isTeamless) return null; + + // [v2] Teamless state — show create/browse UI + if (isTeamless && teamlessInfo) { + return ( +
+ + +
+ ); + } + + if (!teamData) return null; + + return ( +
+ {/* Back Navigation */} + + + {/* Header Card */} +
+
+

{teamData.eventTitle}

+ {!isRegistrationOpen && ( + Registration Closed + )} +
+ +
+
+ {isEditing ? ( +
+ setEditName(e.target.value)} + className={styles.editNameInput} + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter") handleRenameSubmit(); + if (e.key === "Escape") handleRenameCancel(); + }} + disabled={renameLoading} + /> + + +
+ ) : ( + <> +

Team: {teamData.teamName}

+ {isLeader && isRegistrationOpen && ( + + )} + + )} +
+ +
+
+ Team Code + +
+
+ Members + + {teamData.teamSize}/{teamData.maxTeamSize} + +
+
+ Min Required + {teamData.minTeamSize} +
+
+
+
+ + {/* Members Section */} +
+

+ Team Members ({teamData.teamSize}) +

+
+ {teamData.members.map((member) => ( + + ))} +
+
+ + {/* Invite Section — Leader Only, Team Not Full, Registration Open */} + {isLeader && spotsRemaining > 0 && isRegistrationOpen && ( +
+

+ Add Team Members ({spotsRemaining} {spotsRemaining === 1 ? "spot" : "spots"} remaining) +

+ +
+ )} + + {/* Action Footer */} +
+ {/* Non-leader: Leave Team */} + {!isLeader && isRegistrationOpen && ( + + )} + + {/* Leader, sole member: Dissolve Team */} + {isLeader && teamData.teamSize === 1 && isRegistrationOpen && ( + + )} + + {/* Leader with members: Info message */} + {isLeader && teamData.teamSize > 1 && isRegistrationOpen && ( +

+ As the team leader, you must remove all members before you can leave the team. +

+ )} + + {!isRegistrationOpen && ( +

+ Registration is closed. Team changes are no longer allowed. +

+ )} +
+ + {/* Confirm Dialog */} + setConfirmDialog((prev) => ({ ...prev, isOpen: false }))} + /> +
+ ); +}; + +export default TeamManagement; \ No newline at end of file diff --git a/src/pages/TeamManagement/components/ConfirmDialog.jsx b/src/pages/TeamManagement/components/ConfirmDialog.jsx new file mode 100644 index 00000000..c115363d --- /dev/null +++ b/src/pages/TeamManagement/components/ConfirmDialog.jsx @@ -0,0 +1,58 @@ +import React, { useState } from "react"; +import styles from "../styles/TeamManagement.module.scss"; +import { Dialog } from "../../../components"; +import modalCard from "../../../components/ui/ModalCard.module.scss"; + +const ConfirmDialog = ({ isOpen, title, message, confirmText, onConfirm, onCancel }) => { + const [loading, setLoading] = useState(false); + + if (!isOpen) return null; + + const handleConfirm = async () => { + setLoading(true); + try { + await onConfirm(); + } finally { + setLoading(false); + } + }; + + return ( + { + if (!next) onCancel(); + }} + contentStyle={{ + "--dialog-padding": "0", + "--dialog-surface": "transparent", + "--dialog-border": "none", + "--dialog-shadow": "none", + }} + > +
+

{title}

+

{message}

+
+ + +
+
+
+ ); +}; + +export default ConfirmDialog; diff --git a/src/pages/TeamManagement/components/InviteSection.jsx b/src/pages/TeamManagement/components/InviteSection.jsx new file mode 100644 index 00000000..185c38a8 --- /dev/null +++ b/src/pages/TeamManagement/components/InviteSection.jsx @@ -0,0 +1,186 @@ +import React, { useState } from "react"; +import styles from "../styles/TeamManagement.module.scss"; +import { IoMailOutline, IoLinkOutline, IoKeyOutline, IoCopyOutline, IoCheckmark, IoLogoWhatsapp } from "react-icons/io5"; + +const InviteSection = ({ onInviteEmail, onGetInviteLink, teamCode }) => { + const [activeTab, setActiveTab] = useState("email"); + const [inviteEmail, setInviteEmail] = useState(""); + const [sendingEmail, setSendingEmail] = useState(false); + + // Link state + const [inviteLinkData, setInviteLinkData] = useState(null); + const [linkLoading, setLinkLoading] = useState(false); + const [linkCopied, setLinkCopied] = useState(false); + + // [v2] Team Code tab commented out — users now join via browse/request flow + // const [codeCopied, setCodeCopied] = useState(false); + + const handleSendEmail = async (e) => { + e.preventDefault(); + if (!inviteEmail.trim()) return; + + setSendingEmail(true); + try { + await onInviteEmail(inviteEmail.trim()); + setInviteEmail(""); + } catch { + // Error already handled in parent + } finally { + setSendingEmail(false); + } + }; + + const handleGetLink = async () => { + setLinkLoading(true); + const data = await onGetInviteLink(); + if (data) { + setInviteLinkData(data); + } + setLinkLoading(false); + }; + + const handleCopyLink = async () => { + if (!inviteLinkData) return; + try { + await navigator.clipboard.writeText(inviteLinkData.inviteLink); + setLinkCopied(true); + setTimeout(() => setLinkCopied(false), 2000); + } catch { + // Fallback + } + }; + + const handleShareWhatsApp = () => { + if (!inviteLinkData) return; + const text = encodeURIComponent(inviteLinkData.shareText); + window.open(`https://wa.me/?text=${text}`, "_blank"); + }; + + // [v2] Team Code copy handler commented out + // const handleCopyCode = async () => { + // try { + // await navigator.clipboard.writeText(teamCode); + // setCodeCopied(true); + // setTimeout(() => setCodeCopied(false), 2000); + // } catch { + // // Fallback + // } + // }; + + const tabs = [ + { id: "email", label: "Email Invite", icon: }, + { id: "link", label: "Share Link", icon: }, + // [v2] Team Code tab removed — users join via browse/request flow + // { id: "code", label: "Team Code", icon: }, + ]; + + return ( +
+ {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Tab Content */} +
+ {/* Email Tab */} + {activeTab === "email" && ( +
+ setInviteEmail(e.target.value)} + placeholder="teammate@kiit.ac.in" + className={styles.emailInput} + required + disabled={sendingEmail} + /> + +
+ )} + + {/* Link Tab */} + {activeTab === "link" && ( +
+ {!inviteLinkData ? ( + + ) : ( +
+
{inviteLinkData.inviteLink}
+
+ + +
+
+ )} +
+ )} + + {/* [v2] Code Tab commented out — users join via browse/request flow + {activeTab === "code" && ( +
+

+ Share this code with your teammates. They can enter it when selecting "Join Team" during registration. +

+
+ {teamCode} + +
+
+ )} + */} +
+
+ ); +}; + +export default InviteSection; \ No newline at end of file diff --git a/src/pages/TeamManagement/components/MemberCard.jsx b/src/pages/TeamManagement/components/MemberCard.jsx new file mode 100644 index 00000000..11d3d7f3 --- /dev/null +++ b/src/pages/TeamManagement/components/MemberCard.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import styles from "../styles/TeamManagement.module.scss"; +import { FaCrown, FaTrash } from "react-icons/fa"; + +const MemberCard = ({ member, isCurrentUserLeader, isCardLeader, isCurrentUser, onRemove, isRegistrationOpen }) => { + // Generate initials for avatar fallback + const initials = member.name + ? member.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2) + : "?"; + + return ( +
+
+ {member.img ? ( + {member.name} + ) : ( +
{initials}
+ )} + {isCardLeader && ( + + + + )} +
+ +
+
+ {member.name || "Unknown"} + {isCurrentUser && You} +
+ {member.email} + {(member.college || member.year) && ( + + {[member.year, member.college].filter(Boolean).join(" • ")} + + )} +
+ + {isCurrentUserLeader && !isCurrentUser && isRegistrationOpen && onRemove && ( + + )} +
+ ); +}; + +export default MemberCard; diff --git a/src/pages/TeamManagement/components/TeamlessState.jsx b/src/pages/TeamManagement/components/TeamlessState.jsx new file mode 100644 index 00000000..82915338 --- /dev/null +++ b/src/pages/TeamManagement/components/TeamlessState.jsx @@ -0,0 +1,281 @@ +import React, { useState, useCallback, useEffect, useRef } from "react"; +import { api } from "../../../services"; +import { Alert } from "../../../microInteraction"; +import { IoSearch, IoClose, IoPersonAdd, IoAdd } from "react-icons/io5"; +import styles from "../styles/TeamManagement.module.scss"; + +const TeamlessState = ({ formId, eventTitle, maxTeamSize, onTeamJoined }) => { + const [activeTab, setActiveTab] = useState("browse"); // "browse" | "create" + const [searchQuery, setSearchQuery] = useState(""); + const [teams, setTeams] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [newTeamName, setNewTeamName] = useState(""); + const [isCreating, setIsCreating] = useState(false); + const [sendingRequestTo, setSendingRequestTo] = useState(null); + const pollRef = useRef(null); + + // Fetch teams on mount and when search changes + const fetchTeams = useCallback(async (query = "") => { + setIsSearching(true); + try { + const url = query.trim() + ? `/api/form/searchTeams/${formId}?search=${encodeURIComponent(query)}` + : `/api/form/searchTeams/${formId}`; + const response = await api.get(url); + if (response.data?.success) { + setTeams(response.data.data.teams || []); + } + } catch (err) { + console.error("Error fetching teams:", err); + } finally { + setIsSearching(false); + } + }, [formId]); + + useEffect(() => { + fetchTeams(); + }, [fetchTeams]); + + // Debounced search + useEffect(() => { + const timer = setTimeout(() => { + fetchTeams(searchQuery); + }, 300); + return () => clearTimeout(timer); + }, [searchQuery, fetchTeams]); + + // [v2] Poll for join request updates (accepted/rejected/expired) + const checkForUpdates = useCallback(async () => { + try { + const response = await api.get(`/api/form/joinRequestUpdates/${formId}`); + if (!response.data?.success) return; + + const { updates } = response.data.data; + if (!updates || updates.length === 0) return; + + for (const update of updates) { + const teamLabel = update.teamName ? `"${update.teamName}"` : "the team"; + + switch (update.status) { + case "ACCEPTED": + Alert({ + type: "success", + message: `🎉 Your request to join ${teamLabel} was accepted!`, + position: "top-right", + duration: 5000, + }); + // Refresh parent to show team view + setTimeout(() => onTeamJoined(), 1500); + return; // Stop processing — page will re-render + case "REJECTED": + Alert({ + type: "error", + message: `Your request to join ${teamLabel} was declined. Try another team!`, + position: "top-right", + duration: 5000, + }); + break; + case "AUTO_EXPIRED": + case "EXPIRED": + Alert({ + type: "info", + message: `Your request to join ${teamLabel} has expired.`, + position: "top-right", + duration: 4000, + }); + break; + default: + break; + } + } + // Refresh team list to update pending statuses + fetchTeams(searchQuery); + } catch (err) { + // Silent fail — polling shouldn't break the UI + console.error("Error checking join request updates:", err); + } + }, [formId, fetchTeams, searchQuery, onTeamJoined]); + + // Poll on mount + every 15 seconds + useEffect(() => { + checkForUpdates(); // Check immediately on mount (covers offline → online scenario) + pollRef.current = setInterval(checkForUpdates, 15000); + return () => { + if (pollRef.current) clearInterval(pollRef.current); + }; + }, [checkForUpdates]); + + // Create team handler + const handleCreateTeam = async () => { + const trimmed = newTeamName.trim(); + if (!trimmed) { + Alert({ type: "error", message: "Team name cannot be empty", position: "top-right" }); + return; + } + + setIsCreating(true); + try { + const response = await api.post("/api/form/createTeam", { + formId, + teamName: trimmed, + }); + if (response.data?.success) { + Alert({ type: "success", message: response.data.message, position: "top-right" }); + onTeamJoined(); // Refresh parent + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to create team"; + Alert({ type: "error", message: msg, position: "top-right" }); + } finally { + setIsCreating(false); + } + }; + + // Send join request handler + const handleSendRequest = async (teamRegId) => { + setSendingRequestTo(teamRegId); + try { + const response = await api.post("/api/form/sendJoinRequest", { + formId, + teamRegistrationId: teamRegId, + }); + if (response.data?.success) { + Alert({ + type: "success", + message: "Request sent! Check your email for the team leader's decision.", + position: "top-right", + duration: 5000, + }); + // Refresh to show "Pending" status + fetchTeams(searchQuery); + } + } catch (err) { + const msg = err.response?.data?.message || "Failed to send join request"; + Alert({ type: "error", message: msg, position: "top-right" }); + } finally { + setSendingRequestTo(null); + } + }; + + return ( +
+
+

{eventTitle}

+

You're registered! Now create or join a team to participate.

+
+ + {/* Tab Switcher */} +
+ + +
+ + {/* Browse Tab */} + {activeTab === "browse" && ( +
+
+ + setSearchQuery(e.target.value)} + className={styles.searchInput} + /> + {searchQuery && ( + + )} +
+ + {isSearching ? ( +
Searching teams...
+ ) : teams.length === 0 ? ( +
+

No teams found. Be the first to create one!

+
+ ) : ( +
+ {teams.map((team) => ( +
+
+

{team.teamName}

+ + {team.teamSize}/{team.maxTeamSize} members · Led by {team.leaderName} + + + {team.spotsRemaining} {team.spotsRemaining === 1 ? "spot" : "spots"} remaining + +
+
+ {team.hasPendingRequest ? ( + + ) : ( + + )} +
+
+ ))} +
+ )} +
+ )} + + {/* Create Tab */} + {activeTab === "create" && ( +
+

+ Create your own team and invite others to join. + Max team size: {maxTeamSize}. +

+
+ setNewTeamName(e.target.value)} + className={styles.createInput} + onKeyDown={(e) => { + if (e.key === "Enter") handleCreateTeam(); + }} + disabled={isCreating} + /> + +
+
+ )} +
+ ); +}; + +export default TeamlessState; diff --git a/src/pages/TeamManagement/styles/TeamManagement.module.scss b/src/pages/TeamManagement/styles/TeamManagement.module.scss new file mode 100644 index 00000000..80d8c05f --- /dev/null +++ b/src/pages/TeamManagement/styles/TeamManagement.module.scss @@ -0,0 +1,1119 @@ +/* TeamManagement Page Styles — FED Dark Theme */ + +.container { + padding: 2rem 3rem; + max-width: 100%; /* Stretched width */ + width: 100%; + margin: 0 auto; + min-height: calc(100vh - 80px); /* Fill viewport minus navbar */ + box-sizing: border-box; + /* Keeping the gradient background as it adds a nice glow, but the rest is solid */ + background: radial-gradient(1200px 600px at 10% -10%, rgba(255, 138, 0, 0.08), transparent 60%), + radial-gradient(900px 500px at 90% 0%, rgba(0, 214, 214, 0.08), transparent 60%); + border-radius: 18px; +} + +/* Back Button */ +.backButton { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: none; + border: none; + color: var(--primary); + font-size: 0.95rem; + cursor: pointer; + padding: 0.5rem 0; + margin-bottom: 1.5rem; + transition: opacity 0.2s, transform 0.2s; + + &:hover { + opacity: 0.85; + transform: translateX(-2px); + } +} + +/* Error State */ +.errorState { + text-align: center; + padding: 4rem 2rem; + + h2 { + color: #fff; + margin-bottom: 0.5rem; + } + + p { + color: #aaa; + margin-bottom: 1.5rem; + } +} + +/* Header Card */ +.headerCard { + background: #161616; /* Solid background */ + border-radius: 16px; + padding: 1.75rem 2rem; + border: 1px solid #333; /* Solid border */ + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + margin-bottom: 2rem; +} + +.headerTop { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.eventTitle { + font-size: 1.45rem; + font-weight: 700; + color: #fff; + margin: 0; + letter-spacing: 0.2px; +} + +.closedBadge { + font-size: 0.75rem; + padding: 0.25rem 0.75rem; + border-radius: 20px; + background: #331111; /* Solid dark red */ + color: #ff4d4d; + font-weight: 600; + white-space: nowrap; + border: 1px solid #551111; +} + +.teamInfo { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.teamNameRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.teamName { + font-size: 1.1rem; + font-weight: 600; + color: var(--primary); + margin: 0; + text-shadow: 0 0 16px rgba(255, 138, 0, 0.25); +} + +.editButton { + background: none; + border: none; + color: #888; + cursor: pointer; + padding: 0.3rem; + font-size: 0.9rem; + border-radius: 4px; + transition: all 0.2s; + + &:hover { + color: var(--primary); + background: #2a1800; /* Solid dark orange tint */ + } +} + +/* Edit Name Group */ +.editNameGroup { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; +} + +.editNameInput { + background: #222; /* Solid background */ + border: 1px solid #444; /* Solid border */ + border-radius: 8px; + padding: 0.5rem 0.75rem; + color: #fff; + font-size: 1rem; + font-weight: 600; + outline: none; + flex: 1; + min-width: 0; + + &:focus { + border-color: var(--primary); + } +} + +.editAction { + background: #003314; /* Solid dark green */ + border: 1px solid #005522; + color: #00c853; + cursor: pointer; + padding: 0.4rem; + border-radius: 8px; + font-size: 1.1rem; + display: flex; + align-items: center; + transition: all 0.2s; + + &:hover { + background: #00441a; + } +} + +.editActionCancel { + background: #333; /* Solid background */ + border: 1px solid #555; + color: #aaa; + cursor: pointer; + padding: 0.4rem; + border-radius: 8px; + font-size: 1.1rem; + display: flex; + align-items: center; + transition: all 0.2s; + + &:hover { + background: #444; + } +} + +/* Team Meta */ +.teamMeta { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.metaItem { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.metaLabel { + font-size: 0.75rem; + color: #888; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; +} + +.metaValue { + font-size: 1rem; + color: #fff; + font-weight: 600; +} + +.codeBadge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + background: #2a1800; /* Solid dark orange tint */ + border: 1px solid #553000; + border-radius: 8px; + padding: 0.3rem 0.6rem; + color: var(--primary); + font-size: 0.9rem; + font-weight: 700; + font-family: monospace; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #3a2200; + } +} + +/* Sections */ +.section { + margin-bottom: 2rem; +} + +.sectionTitle { + font-size: 1rem; + font-weight: 600; + color: #eee; + margin: 0 0 1rem 0; + padding-bottom: 0.5rem; + border-bottom: 1px solid #333; +} + +/* Member Grid */ +.memberGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.75rem; +} + +/* Member Card */ +.memberCard { + display: flex; + align-items: center; + gap: 1rem; + background: #1a1a1a; /* Solid background */ + border: 1px solid #333; + border-radius: 12px; + padding: 1rem 1.25rem; + transition: border-color 0.2s; + + &:hover { + border-color: #555; + } +} + +.removeMemberButton { + background: transparent; + color: #ff4d4d; + border: none; + font-size: 0.85rem; + padding: 0.5rem; + cursor: pointer; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + margin-left: auto; + + &:hover { + background: #331111; /* Solid dark red */ + transform: scale(1.1); + } +} + +.currentUser { + border-color: #553000; + background: #1f140a; /* Solid very dark orange */ +} + +.memberAvatar { + position: relative; + flex-shrink: 0; + + img { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + border: 2px solid #444; + } +} + +.avatarFallback { + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, #2d2d2d, #3a3a3a); + display: flex; + align-items: center; + justify-content: center; + color: var(--primary); + font-weight: 700; + font-size: 1rem; + border: 2px solid #444; +} + +.leaderBadge { + position: absolute; + top: -4px; + right: -4px; + background: #1a1a1a; + border-radius: 50%; + padding: 3px; + color: #ffd700; + font-size: 0.7rem; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #554400; +} + +.memberInfo { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; +} + +.memberNameRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.memberName { + font-size: 0.95rem; + font-weight: 600; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.youBadge { + font-size: 0.65rem; + padding: 0.1rem 0.4rem; + border-radius: 4px; + background: #2a1800; /* Solid dark orange */ + color: var(--primary); + font-weight: 600; + white-space: nowrap; +} + +.memberEmail { + font-size: 0.8rem; + color: #888; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.memberMeta { + font-size: 0.75rem; + color: #666; +} + +/* ========== Invite Section ========== */ + +.inviteSection { + background: #161616; /* Solid background */ + border-radius: 16px; + border: 1px solid #333; + overflow: hidden; +} + +/* Fixed Tabs - Rounded XL, Solid backgrounds */ +.inviteTabs { + display: flex; + gap: 0.5rem; + padding: 0.5rem; + background: #0a0a0a; /* Solid dark background container */ + border-bottom: 1px solid #222; +} + +.inviteTab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.85rem 0.5rem; + background: #1e1e1e; /* Solid default background */ + border: 1px solid #333; + border-radius: 12px; /* rounded-xl effect */ + color: #888; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + color: #fff; + background: #2a2a2a; + } +} + +.activeTab { + color: var(--primary) !important; + background: #2a1800 !important; /* Solid active state */ + border-color: #553000 !important; + font-weight: 600; +} + +.inviteContent { + padding: 1.5rem; + min-height: 160px; + display: flex; + flex-direction: column; + justify-content: center; +} + +/* Solid Email Form Inputs */ +.emailForm { + display: flex; + gap: 0.75rem; + + @media (max-width: 500px) { + flex-direction: column; + } +} + +.emailInput { + flex: 1; + background: #222; /* Solid background */ + border: 1px solid #444; /* Solid border */ + border-radius: 8px; + padding: 0.65rem 1rem; + color: #fff; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; + + &::placeholder { + color: #666; + } + + &:focus { + border-color: var(--primary); + } +} + +.sendButton { + background: var(--primary); + color: #fff; + border: none; + border-radius: 8px; + padding: 0.65rem 1.25rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s; + + &:hover:not(:disabled) { + background: #e67a00; + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +/* Link Tab */ +.linkContent { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-height: 56px; +} + +.generateLinkButton { + background: #1f140a; /* Solid background */ + border: 1px dashed #553000; + border-radius: 8px; + padding: 1rem; + color: var(--primary); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover:not(:disabled) { + background: #2a1800; + } + + &:disabled { + opacity: 0.5; + cursor: wait; + } +} + +.linkDisplay { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.linkText { + background: #222; /* Solid background */ + border: 1px solid #444; + border-radius: 8px; + padding: 0.75rem 1rem; + color: #ddd; + font-size: 0.82rem; + font-family: monospace; + word-break: break-all; +} + +.linkActions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.copyLinkButton, +.whatsappButton { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.55rem 1rem; + border-radius: 8px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + border: none; +} + +.copyLinkButton { + background: #2a1800; /* Solid background */ + color: var(--primary); + border: 1px solid #553000; + + &:hover { + background: #3a2200; + } +} + +.whatsappButton { + background: #0a3314; /* Solid background */ + color: #25d366; + border: 1px solid #115522; + + &:hover { + background: #11441a; + } +} + +/* Code Tab */ +.codeContent { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.codeLabel { + color: #888; + font-size: 0.85rem; + margin: 0; + line-height: 1.5; +} + +.codeDisplay { + display: flex; + align-items: center; + justify-content: space-between; + background: #222; /* Solid background */ + border: 1px solid #444; + border-radius: 8px; + padding: 0.75rem 1rem; +} + +.codeText { + font-family: monospace; + font-size: 1.2rem; + font-weight: 700; + color: var(--primary); + letter-spacing: 0.05em; +} + +.copyCodeButton { + display: inline-flex; + align-items: center; + gap: 0.3rem; + background: #2a1800; /* Solid background */ + border: 1px solid #553000; + color: var(--primary); + border-radius: 8px; + padding: 0.4rem 0.75rem; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #3a2200; + } +} + +/* ========== Action Footer ========== */ + +.actionFooter { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid #333; + display: flex; + align-items: center; + justify-content: center; +} + +.dangerButton { + background: transparent; + border: 1.5px solid #ff4d4d; /* Solid border */ + color: #ff4d4d; + padding: 0.65rem 2rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #331111; /* Solid dark red */ + transform: translateY(-1px); + } +} + +.leaderNote, +.closedNote { + color: #777; + font-size: 0.85rem; + text-align: center; + font-style: italic; + margin: 0; +} + +/* ========== Confirm Dialog ========== */ + +.dialogOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); /* Overlay needs opacity to blur */ + backdrop-filter: blur(4px); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + animation: fadeIn 0.2s ease; +} + +.dialogContent { + background: transparent; + border: none; + padding: 2rem; + width: 90%; + max-width: 420px; +} + +.dialogTitle { + color: #fff; + font-size: 1.2rem; + font-weight: 700; + margin: 0 0 0.75rem 0; +} + +.dialogMessage { + color: #aaa; + font-size: 0.9rem; + line-height: 1.6; + margin: 0 0 1.5rem 0; +} + +.dialogActions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; +} + +.dialogCancel { + background: #333; /* Solid */ + border: 1px solid #555; + color: #ddd; + padding: 0.55rem 1.25rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #444; + } +} + +.dialogConfirm { + background: #331111; /* Solid */ + border: 1px solid #661111; + color: #ff4d4d; + padding: 0.55rem 1.25rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #441111; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +/* ========== Animations ========== */ + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.92); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* ========== Responsive ========== */ + +@media (max-width: 900px) { + .container { + padding: 1.5rem 2rem; + } +} + +@media (max-width: 600px) { + .container { + padding: 1rem; + } + + .headerCard { + padding: 1.25rem; + } + + .eventTitle { + font-size: 1.15rem; + } + + .teamMeta { + gap: 1.25rem; + } + + .memberGrid { + grid-template-columns: 1fr; + } + + .editNameGroup { + flex-wrap: wrap; + } +} + +/* ========== Teamless State (v2) ========== */ + +.teamlessContainer { + margin-top: 0.5rem; +} + +.teamlessHeader { + text-align: center; + margin-bottom: 2rem; + + h2 { + font-size: 1.6rem; + font-weight: 700; + color: #fff; + margin: 0 0 0.5rem 0; + } + + p { + color: #888; + font-size: 0.95rem; + margin: 0; + } +} + +/* Fixed Tabs - Rounded XL, Solid backgrounds */ +.tabSwitcher { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; + background: #0a0a0a; /* Solid dark background container */ + border-radius: 16px; + border: 1px solid #222; + padding: 0.5rem; +} + +.tab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.85rem 0.75rem; + background: #1e1e1e; /* Solid default background */ + border: 1px solid #333; + border-radius: 12px; /* rounded-xl effect */ + color: #888; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + color: #fff; + background: #2a2a2a; + } +} + +.tabActive { + color: var(--primary) !important; + background: #2a1800 !important; /* Solid active state */ + border-color: #553000 !important; + font-weight: 600; +} + +/* Browse Section */ +.browseSection { + animation: fadeIn 0.2s ease; +} + +.searchBar { + position: relative; + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.searchIcon { + position: absolute; + left: 0.85rem; + color: #666; + font-size: 1.05rem; + pointer-events: none; +} + +.searchInput { + width: 100%; + background: #222; /* Solid background */ + border: 1px solid #444; /* Solid border */ + border-radius: 12px; + padding: 0.7rem 2.5rem 0.7rem 2.5rem; + color: #fff; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; + + &::placeholder { + color: #666; + } + + &:focus { + border-color: var(--primary); + } +} + +.clearSearch { + position: absolute; + right: 0.6rem; + background: none; + border: none; + color: #666; + cursor: pointer; + padding: 0.25rem; + font-size: 1rem; + display: flex; + align-items: center; + + &:hover { + color: #fff; + } +} + +.loadingTeams { + text-align: center; + padding: 2rem 0; + color: #888; + font-size: 0.9rem; +} + +.noTeams { + text-align: center; + padding: 3rem 1rem; + color: #888; + font-size: 0.9rem; + + p { + margin: 0; + } +} + +.teamList { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.teamCard { + display: flex; + align-items: center; + justify-content: space-between; + background: #1a1a1a; /* Solid background */ + border: 1px solid #333; + border-radius: 12px; + padding: 1rem 1.25rem; + transition: border-color 0.2s; + + &:hover { + border-color: #555; + } +} + +.teamCardInfo { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; + flex: 1; +} + +.teamCardName { + font-size: 1rem; + font-weight: 600; + color: #fff; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.teamCardMeta { + font-size: 0.8rem; + color: #888; +} + +.teamCardSpots { + font-size: 0.75rem; + color: var(--primary); + font-weight: 500; +} + +.teamCardAction { + flex-shrink: 0; + margin-left: 1rem; +} + +.requestButton { + background: var(--primary); + color: #fff; + border: none; + border-radius: 8px; + padding: 0.5rem 1rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s; + + &:hover:not(:disabled) { + background: #e67a00; + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.6; + cursor: wait; + } +} + +.pendingButton { + background: #332200; /* Solid background */ + border: 1px solid #554400; + color: #ffc107; + border-radius: 8px; + padding: 0.5rem 1rem; + font-size: 0.85rem; + font-weight: 600; + cursor: default; + white-space: nowrap; +} + +/* Create Section */ +.createSection { + background: #1a1a1a; /* Solid background */ + border: 1px solid #333; + border-radius: 16px; + padding: 1.5rem; + animation: fadeIn 0.2s ease; +} + +.createHint { + color: #888; + font-size: 0.9rem; + margin: 0 0 1.25rem 0; + + strong { + color: var(--primary); + } +} + +.createForm { + display: flex; + gap: 0.75rem; + + @media (max-width: 500px) { + flex-direction: column; + } +} + +.createInput { + flex: 1; + background: #222; /* Solid background */ + border: 1px solid #444; /* Solid border */ + border-radius: 8px; + padding: 0.7rem 1rem; + color: #fff; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; + + &::placeholder { + color: #666; + } + + &:focus { + border-color: var(--primary); + } +} + +.createButton { + background: var(--primary); + color: #fff; + border: none; + border-radius: 8px; + padding: 0.7rem 1.5rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s; + + &:hover:not(:disabled) { + background: #e67a00; + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +/* ========== Teamless Responsive ========== */ +@media (max-width: 600px) { + .teamlessHeader { + h2 { + font-size: 1.3rem; + } + } + + .teamCard { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .teamCardAction { + margin-left: 0; + width: 100%; + + button { + width: 100%; + } + } +} diff --git a/src/sections/Home/Contact/Contact.jsx b/src/sections/Home/Contact/Contact.jsx index 4f92527f..7546056f 100644 --- a/src/sections/Home/Contact/Contact.jsx +++ b/src/sections/Home/Contact/Contact.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import React, { useState, useEffect } from "react"; import { api } from "../../../services"; import styles from "./styles/Contact.module.scss"; @@ -10,7 +9,6 @@ import { Alert, MicroLoading } from "../../../microInteraction"; const ContactForm = () => { const [alert, setAlert] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); useEffect(() => { if (alert) { @@ -19,19 +17,6 @@ const ContactForm = () => { } }, [alert]); - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth <= 768); - }; - - window.addEventListener('resize', handleResize); - - // Cleanup the event listener on component unmount - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); - const handleSubmit = async (event) => { event.preventDefault(); setIsLoading(true); @@ -49,26 +34,18 @@ const ContactForm = () => { if (response.status === 200 || response.status === 201) { setAlert({ type: "success", - message: - "Your message has been submitted! We will get back to you soon.", + message: "Message sent successfully.", position: "bottom-right", duration: 3000, }); event.target.reset(); } else { - setAlert({ - type: "error", - message: - "There was an error submitting your message. Please try again.", - position: "bottom-right", - duration: 3000, - }); + throw new Error(); } - } catch (error) { + } catch { setAlert({ type: "error", - message: - "There was an error submitting your message. Please try again.", + message: "Failed to send message.", position: "bottom-right", duration: 3000, }); @@ -78,55 +55,56 @@ const ContactForm = () => { }; return ( -
-
-

- GET IN TOUCH -

-
-
-
-
- -
-
- -
-
- -
- -
+
+
+
+

+ Get In Touch +

+
+
+ +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ + +
+
- {!isMobile && ( -
-
- - Contact - -
-
- )} -
+
+ + Contact Illustration + +
@@ -134,4 +112,4 @@ const ContactForm = () => { ); }; -export default ContactForm; +export default ContactForm; \ No newline at end of file diff --git a/src/sections/Home/Contact/styles/Contact.module.scss b/src/sections/Home/Contact/styles/Contact.module.scss index 6e1f763a..017cfa36 100644 --- a/src/sections/Home/Contact/styles/Contact.module.scss +++ b/src/sections/Home/Contact/styles/Contact.module.scss @@ -1,273 +1,142 @@ -.contactFormContainer { - position: relative; +.section { + background: #111111; + padding: 8rem 0; display: flex; - align-items: center; justify-content: center; - flex-direction: column; - padding: 20px; - background-color: #1C1C1C; - color: white; - margin-top: 1rem; +} + +.container { width: 100%; + max-width: 1200px; + padding: 0 2rem; } -.contactFormContainer h2 { - font-size: 50px; - margin-bottom: 10px; - margin-top: 1.5rem; +/* ===== Header ===== */ + +.header { + text-align: center; + margin-bottom: 5rem; } -.bottomLine { - width: 10rem; - height: 0.3125rem; - background: var(--primary); - color: transparent; - border-radius: 5px; - margin-bottom: 3rem; +.header h2 { + font-size: 3rem; + font-weight: 700; + color: #ffffff; } -.highlight { +.header span { background: var(--primary); background-clip: text; color: transparent; } -.formSection { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - margin-top: 20px; - margin-left: 0rem; +.line { + width: 6rem; + height: 4px; + background: var(--primary); + margin: 1.5rem auto 0 auto; + border-radius: 4px; } -.contactForm { - width: 60%; - padding-left: 30px; - padding-right: 60px; -} +/* ===== Grid Layout ===== */ -.formGroup { - margin-bottom: 15px; +.grid { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 4rem; + align-items: center; } -.formGroup input, -.formGroup textarea { - width: 100%; - padding: 10px; - border-radius: 5px; - font-size: 16px; - border: 1px solid rgba(255, 255, 255, 0.40); - background: rgba(255, 255, 255, 0.10); - backdrop-filter: blur(7.5px); - color: white; -} +/* ===== Form ===== */ -.formGroup textarea::-webkit-scrollbar { - width: 10px; - height: 0px; - transition: .2s linear; - background-color: transparent; +.formWrapper { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(12px); + padding: 3rem; + border-radius: 2rem; } -.formGroup textarea::-webkit-scrollbar-thumb { - border-radius: 22px; - background: var(--primary); - // border: 1.5px solid hwb(0 11% 89%); +.form { + display: flex; + flex-direction: column; + gap: 1.5rem; } -.formGroup textarea::-webkit-scrollbar-track { - padding: 0px 20px; - background-color: transparent; +.field input, +.field textarea { + width: 100%; + padding: 1rem 1.2rem; + border-radius: 12px; + font-size: 1rem; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.06); + color: white; + outline: none; + transition: border 0.2s ease; } -.formGroup input { - height: 60px; +.field input:focus, +.field textarea:focus { + border: 1px solid var(--primary); } -.formGroup textarea { - height: 200px; - resize: none; +.field input { + height: 55px; } -// .contactForm button { -// background: var(--primary); -// color: transparent; -// color: white; -// border: none; -// padding: 10px 20px; -// border-radius: 15px; -// cursor: pointer; -// font-size: 16px; -// width: 100%; -// } - -.contactForm Button:hover { - transition: .2s linear; - background: linear-gradient(260deg, rgba(255, 190, 11, .9) -29.7%, rgba(244, 43, 3, .9) 128.34%); +.field textarea { + height: 160px; + resize: none; } -.imageSection { - width: 50%; - position: relative; - margin-left: 125px; -} +/* ===== Visual ===== */ -.backCircle { - width: 20rem; - height: 10rem; - flex-shrink: 0; - margin-top: 4rem; - z-index: 1; - padding-left: 40px; - border-radius: 32.99306rem; - opacity: 0.4; - background: rgba(255, 92, 0, 0.29); - mix-blend-mode: color-dodge; - filter: blur(110.9010238647461px); - position: absolute; - right: 150px; - top: 150px; +.visual { + display: flex; + justify-content: center; + align-items: center; } -.contactFormContainer img { - width: 80%; - height: 70%; - margin-right: 0; +.visual img { + width: 100%; + max-width: 450px; + height: auto; } -.circle { - height: 50vh; - width: 50vh; - background-color: rgba(255, 92, 0, 0.6); - position: absolute; - top: 15%; - right: -10%; - opacity: 50%; - border-radius: 50%; - filter: blur(150px); - z-index: 1; -} +/* ===== Responsive ===== */ -/* Responsive Styles */ @media (max-width: 1024px) { - .contactForm { - padding-left: 10px; - padding-right: 10px; - width: 100%; + .grid { + grid-template-columns: 1fr; + gap: 3rem; } - .formSection { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - z-index: 0; - } - .circle { - top: -5%; - right: -20%; - } - .formGroup input, - .formGroup textarea { - width: 100%; - } - form button { - width: 100%; - } - .imageSection { - margin-left: 3rem; - } - .circle { - margin-top: -30rem; - margin-right: -6rem; - z-index: -1; + + .visual { + order: -1; } - .backCircle { - position: absolute; - left: 2%; - top: -10%; + + .formWrapper { + padding: 2.5rem; } } @media (max-width: 768px) { - .formSection { - flex-direction: column; - align-items: center; - } - .contactForm { - width: 150%; - // padding: 0 20px; + .header h2 { + font-size: 2.2rem; } - .imageSection { - width: 100%; - text-align: center; - margin-top: 20px; - } - .contactFormContainer img { - width: 60%; - height: auto; - margin-right: 2rem; - } - .backCircle { - width: 15rem; - height: 7rem; - right: 90%; - top: 50%; - transform: translateX(50%); + + .formWrapper { + padding: 2rem; } } @media (max-width: 480px) { - .contactFormContainer h2 { - font-size: 30px; - } - .bottomLine{ - width: 6rem; - } - - .contactForm{ - width: 150%; - } - .imageSection{ - width: 100%; - margin-top: 40px; - } - - .form-section{ - margin-top: -3rem; - } - - .form-group{ - width: 110%; - margin-left: -2rem; - } - - .submitbutton{ - margin-left: -2rem; - width: 100%; - } - - .backCircle{ - display: none; + .header h2 { + font-size: 1.8rem; } - .circle{ - top: 25rem; - background-color: rgba(255, 92, 0, 0.59); - + .formWrapper { + padding: 1.5rem; } - - -} - -@media (max-width: 430px){ - .contactForm{ - width: 150%; - } -} -@media (max-width: 400px){ - .contactForm{ - width: 150%; - } -} - +} \ No newline at end of file diff --git a/src/sections/Home/Feedback/Feedback.jsx b/src/sections/Home/Feedback/Feedback.jsx index 906a71d8..83453c17 100644 --- a/src/sections/Home/Feedback/Feedback.jsx +++ b/src/sections/Home/Feedback/Feedback.jsx @@ -1,78 +1,73 @@ -import { useState, useRef, useEffect} from "react"; +import { useRef, useEffect, useState } from "react"; import styles from "./styles/Feedback.module.scss"; import feedbackData from "../../../data/Feedback.json"; -import quoteImg from "../../../assets/images/quote.png"; +import { FaQuoteLeft } from "react-icons/fa"; const Feedback = () => { - const feedbacksRef = useRef(null); + const trackRef = useRef(null); - const FeedbackCard = ({ quote }) => { - const [isExpanded, setIsExpanded] = useState(false); + const FeedbackCard = ({ item }) => { + const [expanded, setExpanded] = useState(false); - const toggleExpand = () => { - setIsExpanded(!isExpanded); - }; - - const truncatedQuote = quote.quote.length > 200 - ? `${quote.quote.substring(0, 160)}...` - : quote.quote; + const shortText = + item.quote.length > 180 + ? item.quote.slice(0, 150) + "..." + : item.quote; return ( -
-
-

- {isExpanded ? quote.quote : truncatedQuote} - {quote.quote.length > 160 && !isExpanded && ( - read more - )} -

-
+
+ + +

setExpanded(!expanded)} + > + {expanded ? item.quote : shortText} + {item.quote.length > 150 && !expanded && ( + read more + )} +

-
-

{quote.title}

-

{quote.post}

+
+

{item.title}

+ {item.post}
); }; useEffect(() => { - const feedbacksContainer = feedbacksRef.current; - const handleMouseEnter = () => { - feedbacksContainer.style.animationPlayState = "paused"; - }; - const handleMouseLeave = () => { - feedbacksContainer.style.animationPlayState = "running"; - }; + const track = trackRef.current; + const pause = () => (track.style.animationPlayState = "paused"); + const run = () => (track.style.animationPlayState = "running"); - feedbacksContainer.addEventListener("mouseenter", handleMouseEnter); - feedbacksContainer.addEventListener("mouseleave", handleMouseLeave); + track.addEventListener("mouseenter", pause); + track.addEventListener("mouseleave", run); return () => { - feedbacksContainer.removeEventListener("mouseenter", handleMouseEnter); - feedbacksContainer.removeEventListener("mouseleave", handleMouseLeave); + track.removeEventListener("mouseenter", pause); + track.removeEventListener("mouseleave", run); }; }, []); return ( -
- Up Quote +

- TESTIMONIALS + Testimonials Voices

-
+
-
-
- {feedbackData.concat(feedbackData).map((quote, index) => ( - + +
+
+ {[...feedbackData, ...feedbackData].map((item, i) => ( + ))}
- Down Quote -
+
); }; -export default Feedback; +export default Feedback; \ No newline at end of file diff --git a/src/sections/Home/Feedback/styles/Feedback.module.scss b/src/sections/Home/Feedback/styles/Feedback.module.scss index c8a6df36..e427f659 100644 --- a/src/sections/Home/Feedback/styles/Feedback.module.scss +++ b/src/sections/Home/Feedback/styles/Feedback.module.scss @@ -1,217 +1,145 @@ -.feedbackContainer { - background-color: #1C1C1C; - color: #ffffff; - text-align: center; - position: relative; +.section { + background: #111111; + padding: 8rem 0; overflow: hidden; - padding-bottom: 60px; -} -.FeedbackMsg{ - width: 260px; - height: 132px; - margin-bottom: 8px; - overflow-y: scroll; -} -::-webkit-scrollbar{ - visibility: hidden; -} -.feedbackContainer .upQuote { - width: 150px; - height: 90px; - position: absolute; - right: 0%; - top: 15%; } -.feedbackContainer .downQuote { - width: 150px; - height: 90px; - position: absolute; - left: 0%; - bottom: 0%; - rotate: 180deg; - margin-bottom: 5px; +/* ===== Heading ===== */ + +.heading { + text-align: center; + margin-bottom: 5rem; } -.feedbackContainer h2 { - font-size: 50px; - margin-bottom: 20px; - padding-top: 20px; +.heading h2 { + font-size: 3rem; + font-weight: 700; + color: #ffffff; } -.bottomLine { - width: 12rem; - height: 0.3125rem; +.heading span { background: var(--primary); + background-clip: text; color: transparent; - border-radius: 5px; - margin-bottom: 4rem; } -.heading { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - margin-top: 3rem; - font-size: 1.7rem; -} -.heading span{ - background: var(--primary); - background-clip: text; - color: transparent; -} -.highlight { +.line { + width: 6rem; + height: 4px; background: var(--primary); - background-clip: text; - color: transparent; + margin: 1.5rem auto 0 auto; + border-radius: 4px; } -.feedbacksContainer { +/* ===== Marquee ===== */ + +.marquee { overflow: hidden; width: 100%; - position: relative; } -.feedbacks { +.track { display: flex; - animation: scroll 20s linear infinite; - margin-left: 1300px; - + gap: 3rem; + width: max-content; + animation: scroll 38s linear infinite; } +@keyframes scroll { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +/* ===== Card ===== */ -.feedbackCard { - border-radius: 1.5rem; - border: 1px solid rgba(255, 255, 255, 0.40); - background: rgba(255, 255, 255, 0.10); - backdrop-filter: blur(7.5px); - padding: 20px; - margin: 10px; - min-width: 300px; - text-align: left; +.card { + position: relative; + min-width: 380px; + max-width: 380px; + padding: 2.8rem 2.2rem 2.2rem 2.2rem; + border-radius: 1.8rem; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(10px); display: flex; - align-items: flex-start; - justify-content: space-between; flex-direction: column; - height: auto; - + justify-content: space-between; } -@keyframes scroll { - 0% { - transform: translateX(0); - } - 100% { - transform: translateX(-50%); - } +/* Quote Icon */ + +.quoteIcon { + font-size: 2rem; + color: var(--primary); + opacity: 0.25; + margin-bottom: 1.5rem; } +/* Text */ -.feedbackText { - font-size: 1em; - margin-bottom: 20px; - white-space: normal; /* Allow text to wrap */ +.text { + font-size: 1rem; + line-height: 1.8; + color: #e5e5e5; + margin-bottom: 2rem; + cursor: pointer; } -.feedbackAuthor { +.more { + color: var(--primary); + font-weight: 600; +} + +/* Author */ + +.author h4 { + font-weight: 600; background: var(--primary); - background-clip: text; - color: transparent; - font-weight: bold; + background-clip: text; + color: transparent; margin-bottom: 5px; - white-space: normal; } -.feedbackEv { - font-size: 0.9em; - color: #cccccc; - white-space: normal; +.author span { + font-size: 0.9rem; + color: #a0a0a0; } -/* Responsive Styles */ -@media (max-width: 768px) { - .feedbackContainer h2 { - font-size: 1.5em; - } +/* ===== Responsive ===== */ - .heading { - font-size: 2rem; +@media (max-width: 768px) { + .heading h2 { + font-size: 2.2rem; } - .feedbackCard { - min-width: 250px; - padding: 15px; + .card { + min-width: 300px; + max-width: 300px; } - .feedbackContainer .upQuote, - .feedbackContainer .downQuote { - width: 100px; - height: 60px; + .track { + gap: 2rem; + animation-duration: 30s; } } @media (max-width: 480px) { - .feedbackContainer{ - width: 100%; - } - - .FeedbackMsg{ - width: 220px; - height: 100px; - overflow-y: scroll; - } - .feedbackContainer h2 { - font-size: 30px; - } - - .bottomLine{ - width: 7rem; - } - - .feedbackCard { - min-width: 240px; - padding: 15px; - } - - .heading { + .heading h2 { font-size: 1.8rem; - margin-top: 20px; - } - - .feedbackText { - font-size: 0.86em; - margin-bottom: 15px; } - .feedbackAuthor { - font-size: 0.7em; + .card { + min-width: 260px; + max-width: 260px; + padding: 1.8rem; } - .feedbackEv { - font-size: 0.6em; + .track { + gap: 1.5rem; + animation-duration: 26s; } - .feedbacks { - display: flex; - animation: scroll 17s linear infinite; - margin-left: -1500px; - margin-right: -2000px; - + .quoteIcon { + font-size: 1.6rem; } - - .feedbackContainer .upQuote, - .feedbackContainer .downQuote { - width: 80px; - height: 50px; - } - - .feedbackContainer .upQuote { - top: 37%; - } - - .feedbackContainer .downQuote { - bottom: 5%; - } -} +} \ No newline at end of file diff --git a/src/sections/Home/Hero/Hero.jsx b/src/sections/Home/Hero/Hero.jsx index 6323b991..dbaa5cc6 100644 --- a/src/sections/Home/Hero/Hero.jsx +++ b/src/sections/Home/Hero/Hero.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import styles from "./styles/Hero.module.scss"; import CarouselImg from "../../../data/Carousel.json"; import { Carousel } from "../../../components"; -import { AnimatedBox } from "../../../assets/animations/AnimatedBox"; +import heroBgImage from "../../../assets/images/herobgimage.png"; const titles = [ "Entrepreneurship.", @@ -26,74 +26,51 @@ function Hero() { useEffect(() => { const title = titles[titleIndex]; - const typingSpeed = isDeleting ? 50 : 150; + const typingSpeed = isDeleting ? 50 : 100; - const interval = setInterval(() => { + const timeout = setTimeout(() => { if (isDeleting) { setCurrentTitle(title.substring(0, charIndex - 1)); - setCharIndex(charIndex - 1); + setCharIndex((prev) => prev - 1); } else { setCurrentTitle(title.substring(0, charIndex + 1)); - setCharIndex(charIndex + 1); + setCharIndex((prev) => prev + 1); } if (!isDeleting && charIndex === title.length) { - setIsDeleting(true); + setTimeout(() => setIsDeleting(true), 800); } else if (isDeleting && charIndex === 0) { setIsDeleting(false); - setTitleIndex((titleIndex + 1) % titles.length); + setTitleIndex((prev) => (prev + 1) % titles.length); } }, typingSpeed); - return () => clearInterval(interval); + return () => clearTimeout(timeout); }, [charIndex, isDeleting, titleIndex]); return ( -
+
-
- -
-

- Nurturing Using Innovative & Creative strategies{" "} - -

{currentTitle}

- {" "} -

-
-
-
-

- Inspiring{" "} - - visionaries - {" "} - towards cultivating excellence and enrouting future - generations towards growth. -

-
-
-
+
+

+ Nurturing Innovative & Creative Strategies{" "} + {currentTitle} +

+ +

+ Inspiring visionaries towards cultivating excellence and guiding + future generations toward sustainable growth. +

-
+ +
-
-
+
); } diff --git a/src/sections/Home/Hero/styles/Hero.module.scss b/src/sections/Home/Hero/styles/Hero.module.scss index 3e51b42c..b0a99ef2 100644 --- a/src/sections/Home/Hero/styles/Hero.module.scss +++ b/src/sections/Home/Hero/styles/Hero.module.scss @@ -1,219 +1,138 @@ .main { - height: auto; width: 100%; - background-color: #1C1C1C; - margin-top: 20px; + background: radial-gradient( + circle at 20% 30%, + rgba(255, 138, 0, 0.16), + transparent 40% + ), + linear-gradient( + 115deg, + rgba(8, 10, 14, 0.86) 8%, + rgba(8, 10, 14, 0.68) 52%, + rgba(8, 10, 14, 0.78) 100% + ), + var(--hero-bg-image), + radial-gradient( + circle at 80% 70%, + rgba(255, 60, 0, 0.12), + transparent 40% + ), + #0f1115; + background-size: auto, auto, cover, auto, auto; + background-position: center, center, center, center, center; + background-repeat: no-repeat; + + padding: clamp(60px, 7vw, 110px) clamp(24px, 6vw, 100px); /* reduced top */ + margin-top: -20px; /* shifts whole section slightly upward */ } .hero { width: 100%; - display: flex; - flex-direction: row; - background-color: #1C1C1C; - position: relative; - margin-top: 2rem; - padding-bottom: 4.5vw; - padding-left: 0px; - z-index: 0; -} + max-width: 1200px; + margin: 0 auto; -.heroTextContainer { - height: 100%; - width: 100%; - z-index: 1; - padding-left: 2.5rem; - margin-top: 3rem; - -} - -.heroCarousel { - display: flex; - justify-content: center; + display: grid; + grid-template-columns: 1.1fr 0.9fr; align-items: center; - position: relative; -} - -.main span{ - color: transparent; - background-clip: text; -} - -.largeContent p { - font-weight: 700; - font-size: 2.45rem; - color: #FFFFFF; -} - -.largeContent h3{ - animation: fadeIn 3s ease-in-out; - white-space: nowrap; + gap: clamp(48px, 6vw, 90px); } -.dynamicText { - height: 65px; +.heroText { display: flex; - justify-content: left; - align-items: center; - text-align: center; - overflow: hidden; + flex-direction: column; + gap: 28px; } -.typing { - display: inline-block; - white-space: nowrap; - border-right: 4px solid; - animation: blink 0.75s step-end infinite; - max-width: 100%; - overflow: hidden; -} +.heading { + margin: 0; + font-weight: 800; + line-height: 1.05; + font-size: clamp(2.2rem, 4.8vw, 4rem); -.smallContent p { - font-size: 1.1rem; - color: #FFFFFF; - letter-spacing: 0.1vw; - font-weight: 500; + color: #ffffff; + letter-spacing: -0.02em; + max-width: 20ch; } -// .circle { -// height: 52rem; -// width: 50rem; -// background-color: rgba(255, 92, 0, 0.29); -// filter: blur(100px); -// position: absolute; -// top: 60%; -// opacity: 50%; -// left: -35%; -// border-radius: 50%; -// overflow: hidden; -// z-index: -1; -// } - -.circle { - height: 52rem; - width: 50rem; - flex-shrink: 0; - // margin-top: 4rem; - z-index: 1; - // padding-left: 40px; - border-radius: 32.99306rem; - opacity: 0.5; - background: rgba(255, 92, 0, 0.29); - // mix-blend-mode: color-dodge; - filter: blur(110.9010238647461px); - position: absolute; - top: 60%; - left: -35%; -} - - -.maincontent { - padding-top: 50px; - display: flex; - align-items: center; -} - -.smallContent { - display: flex; - align-items: center; -} +.dynamicText { + display: block; + margin-top: 14px; + background: linear-gradient( + 90deg, + #ffb347, + #ff7a00, + #ff3c00 + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; -.smallContainer { - padding-top: 2vw; - padding-bottom: 3vw; + min-height: 1.2em; } +.subtext { + margin: 0; + font-size: clamp(1rem, 1.4vw, 1.15rem); + line-height: 1.75; -@media (max-width: 1025px) { -.largeContent p{ - font-size: 1.3rem; -} - -.smallContainer strong{ - font-size: 0.8rem; + color: rgba(255, 255, 255, 0.75); + max-width: 55ch; } +.heroMedia { + width: 100%; + max-width: 580px; + justify-self: end; + position: relative; } -@media screen and (max-width: 768px) { +.heroMedia::before { + content: ""; + position: absolute; + inset: -20px; + background: radial-gradient( + circle, + rgba(255, 122, 0, 0.15), + transparent 60% + ); + filter: blur(40px); + z-index: -1; +} + +/* Tablet */ +@media (max-width: 1024px) { .hero { - width: 100%; - display: flex; - flex-direction: column-reverse; - background-color: #1C1C1C; - position: relative; - margin-top: 1rem; - margin-right: 0rem; + grid-template-columns: 1fr; + gap: 64px; } - .smallcontent { - padding-top: 1rem; + .heroMedia { + justify-self: center; + max-width: 680px; } - .heroTextContainer { - z-index: 1; - padding-left: 25px; - padding-right: 30px; + .heading { + max-width: 100%; } } - - -@media screen and (max-width: 480px) { - - .largeContent p{ - font-size: 1.2rem; - text-align: center; - - } - - .heroCarousel{ - margin-top: 1rem; +/* Mobile */ +@media (max-width: 640px) { + .main { + padding: 80px 24px; + } - .dynamicText { - height: 45px; /* Set to a fixed height */ - display: flex; - justify-content: center; - align-items: center; + .heroText { text-align: center; - overflow: hidden; - - } - - .smallContent p{ - font-size: 1rem; - } - .hero { - width: 100%; - display: flex; - flex-direction: column-reverse; - background-color: #1C1C1C; - position: relative; align-items: center; } - .heroTextContainer{ - margin-top: 4rem; - margin-left: 1rem; - justify-content: center; - text-align: center; - word-spacing: 0.1rem; - } - - .main{ - margin-top: -1rem; + .subtext { + max-width: 100%; } - .smallContent { - padding-top: 2rem; + .heading { + letter-spacing: -0.01em; } - - .circle{ - top: 80%; - left: -150%; - opacity: 50%; - height: 100vw; - background-color: rgba(255, 92, 0, 0.3); - } -} \ No newline at end of file +} diff --git a/src/sections/Home/Sponser/Sponser.jsx b/src/sections/Home/Sponser/Sponser.jsx index 7f0c79d7..bd690596 100644 --- a/src/sections/Home/Sponser/Sponser.jsx +++ b/src/sections/Home/Sponser/Sponser.jsx @@ -1,42 +1,17 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import SponserImg from '../../../data/Sponser.json'; import styles from './styles/Sponser.module.scss'; -import Carousel from '../../../components/Carousel/Carousel'; import SkeletonCard from '../../../layouts/Skeleton/Sponser/Sponser'; import { Blurhash } from 'react-blurhash'; const Sponser = () => { - const [itemsPerPage, setItemsPerPage] = useState(12); const [loading, setLoading] = useState(true); useEffect(() => { - const updateItemsPerPage = () => { - if (window.innerWidth < 480) { - setItemsPerPage(6); - } else { - setItemsPerPage(12); - } - }; - - updateItemsPerPage(); - window.addEventListener('resize', updateItemsPerPage); - - // Simulate loading time - setTimeout(() => setLoading(false), 2000); - - return () => { - window.removeEventListener('resize', updateItemsPerPage); - }; + const timer = setTimeout(() => setLoading(false), 900); + return () => clearTimeout(timer); }, []); - const groupSponserCards = () => { - const groupedSponsers = []; - for (let i = 0; i < SponserImg.length; i += itemsPerPage) { - groupedSponsers.push(SponserImg.slice(i, i + itemsPerPage)); - } - return groupedSponsers; - }; - const SponserCard = ({ image }) => { const [isImageLoaded, setIsImageLoaded] = useState(false); @@ -45,8 +20,8 @@ const Sponser = () => { {!isImageLoaded && ( { )} Sponsor logo setIsImageLoaded(true)} - style={{ display: isImageLoaded ? 'block' : 'none' }} + style={{ opacity: isImageLoaded ? 1 : 0 }} />
); }; - const groupedSponserCards = groupSponserCards(); - return ( - <> -
- our Sponsors +
+
+

Partnership Ecosystem

+

+ Our Sponsors +

+
-
-
-
- - {groupedSponserCards.map((group, index) => ( -
- {group.map((image, idx) => ( - loading ? : - ))} -
- ))} -
+ + {/* Top Track */} +
+
+ {[...SponserImg, ...SponserImg].map((image, index) => + loading ? ( + + ) : ( + + ) + )} +
+
+ + {/* Bottom Track (reverse direction) */} +
+
+ {[...SponserImg.slice().reverse(), ...SponserImg.slice().reverse()].map((image, index) => + loading ? ( + + ) : ( + + ) + )}
- +
); }; -export default Sponser; +export default Sponser; \ No newline at end of file diff --git a/src/sections/Home/Sponser/styles/Sponser.module.scss b/src/sections/Home/Sponser/styles/Sponser.module.scss index adea514a..1ceb39b6 100644 --- a/src/sections/Home/Sponser/styles/Sponser.module.scss +++ b/src/sections/Home/Sponser/styles/Sponser.module.scss @@ -1,256 +1,168 @@ -.sponser_container { +.sponser_section { width: 100%; - height: auto; - display: flex; - justify-content: center; - align-items: center; - padding-top: 3rem; + padding: 6rem 0; + overflow: hidden; + position: relative; } -.sponser_all { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 3rem 3rem; +/* Soft edge fade for cinematic entry/exit */ +.sponser_section::before, +.sponser_section::after { + content: ""; + position: absolute; + top: 0; + width: 120px; + height: 100%; + z-index: 2; + pointer-events: none; } -.sponser_div{ - border-radius: 2.77rem; - border: 0.88px solid rgba(255, 255, 255, 0.40); - background: rgba(255, 255, 255, 0.10); - backdrop-filter: blur(6.7px); - width: 65rem; - height: 25rem; - display: flex; - align-items: center; - justify-content: flex-end; - flex-direction: column; - overflow: hidden; +.sponser_section::before { + left: 0; + background: linear-gradient(to right, #000 0%, transparent 100%); } -.sponser_card { - height: 7rem; - width: 7rem; - border-radius: 4.9rem; - border: 2px solid white; - background-color: white; - overflow: hidden; +.sponser_section::after { + right: 0; + background: linear-gradient(to left, #000 0%, transparent 100%); } -.SponserCard_image { - height: 100%; - width: 110%; - margin-left: -2px; - object-fit: cover; +.sponser_heading { + text-align: center; + margin-bottom: 4rem; +} + +.kicker { + color: var(--primary); + font-size: 0.85rem; + letter-spacing: 2px; + text-transform: uppercase; + margin-bottom: 1rem; } .sponser_title { - text-align: center; - font-size: 50px; - color: white; + font-size: 3rem; font-weight: 700; - /* font-family: Poppins; */ - line-height: 149.415%; - letter-spacing: 0.035rem; + color: white; text-transform: uppercase; - margin-top: 2rem; - margin-bottom: 10px; - margin-right: 10px; -} -.sponser_title2 { - background: var(--primary); - background-clip: text; - color: transparent; + span { + background: var(--primary); + background-clip: text; + color: transparent; + } } .bottom_line { - width: 14rem; - margin-left: 25rem; - height: 0.3125rem; + width: 6rem; + height: 4px; background: var(--primary); - color: transparent; + margin: 1.5rem auto 0 auto; border-radius: 5px; } -.customCarouselCard { - border: none; - width: 65rem; - height: 25rem; - box-shadow: none; - display: flex; - justify-content: center; - align-items: center; -} +/* ===== Marquee Structure ===== */ -/* Media Queries */ - -@media (max-width: 1024px) { - - .sponser_div{ - height: 20rem; - width: 55rem; - } - .sponser_all{ - width: 50rem; - margin-top: 5rem; - gap: 2rem; - } - - - .sponser_card { - height: 5rem; - width: 5rem; - } - - .sponser_title { - font-size: 60.8px; - } - - .bottom_line { - margin-left: 10%; - } +.marquee { + width: 100%; + overflow: hidden; + position: relative; + margin-bottom: 3rem; } -@media (max-width: 768px) { - .sponser_div{ - height: 20rem; - width: 45rem; - } - .sponser_all{ - width: 45rem; - margin-top: 5rem; - } - - .sponser_container - { - padding-top: 1rem; - align-items: center; - } - // .sponser_all { - // width: 90%; - // padding: 3rem 0; - // gap: 1.5rem; - // // z-index: 1; - // } - - .sponser_card { - height: 5rem; - width: 5rem; - border-radius: 4.9rem; - border: 2px solid white; - background-color: white; - } - - .sponser_title { - font-size: 45px; - // margin-top: -15rem; - } - - .bottom_line { - margin-left: 35%; - width: 8rem; - } +.second_row { + margin-top: -1.5rem; } -@media (max-width: 480px) { - .sponser_div{ - height: 20rem; - width: 22rem; - } - .sponser_all{ - width: 22rem; - margin-left: -0.5rem; - } - .sponser_title { - font-size: 30px; - margin-top: -18rem; - margin-right: 0; - } - - - .bottom_line{ - margin-right: 7rem; - } - - .sponser_container{ - padding-top: 3rem; - align-items: center; - } +.marquee_track { + display: flex; + align-items: center; + gap: 4rem; + width: max-content; +} +/* Directions */ +.forward { + animation: scrollLeft 40s linear infinite; } -@media (max-width: 380px) { - .sponser_container{ - width: 93%; - margin-left: 0.8rem; - } +.reverse { + animation: scrollRight 45s linear infinite; } -@media (min-width: 481px) and (max-width: 730px) { - .sponser_div { - height: 24rem; - width: 30rem; +@keyframes scrollLeft { + from { + transform: translateX(0); } - - .sponser_all { - width: 30rem; - margin-top: 3rem; + to { + transform: translateX(-50%); } +} - .sponser_card { - height: 4.5rem; - width: 4.5rem; +@keyframes scrollRight { + from { + transform: translateX(-50%); } - - .sponser_title { - font-size: 35px; - margin-top: -15rem; + to { + transform: translateX(0); } +} - .bottom_line { - width: 6rem; - margin-left: 10rem; - } +/* ===== Cards ===== */ - .sponser_container { - padding-top: 2rem; - } +.sponser_card { + height: 6.5rem; + width: 6.5rem; + min-width: 6.5rem; + border-radius: 50%; + background: white; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; } +.SponserCard_image { + width: 80%; + height: 80%; + object-fit: contain; + transition: opacity 0.4s ease; +} +.SponserCard_blurhash { + position: absolute; +} -@media (min-width: 769px) and (max-width: 890px) { - +/* ===== Responsive ===== */ - .sponser_div { - height: 22rem; - width: 48rem; +@media (max-width: 768px) { + .sponser_title { + font-size: 2.2rem; } - .sponser_all { - width: 50rem; - margin-top: 4rem; + .marquee_track { + gap: 2.5rem; } .sponser_card { - height: 5.5rem; - width: 5.5rem; + height: 5rem; + width: 5rem; + min-width: 5rem; } +} +@media (max-width: 480px) { .sponser_title { - padding-top: 24%; - font-size: 48px; - margin-top: -12rem; + font-size: 1.7rem; } - .bottom_line { - width: 10rem; - margin-left: 12rem; + .marquee_track { + gap: 2rem; } - .sponser_container { - padding-top: 2.5rem; + .sponser_card { + height: 4.3rem; + width: 4.3rem; + min-width: 4.3rem; } -} +} \ No newline at end of file diff --git a/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx b/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx index 3f36f08f..1227f2fd 100644 --- a/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx +++ b/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx @@ -1,65 +1,102 @@ -import { useState, useEffect, useContext } from "react"; -import { useParams } from "react-router-dom"; -import Input from "../../../../../components/Core/Input"; +import { useState, useEffect, useContext, useMemo, useRef } from "react"; +import { useParams, Link } from "react-router-dom"; import { Button } from "../../../../../components"; import { api } from "../../../../../services"; -import { - accessOrCreateEventByFormId, - // getCertificatePreview, - generatedAndSendCertificate, -} from "./tools/certificateTools"; +import { accessOrCreateEventByFormId } from "./tools/certificateTools"; import { Alert, MicroLoading } from "../../../../../microInteraction"; -import { Link } from "react-router-dom"; import AuthContext from "../../../../../context/AuthContext"; +import styles from "./styles/CertificatesForm.module.scss"; + +const REQUIRED_FIELDS = [ + { + fieldName: "name", + x: 50, + y: 60, + fontSize: 42, + fontColor: "#FFFFFF", + locked: true, + }, + { + fieldName: "qr", + x: 86, + y: 82, + fontSize: 24, + fontColor: "#FFFFFF", + locked: true, + }, +]; + +const createEmptyField = (count) => ({ + fieldName: `field_${count + 1}`, + x: 50, + y: 50, + fontSize: 32, + fontColor: "#FFFFFF", + locked: false, +}); + +const clamp = (value, min, max) => Math.min(Math.max(value, min), max); + +const ensureRequiredFields = (fields) => { + const byName = new Map(fields.map((field) => [field.fieldName, field])); + + const required = REQUIRED_FIELDS.map((requiredField) => { + const existing = byName.get(requiredField.fieldName); + return existing + ? { + ...existing, + fieldName: requiredField.fieldName, + locked: true, + } + : { ...requiredField }; + }); + + const custom = fields.filter( + (field) => field.fieldName !== "name" && field.fieldName !== "qr" + ); + + return [...required, ...custom]; +}; + +const toMongoFieldPayload = (fields) => + ensureRequiredFields(fields).map(({ fieldName, x, y, fontSize, fontColor }) => ({ + fieldName, + x: Number(clamp(Number(x) || 0, 0, 100).toFixed(2)), + y: Number(clamp(Number(y) || 0, 0, 100).toFixed(2)), + fontSize: Number(fontSize) || 32, + fontColor: fontColor || "#FFFFFF", + })); + +const getPointerPoint = (event) => { + if (event.touches && event.touches[0]) { + return { x: event.touches[0].clientX, y: event.touches[0].clientY }; + } + + if (event.changedTouches && event.changedTouches[0]) { + return { + x: event.changedTouches[0].clientX, + y: event.changedTouches[0].clientY, + }; + } + + return { x: event.clientX, y: event.clientY }; +}; const CertificatesForm = () => { const authCtx = useContext(AuthContext); const { eventId } = useParams(); + const [certificate, setCertificate] = useState(null); const [certificateFile, setCertificateFile] = useState(null); - const [fields, setFields] = useState([]); - const [loading, setLoading] = useState(false); + const [fields, setFields] = useState(() => ensureRequiredFields([])); const [previewLoading, setPreviewLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false); const [alert, setAlert] = useState(null); const [responseImg, setResponseImg] = useState(""); + const [draggingIndex, setDraggingIndex] = useState(null); + + const stageRef = useRef(null); const SendCertificatePath = "/profile/events/SendCertificate"; - // const test = async () => { - // let formId = await accessOrCreateEventByFormId(eventId); - - // console.log(formId); - - // //APNA EMAIL DAAL KE TEST KR LENA - - // console.log(formId); - - // const attendees = [ - // { - // fieldValues: { - // name: `Prakash Bhaia21 ${Date.now()}`, - // email: "23051625@kiit.ac.in", - // }, - // certificateId: formId.certificates[formId.certificates.length - 1].id, - // }, - // { - // fieldValues: { - // name: `Prakash Bhaia22 ${Date.now()}`, - // email: "shreyashks02@gmail.com", - // }, - // certificateId: formId.certificates[formId.certificates.length - 1].id, - // }, - // ]; - - // console.log; - // await generatedAndSendCertificate({ - // eventId: formId.id, - // attendees, - // }); - // // attendees - // // (); - // }; - - //test(); useEffect(() => { if (alert) { @@ -68,108 +105,144 @@ const CertificatesForm = () => { } }, [alert]); - const handleCertificateChange = (e) => { - const file = e.target.files[0]; - if (file) { - if (!file.type.startsWith("image/")) { - setAlert({ - type: "error", - message: "Please upload a valid image file", - position: "top-right", - duration: 3000, - }); - return; - } - const reader = new FileReader(); - reader.onload = () => { - setCertificate(reader.result); - setAlert({ - type: "success", - message: "Certificate image uploaded successfully", - position: "top-right", - duration: 2000, - }); - }; - setCertificateFile(file); - reader.readAsDataURL(file); + const previewImage = useMemo( + () => responseImg || certificate || "", + [responseImg, certificate] + ); + + const canRunActions = useMemo(() => Boolean(certificateFile), [certificateFile]); + + const handleCertificateChange = (event) => { + const file = event.target.files?.[0]; + + if (!file) { + return; + } + + if (!file.type.startsWith("image/")) { + setAlert({ type: "error", message: "Please upload a valid image file" }); + return; } + + const reader = new FileReader(); + reader.onload = () => { + setCertificate(reader.result); + setResponseImg(""); + setAlert({ type: "success", message: "Template uploaded" }); + }; + + setCertificateFile(file); + reader.readAsDataURL(file); }; const handleFieldChange = (index, key, value) => { - const updatedFields = [...fields]; - updatedFields[index] = { ...updatedFields[index], [key]: value }; - setFields(updatedFields); + setFields((previous) => { + const updated = [...previous]; + updated[index] = { ...updated[index], [key]: value }; + return ensureRequiredFields(updated); + }); }; const addField = () => { - setFields([ - ...fields, - { - fieldName: "", - x: 0, - y: 0, - fontSize: 16, - fontColor: "#000000", - minimized: false, - }, - ]); - setAlert({ - type: "info", - message: "New field added", - position: "top-right", - duration: 2000, + setFields((previous) => { + const customCount = previous.filter((field) => !field.locked).length; + return [...previous, createEmptyField(customCount)]; }); }; const removeField = (index) => { - setFields(fields.filter((_, i) => i !== index)); - setAlert({ - type: "info", - message: "Field removed", - position: "top-right", - duration: 2000, + setFields((previous) => { + if (previous[index]?.locked) { + setAlert({ + type: "warning", + message: "Default fields name and qr are required", + }); + return previous; + } + + return previous.filter((_, itemIndex) => itemIndex !== index); }); }; + const updateFieldPositionFromPoint = (index, point) => { + const stage = stageRef.current; + + if (!stage) { + return; + } + + const rect = stage.getBoundingClientRect(); + if (!rect.width || !rect.height) { + return; + } + + const x = ((point.x - rect.left) / rect.width) * 100; + const y = ((point.y - rect.top) / rect.height) * 100; + + handleFieldChange(index, "x", Number(clamp(x, 0, 100).toFixed(2))); + handleFieldChange(index, "y", Number(clamp(y, 0, 100).toFixed(2))); + }; + + const startDragging = (index, event) => { + if (!previewImage) { + setAlert({ type: "warning", message: "Upload a template before dragging" }); + return; + } + + event.preventDefault(); + setDraggingIndex(index); + updateFieldPositionFromPoint(index, getPointerPoint(event)); + }; + + useEffect(() => { + if (draggingIndex === null) { + return undefined; + } + + const onMove = (event) => { + if (event.touches) { + event.preventDefault(); + } + + updateFieldPositionFromPoint(draggingIndex, getPointerPoint(event)); + }; + + const onUp = () => setDraggingIndex(null); + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + window.addEventListener("touchmove", onMove, { passive: false }); + window.addEventListener("touchend", onUp); + + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + window.removeEventListener("touchmove", onMove); + window.removeEventListener("touchend", onUp); + }; + }, [draggingIndex]); + const handleRefresh = async () => { if (!certificateFile) { - setAlert({ - type: "warning", - message: "Please upload a certificate image first", - position: "top-right", - duration: 3000, - }); + setAlert({ type: "warning", message: "Upload template first" }); return; } + setPreviewLoading(true); + try { const formData = new FormData(); formData.append("image", certificateFile); - formData.append("fields", JSON.stringify(fields)); - const response = await api.post( - "/api/certificate/dummyCertificate", - formData, - { - headers: { Authorization: `Bearer ${authCtx.token}` }, - } - ); - if (response.status !== 200) { - throw new Error(`API error: ${response.statusText}`); - } - setResponseImg(response.data.imageSrc); - setAlert({ - type: "success", - message: "Preview updated successfully", - position: "top-right", - duration: 2000, + formData.append("fields", JSON.stringify(toMongoFieldPayload(fields))); + + const response = await api.post("/api/certificate/dummyCertificate", formData, { + headers: { Authorization: `Bearer ${authCtx.token}` }, }); + + setResponseImg(response.data.imageSrc); + setAlert({ type: "success", message: "Preview updated" }); } catch (error) { - setAlert({ - type: "error", - message: "Error updating preview. Please try again", - position: "top-right", - duration: 3000, - }); + setAlert({ type: "error", message: "Preview refresh failed" }); } finally { setPreviewLoading(false); } @@ -177,245 +250,199 @@ const CertificatesForm = () => { const handleSave = async () => { if (!certificateFile) { - setAlert({ - type: "warning", - message: "Please upload a certificate image first", - position: "top-right", - duration: 3000, - }); + setAlert({ type: "warning", message: "Upload template first" }); return; } setSaveLoading(true); + try { - const eventData = await accessOrCreateEventByFormId( - eventId, - authCtx.token - ); - if (!eventData || !eventData.id) { - throw new Error("Failed to retrieve or create event."); + const eventData = await accessOrCreateEventByFormId(eventId, authCtx.token); + if (!eventData?.id) { + throw new Error("Event not found"); } const formData = new FormData(); formData.append("image", certificateFile); formData.append("eventId", eventData.id); - formData.append("fields", JSON.stringify(fields)); - - const response = await api.post( - "/api/certificate/addCertificateTemplate", - - formData, - { - headers: { Authorization: `Bearer ${authCtx.token}` }, - } - ); + formData.append("fields", JSON.stringify(toMongoFieldPayload(fields))); - if (response.status !== 200) { - throw new Error(`API error: ${response.statusText}`); - } - - setAlert({ - type: "success", - message: "Certificate template saved successfully", - position: "top-right", - duration: 3000, + await api.post("/api/certificate/addCertificateTemplate", formData, { + headers: { Authorization: `Bearer ${authCtx.token}` }, }); + + setAlert({ type: "success", message: "Template saved successfully" }); } catch (error) { - console.error("Error saving certificate template:", error); - setAlert({ - type: "error", - message: "Error saving certificate template. Please try again", - position: "top-right", - duration: 3000, - }); + setAlert({ type: "error", message: "Save failed" }); } finally { setSaveLoading(false); } }; return ( -
-

- Create Certificate -

-

for Event: {eventId}

-
-
- {loading && ( -
- -
- )} +
+
+
+

+ Certificate Designer +

+

+ Drag fields on preview. Default Mongo fields `name` and `qr` are always included. +

+
+ +
+
+ ID: + {eventId} +
+
+ FIELDS: + {fields.length} active +
+
+ +
+
+
+

Live Preview

+ {previewImage ? "Ready" : "Empty"} +
+ +
+ {previewImage ? ( +
+ Preview -
- - - -
+
+ {fields.map((field, index) => ( + + ))} +
+
+ ) : ( +
+

Upload a template to start positioning fields.

+
+ )} + + {(previewLoading || saveLoading) && ( +
+ +
+ )} +
+ +

Tip: drag each label directly on the certificate preview to set position.

+
+ +
+
+

Field Mapping

+
+ + + +
+
+ +
{fields.map((field, index) => ( -
-
- {field.fieldName || `Field ${index + 1}`} -
- {!field.minimized && ( - <> - - handleFieldChange(index, "fieldName", e.target.value) - } - /> - - handleFieldChange(index, "x", Number(e.target.value)) - } - /> - - handleFieldChange(index, "y", Number(e.target.value)) - } - /> - +
+ Font Size + - handleFieldChange( - index, - "fontSize", - Number(e.target.value) - ) + min={8} + onChange={(event) => + handleFieldChange(index, "fontSize", Number(event.target.value)) } /> - + +
+ Font Color + - handleFieldChange(index, "fontColor", e.target.value) + onChange={(event) => + handleFieldChange(index, "fontColor", event.target.value) } /> - - )} - -
+
+
+ ))}
-
+
+ + - +
-
+
); diff --git a/src/sections/Profile/Admin/Form/CertificatesForm/styles/CertificatesForm.module.scss b/src/sections/Profile/Admin/Form/CertificatesForm/styles/CertificatesForm.module.scss index dc3fe743..f2fc86a1 100644 --- a/src/sections/Profile/Admin/Form/CertificatesForm/styles/CertificatesForm.module.scss +++ b/src/sections/Profile/Admin/Form/CertificatesForm/styles/CertificatesForm.module.scss @@ -1,100 +1,384 @@ -.container { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - } - - .preview { - width: 90%; - padding: 20px; - position: relative; - text-align: center; - } - - .certificateContainer { - position: relative; - display: inline-block; - width: 100%; - } - - .certificateImage { - width: 100%; - height: auto; - } - - .text { - position: absolute; - transform: translate(-50%, -50%); - font-weight: bold; - white-space: nowrap; - z-index: 1; - } - +.page { + --bg-card: #0b0b0c; + --bg-soft: #111113; + --border-color: #1f1f23; + --text-white: #ffffff; + --text-dim: #71717a; + --brand-orange: #f97316; - - .form { - width: 90%; - padding: 20px; - box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); - } - - .heading { - font-size: 24px; - margin-bottom: 20px; - text-align: center; - } - - .table { - width: 100%; - margin-bottom: 20px; + background: transparent; + color: var(--text-white); + font-family: "Poppins", sans-serif; + padding: 32px; + min-height: 100vh; +} + +/* ---------------- HERO ---------------- */ + +.hero { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 40px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border-color); + gap: 24px; +} + +.title { + font-size: 2rem; + font-weight: 800; + margin: 0; + line-height: 1.2; +} + +.title span { + color: var(--brand-orange); +} + +.subtitle { + color: var(--text-dim); + font-size: 0.9rem; + margin-top: 6px; +} + +.metaPanel { + background: var(--bg-card); + border: 1px solid var(--border-color); + padding: 16px 20px; + border-radius: 12px; + display: flex; + flex-direction: column; + gap: 12px; + min-width: 280px; +} + +.metaRow { + display: flex; + justify-content: space-between; + align-items: center; +} + +.metaLabel { + font-size: 0.7rem; + font-weight: 700; + color: var(--brand-orange); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.eventId, +.counter { + font-size: 0.82rem; + font-weight: 600; + color: var(--text-white); + word-break: break-all; +} + +/* ---------------- LAYOUT ---------------- */ + +.layout { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 32px; + align-items: start; +} + +.previewSection, +.editorSection { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 16px; + padding: 24px; +} + +/* ---------------- SECTION HEADER ---------------- */ + +.sectionHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.sectionHeader h2 { + font-size: 1rem; + font-weight: 600; + margin: 0; +} + +.statusBadge { + background: rgba(249, 115, 22, 0.15); + border: 1px solid rgba(249, 115, 22, 0.45); + color: #fdba74; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.08em; + border-radius: 999px; + padding: 6px 12px; +} + +/* ---------------- PREVIEW ---------------- */ + +.previewCanvas { + width: 100%; + min-height: 460px; + background: #000; + border-radius: 12px; + border: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.previewStage { + position: relative; + display: inline-block; + max-width: 100%; + max-height: 560px; +} + +.previewImage { + display: block; + max-width: 100%; + max-height: 560px; + border-radius: 6px; + user-select: none; +} + +.markerLayer { + position: absolute; + inset: 0; +} + +.dragMarker { + position: absolute; + transform: translate(-50%, -50%); + background: rgba(249, 115, 22, 0.95); + color: #111827; + border-radius: 999px; + padding: 6px 12px; + font-size: 0.75rem; + font-weight: 700; + cursor: grab; + white-space: nowrap; +} + +.dragMarker:active { + cursor: grabbing; +} + +.lockedMarker { + background: rgba(56, 189, 248, 0.95); + color: #082f49; +} + +.previewEmpty { + color: var(--text-dim); + text-align: center; +} + +.loadingOverlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; +} + +.dragHint { + margin-top: 14px; + color: #a1a1aa; + font-size: 0.82rem; +} + +/* ---------------- BUTTONS ---------------- */ + +.editorActions { + display: flex; + gap: 12px; +} + +.uploadBtn, +.addBtn, +.nextBtn { + height: 40px; + padding: 0 18px; + border-radius: 8px; + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease, opacity 0.2s ease; +} + +.uploadBtn { + background: var(--bg-soft); + border: 1px solid var(--border-color); + color: #fff; +} + +.uploadBtn:hover { + background: #1a1a1d; +} + +.addBtn { + background: var(--brand-orange); + border: none; + color: #000; +} + +.addBtn:hover { + opacity: 0.9; +} + +.nextBtn { + background: #fff; + color: #000; + border: none; + width: 100%; +} + +/* ---------------- FIELD LIST ---------------- */ + +.fieldList { + max-height: 520px; + overflow-y: auto; + padding-right: 6px; +} + +.fieldCard { + background: var(--bg-soft); + border: 1px solid var(--border-color); + padding: 18px; + border-radius: 12px; + margin-bottom: 18px; +} + +.fieldHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + gap: 12px; +} + +.fieldTitleInput { + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color); + color: var(--brand-orange); + font-weight: 600; + font-size: 0.92rem; + width: 100%; + padding-bottom: 6px; +} + +.fieldTitleInput:focus { + outline: none; + border-color: var(--brand-orange); +} + +.fieldTitleInput:disabled { + color: #38bdf8; +} + +.fieldGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 6px; +} + +.inputGroup span { + font-size: 0.65rem; + color: var(--text-dim); + text-transform: uppercase; +} + +.inputGroup input { + background: #141416; + border: 1px solid var(--border-color); + color: #fff; + padding: 8px 10px; + border-radius: 6px; + font-size: 0.82rem; +} + +.delBtn { + background: #991b1b; + color: #fff; + border: none; + width: 30px; + height: 30px; + border-radius: 6px; + font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; +} + +.delBtn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* ---------------- FOOTER ---------------- */ + +.footer { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 14px; +} + +/* ---------------- RESPONSIVE ---------------- */ + +@media (max-width: 1024px) { + .layout { + grid-template-columns: 1fr; } - - .td { - padding: 10px; - font-size: 16px; + + .hero { + flex-direction: column; + align-items: flex-start; } - - .input { + + .metaPanel { width: 100%; - padding: 8px; - font-size: 14px; - border: 1px solid #ddd; - border-radius: 4px; } - - .imgURLInputContainer { - margin-top: 20px; +} + +@media (max-width: 600px) { + .footer { + grid-template-columns: 1fr; } - - .label { - display: block; - margin-bottom: 5px; - font-size: 14px; - font-weight: bold; + + .fieldGrid { + grid-template-columns: 1fr; } - .buttonGroup { - display: flex; - gap: 10px; - margin-top: 20px; - justify-content: center; + .previewCanvas { + min-height: 340px; } - - .saveButton, - .downloadButton { - padding: 10px 20px; - font-size: 16px; - background-color: #007bff; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; + + .editorActions { + flex-direction: column; } - - .saveButton:hover, - .downloadButton:hover { - background-color: #0056b3; + + .uploadBtn, + .addBtn, + .nextBtn { + width: 100%; } - - \ No newline at end of file +} \ No newline at end of file diff --git a/src/sections/Profile/Admin/Form/NewForm/NewForm.jsx b/src/sections/Profile/Admin/Form/NewForm/NewForm.jsx index 777ceb8d..a6f73f65 100644 --- a/src/sections/Profile/Admin/Form/NewForm/NewForm.jsx +++ b/src/sections/Profile/Admin/Form/NewForm/NewForm.jsx @@ -1072,70 +1072,63 @@ function NewForm() {
)} -
-
-
- setdata({ ...data, eventTitle: e.target.value })} - /> - setdata({ ...data, eventImg: e.target.value })} - className={styles.formInput} - /> - setdata({ ...data, eventDate: date })} - /> - onChangeEventType(value)} - /> +
+
+
+
+ setdata({ ...data, eventTitle: e.target.value })} + /> +
+ +
+ setdata({ ...data, eventImg: e.target.value })} + className={styles.formInput} + /> +
+ +
+ setdata({ ...data, eventDate: date })} + /> +
+ +
+ onChangeEventType(value)} + /> +
+ {data.eventType === "Paid" && ( -
+
-
)} - - setdata({ ...data, relatedEvent: e.target.value }) - } - /> - { - handleChangeParticipantType(value); - }} - /> + +
+ + setdata({ ...data, relatedEvent: e.target.value }) + } + /> +
+ +
+ { + handleChangeParticipantType(value); + }} + /> +
+ {data.participationType === "Team" && ( -
- item.value <= (data.maxTeamSize || 7) - )} - className={styles.formInput} - value={data.minTeamSize} - onChange={(value) => setdata({ ...data, minTeamSize: value })} - /> - {data.minTeamSize && ( +
+
item.value >= (data.minTeamSize || 1) + (item) => item.value <= (data.maxTeamSize || 7) )} className={styles.formInput} - value={data.maxTeamSize} - onChange={(value) => - setdata({ ...data, maxTeamSize: value }) - } + value={data.minTeamSize} + onChange={(value) => setdata({ ...data, minTeamSize: value })} /> +
+ {data.minTeamSize && ( +
+ item.value >= (data.minTeamSize || 1) + )} + className={styles.formInput} + value={data.maxTeamSize} + onChange={(value) => + setdata({ ...data, maxTeamSize: value }) + } + /> +
)}
)} - - setdata({ ...data, eventPriority: e.target.value }) - } - /> - { - setdata({ - ...data, - regDateAndTime: moment(date).format( - "MMMM Do YYYY, h:mm:ss a" - ), - }); - }} - /> + +
+ + setdata({ ...data, eventPriority: e.target.value }) + } + /> +
+ +
+ { + setdata({ + ...data, + regDateAndTime: moment(date).format( + "MMMM Do YYYY, h:mm:ss a" + ), + }); + }} + /> +
-
- - setdata({ ...data, eventdescription: e.target.value }) - } - /> - - setdata({ ...data, successMessage: e.target.value }) - } - /> - - setdata({ ...data, eventMaxReg: e.target.value }) - } - /> + +
+
+ + setdata({ ...data, eventdescription: e.target.value }) + } + /> +
+ +
+ + setdata({ ...data, successMessage: e.target.value }) + } + /> +
+ +
+ + setdata({ ...data, eventMaxReg: e.target.value }) + } + /> +
{sections && sections !== undefined ? ( @@ -1373,4 +1384,4 @@ function NewForm() { ); } -export default NewForm; +export default NewForm; \ No newline at end of file diff --git a/src/sections/Profile/Admin/Form/NewForm/styles/NewForm.module.scss b/src/sections/Profile/Admin/Form/NewForm/styles/NewForm.module.scss index 32ed6302..146e2f55 100644 --- a/src/sections/Profile/Admin/Form/NewForm/styles/NewForm.module.scss +++ b/src/sections/Profile/Admin/Form/NewForm/styles/NewForm.module.scss @@ -22,14 +22,80 @@ color: transparent; } - .formInput { - width: 90% !important; +.formInput { + width: 100% !important; margin-bottom: 1.4em; } .formInputTxtArea { - width: 90% !important; - height: 31vh !important; + width: 100% !important; + min-height: 160px !important; + height: 200px !important; + resize: vertical; + } + +.formBody { + height: 90vh; + width: 90%; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + margin-bottom: 50px; +} + +.detailsGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 2rem; + align-items: start; +} + +.detailsColumn { + min-width: 0; +} + +.detailsRight { + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +.detailsRight .formInput { + margin-bottom: 0; +} + +@media (max-width: 900px) { + .formHeader { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + width: 100%; } + + .formBody { + width: 100%; + height: auto; + margin-bottom: 30px; + } + + .detailsGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .buttonContainer { + font-size: 24px; + line-height: 24px; + } + + .detailsGrid { + gap: 1.25rem; + } + + .formFieldContainer { + width: 100%; + } +} .formFieldContainer { display: flex; @@ -65,4 +131,4 @@ flex-direction: column; width: 30%; } - \ No newline at end of file + diff --git a/src/sections/Profile/Admin/View/ViewEvent/VIewEvent.jsx b/src/sections/Profile/Admin/View/ViewEvent/VIewEvent.jsx index a142ab48..ebe24f36 100644 --- a/src/sections/Profile/Admin/View/ViewEvent/VIewEvent.jsx +++ b/src/sections/Profile/Admin/View/ViewEvent/VIewEvent.jsx @@ -77,8 +77,11 @@ function ViewEvent({ handleChangePage }) { }, }; - const handleDeleteEvent = async () => { - const id = authCtx.eventData.id; + const handleDeleteEvent = async (id) => { + if (!id) { + console.error("Missing event id for delete."); + return; + } try { const response = await api.delete(`/api/form/deleteForm/${id}`, { headers: { diff --git a/src/sections/Profile/Admin/View/ViewEvent/styles/ViewEvent.module.scss b/src/sections/Profile/Admin/View/ViewEvent/styles/ViewEvent.module.scss index abfc0e09..753d15e9 100644 --- a/src/sections/Profile/Admin/View/ViewEvent/styles/ViewEvent.module.scss +++ b/src/sections/Profile/Admin/View/ViewEvent/styles/ViewEvent.module.scss @@ -1,15 +1,21 @@ .container { background-color: transparent; color: #fff; - padding: 20px; + padding: 2rem 3rem; border-radius: 8px; + width: 100%; + max-width: 1400px; + margin: 0 auto; + box-sizing: border-box; } +/* ================= HEADER ================= */ + .buttonContainer { - font-size: 30px; + font-size: 2rem; letter-spacing: 0.02em; - line-height: 40px; - padding-bottom: 50px; + line-height: 2.5rem; + padding-bottom: 2.5rem; width: 100%; display: flex; align-items: center; @@ -17,41 +23,40 @@ } .headInnerText { - margin-left: 3.5rem; - margin-top: 12px; + margin-left: 1rem; + margin-top: 0.5rem; } .headInnerText span { - margin-right: 3px; + margin-right: 4px; background: linear-gradient(to right, #d03e21 40%, #FF8A00); -webkit-background-clip: text; color: transparent; } +/* ================= FORM ================= */ + .form { display: flex; flex-direction: column; + gap: 1.5rem; width: 100%; } -.eventListContainer { - display: flex; - flex-direction: column; - width: 100%; -} +/* ================= TABS ================= */ .tabContainer { display: flex; justify-content: center; - margin-bottom: 2rem; + gap: 3rem; + margin-bottom: 2.5rem; + flex-wrap: wrap; } .tabHeading { cursor: pointer; - margin: 0 4rem; - margin-top: -1rem; - padding: 0.5rem; - font-size: 1.4rem; + padding: 0.5rem 0; + font-size: 1.2rem; user-select: none; transition: color 0.3s ease, border-bottom 0.3s ease; } @@ -62,87 +67,115 @@ .activeTab { border-bottom: 2px solid #dc5e14; - font-weight: bold; + font-weight: 600; +} + +/* ================= EVENT SECTION ================= */ + +.eventListContainer { + display: flex; + flex-direction: column; + width: 100%; } .eventSectionContainer { display: flex; flex-direction: column; + gap: 2rem; } .eventSection { display: flex; flex-direction: column; - margin: 0 4rem 2rem 4rem; + margin: 0 2rem 2rem 2rem; } +/* ================= GRID FIX ================= */ + .eventGrid { display: grid; width: 100%; - grid-template-columns: repeat(auto-fit, minmax(23rem, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 2rem; + align-items: stretch; } .eventCardContainer { - margin-bottom: 1rem; + width: 100%; + max-width: none; /* removed restriction causing 1 per line */ position: relative; - max-width: 24rem; } +/* ================= SCROLLBAR ================= */ + .eventList::-webkit-scrollbar { - width: 10px; - height: 0px; - transition: .2s linear; + width: 8px; background-color: #292929; } .eventList::-webkit-scrollbar-thumb { - border-radius: 22px; + border-radius: 20px; background: var(--primary); } .eventList::-webkit-scrollbar-track { - padding: 0px 20px; background-color: transparent; } +/* ================= RESPONSIVE ================= */ + +@media (max-width: 1024px) { + .container { + padding: 2rem; + } + + .eventSection { + margin: 0 1rem 2rem 1rem; + } +} + @media (max-width: 768px) { .buttonContainer { display: none; } -} - -@media (max-width: 768px) { .topSection { flex-direction: column; + gap: 1rem; } - .topSectionInputContainer:nth-child(1) { - margin-right: 0; - } - .EventInput { - width: 100%; - } - .EventInputArea { - width: 100%; - } + + .EventInput, + .EventInputArea, .posterPreview { width: 100%; } - .buttonContainer { - display: none; + + .eventGrid { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } } @media (max-width: 540px) { + .container { + padding: 1.5rem 1rem; + } + .eventSection { - margin: 0rem; + margin: 0 0 2rem 0; + } + + .tabContainer { + gap: 1.5rem; } } @media (max-width: 390px) { .container { - padding-left: 0rem; - padding-right: 0rem; + padding-left: 0.8rem; + padding-right: 0.8rem; } -} + + .eventGrid { + grid-template-columns: 1fr; /* single column only for very small screens */ + } +} \ No newline at end of file diff --git a/src/sections/Profile/Admin/View/ViewMember/ViewMember.jsx b/src/sections/Profile/Admin/View/ViewMember/ViewMember.jsx index 7816f2a6..26f41d80 100644 --- a/src/sections/Profile/Admin/View/ViewMember/ViewMember.jsx +++ b/src/sections/Profile/Admin/View/ViewMember/ViewMember.jsx @@ -1,5 +1,4 @@ -import React, { useState, useEffect, useContext } from "react"; -import axios from "axios"; +import React, { useState, useEffect, useContext, useRef } from "react"; import styles from "./styles/ViewMember.module.scss"; import { Button, TeamCard } from "../../../../../components"; import AddMemberForm from "../../Form/MemberForm/AddMemberForm"; @@ -13,13 +12,15 @@ function ViewMember() { const [memberActivePage, setMemberActivePage] = useState("Board"); const [members, setMembers] = useState([]); const [access, setAccess] = useState([]); - const [selectedMember, setSelectedMember] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [loading, setLoading] = useState(false); - const authCtx = useContext(AuthContext); + const [dropdownOpen, setDropdownOpen] = useState(false); const [enablingUpdate, setEnable] = useState(false); const [alert, setAlert] = useState(null); + const authCtx = useContext(AuthContext); + const dropdownRef = useRef(null); + useEffect(() => { if (alert) { const { type, message, position, duration } = alert; @@ -27,16 +28,24 @@ function ViewMember() { } }, [alert]); + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setDropdownOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + useEffect(() => { const fetchMemberData = async () => { try { setLoading(true); const response = await api.get("/api/user/fetchTeam"); - const fetchedMembers = response.data.data; - setMembers(fetchedMembers); + setMembers(response.data.data); } catch (error) { - console.error("Error fetching member data:", error); - setMembers(localTeamMembers); // Fallback to test data + setMembers(localTeamMembers); } finally { setLoading(false); } @@ -44,58 +53,29 @@ function ViewMember() { const fetchAccessTypes = async () => { try { - const response = await api.get("/api/user/fetchAccessTypes"); - const fetchedAccess = response.data.data; - const testAccess = [ - "ADMIN", - "USER", - - "PRESIDENT", - "VICEPRESIDENT", - "DIRECTOR_TECHNICAL", - "DIRECTOR_CREATIVE", - "DIRECTOR_MARKETING", - "DIRECTOR_OPERATIONS", - "DIRECTOR_PR_AND_FINANCE", - "DIRECTOR_HUMAN_RESOURCE", - "DEPUTY_DIRECTOR_TECHNICAL", - "DEPUTY_DIRECTOR_CREATIVE", - "DEPUTY_DIRECTOR_MARKETING", - "DEPUTY_DIRECTOR_OPERATIONS", - "DEPUTY_DIRECTOR_PR_AND_FINANCE", - "DEPUTY_DIRECTOR_HUMAN_RESOURCE", + "BOARD", "TECHNICAL", "CREATIVE", "MARKETING", "OPERATIONS", "PR_AND_FINANCE", "HUMAN_RESOURCE", - "ALUMNI", "EX_MEMBER", + "ADD_MEMBER", ]; - const filteredAccess = testAccess.filter( - (accessType) => - !["ADMIN", "USER", "PRESIDENT", "VICEPRESIDENT"].includes( - accessType - ) && - !accessType.startsWith("DIRECTOR_") && - !accessType.startsWith("DEPUTY_") - ).map((accessType) => { - return accessType - .split("_") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(" "); - }); + const formatted = testAccess.map((item) => + item + .replace(/_/g, " ") + .toLowerCase() + .replace(/\b\w/g, (l) => l.toUpperCase()) + ); - // Adding "Directors" and "Add Member" to the filteredAccess array - filteredAccess.push("Board", "Add Member"); - setAccess(filteredAccess); + setAccess(formatted); } catch (error) { - console.error("Error fetching Access Types:", error); - setAccess(AccessTypes.data); // Fallback to test data + setAccess(AccessTypes.data); } }; @@ -103,179 +83,103 @@ function ViewMember() { fetchMemberData(); }, []); - const handleButtonClick = (menu) => { - if (menu === "add member" && enablingUpdate) { - authCtx.memberData = null; - authCtx.croppedImageFile = null; - setEnable(false); - setMemberActivePage(""); - setTimeout(() => { - setMemberActivePage("add member"); - }, 0); - } else { - setMemberActivePage(menu); - } + const handleSelect = (value) => { + setMemberActivePage(value); + setDropdownOpen(false); }; - const headerMenu = access.map((accessType) => accessType.toLowerCase()); - - const renderButtons = () => - headerMenu.map((menu) => ( - - )); - const getMembersByPage = () => { - if (memberActivePage.toLowerCase() === "add member" && !enablingUpdate) { - authCtx.memberData = null; - } + if (memberActivePage === "Add Member") return []; - let filteredMembers = []; - if (memberActivePage.toLowerCase() === "board") { - filteredMembers = members.filter( - (member) => - member.access.startsWith("DIRECTOR_") || - member.access.startsWith("DEPUTY_") || - member.access === "PRESIDENT" || - member.access === "VICEPRESIDENT" + let filtered = []; + + if (memberActivePage === "Board") { + filtered = members.filter( + (m) => + m.access.startsWith("DIRECTOR_") || + m.access.startsWith("DEPUTY_") || + m.access === "PRESIDENT" || + m.access === "VICEPRESIDENT" ); } else { - filteredMembers = members.filter((member) => { - // Convert accessCategory to lowercase for case-insensitive comparison - const accessCategory = member.access - .replace(/_/g, " ") // Replace all underscores with spaces - .toLowerCase(); - - // Convert department name to match format (e.g., "operations" -> "operations") - const activeDepartment = memberActivePage.toLowerCase(); - - // Check for exact match (e.g., "operations" === "operations") - // OR match senior executive (e.g., "senior executive operations" ends with "operations") - const seniorExecutivePrefix = "senior executive "; - const isSeniorExecutive = accessCategory.startsWith(seniorExecutivePrefix) && - accessCategory.substring(seniorExecutivePrefix.length) === activeDepartment; - - return accessCategory === activeDepartment || isSeniorExecutive; - }); + const active = memberActivePage.toLowerCase(); + filtered = members.filter((m) => + m.access.replace(/_/g, " ").toLowerCase().includes(active) + ); } - // Apply search query filter if (searchQuery) { - filteredMembers = filteredMembers.filter((member) => - member.name.toLowerCase().includes(searchQuery.toLowerCase()) + filtered = filtered.filter((m) => + m.name.toLowerCase().includes(searchQuery.toLowerCase()) ); } - // Sort filtered members alphabetically - filteredMembers.sort((a, b) => a.name.localeCompare(b.name)); - return filteredMembers; - }; - - useEffect(() => { - setTimeout(() => { - setLoading(false); - }, 1000); - }, [searchQuery]); - - const customStyles = { - teamMember: styles.teamMemberCustom, - teamMemberBackh5: styles.teamMemberBackh5Custom, - socialLinksa: styles.socialLinksaCustom, - button: styles.buttonCustom, - knowPara: styles.knowParaCustom, - updatebtn: styles.updatebtnCustom, - teamMemberBack: styles.teamMemberBackCustom, - }; - - const handleUpdate = (member) => { - setMemberActivePage("Add Member"); - setEnable(true); - }; - - const handleRemove = async () => { - try { - const id = authCtx.memberData.id; - const response = await api.delete(`/api/user/deleteMember/${id}`, { - headers: { - Authorization: `Bearer ${window.localStorage.getItem("token")}`, - }, - }); - if (response.status === 200 || response.status === 201) { - setAlert({ - type: "success", - message: "Member deleted successfully.", - position: "bottom-right", - duration: 3000, - }); - setMembers((members) => - members.filter((m) => m.id !== response.data.user.id) - ); - } - } catch (error) { - console.error("Error removing member:", error); - } + return filtered.sort((a, b) => a.name.localeCompare(b.name)); }; const membersToDisplay = getMembersByPage(); return (
-
-
-
-

- View Member -

-
- { - setSearchQuery(e.target.value); - setLoading(true); // Show loading when searching - }} - className={styles.searchInput} - /> +
+
+

+ View Member +

+ +
+
setDropdownOpen(!dropdownOpen)} + > + {memberActivePage} + {dropdownOpen ? "▲" : "▼"}
+ + {dropdownOpen && ( +
+ {access.map((item, i) => ( +
handleSelect(item)} + > + {item} +
+ ))} +
+ )}
-
{renderButtons()}
- {loading ? ( - // Show loading component - ) : memberActivePage.toLowerCase() === "add member" ? ( - - ) : ( -
- {membersToDisplay.map((member, idx) => ( - - ))} -
- )} +
+ +
+ setSearchQuery(e.target.value)} + className={styles.searchInput} + />
+ + {loading ? ( + + ) : memberActivePage === "Add Member" ? ( + + ) : ( +
+ {membersToDisplay.map((member, idx) => ( + + ))} +
+ )} +
); } -export default ViewMember; +export default ViewMember; \ No newline at end of file diff --git a/src/sections/Profile/Admin/View/ViewMember/styles/ViewMember.module.scss b/src/sections/Profile/Admin/View/ViewMember/styles/ViewMember.module.scss index 8fb2e3af..abe0f8c0 100644 --- a/src/sections/Profile/Admin/View/ViewMember/styles/ViewMember.module.scss +++ b/src/sections/Profile/Admin/View/ViewMember/styles/ViewMember.module.scss @@ -1,137 +1,150 @@ -.right { - width: 100%; -} - .mainMember { - height: 100%; width: 100%; - position: relative; - padding: 2em 3em; - padding-right: 1em; + height: 100%; + padding: 2rem 3rem; + display: flex; + flex-direction: column; + gap: 2rem; + box-sizing: border-box; } -.searchContainer { + +.header { display: flex; justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: 1.5rem; +} + +.titleSection { + display: flex; align-items: center; + gap: 1.5rem; } -.searchInput { - width: 90%; - padding: 10px; - border-radius: 15px; - border: 1px solid #ffffff; - background-color: transparent; - color: #ffffff; +.headInnerText { + font-size: 1.8rem; + font-weight: 600; + margin: 0; + + span { + background: linear-gradient(to right, #d03e21 40%, #ff8a00); + -webkit-background-clip: text; + color: transparent; + } } +/* Dropdown */ -.buttonContainer { - font-size: 30px; - line-height: 40px; - padding-bottom: 10px; - width: 100%; +.dropdownWrapper { + position: relative; + width: 220px; +} + +.dropdownHeader { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.15); + padding: 0.7rem 1rem; + border-radius: 12px; + cursor: pointer; display: flex; - align-items: center; justify-content: space-between; + align-items: center; + transition: 0.2s ease; } -.headInnerText { - margin-left: 1.2rem; - margin-top: 12px; +.dropdownHeader:hover { + background: rgba(255, 255, 255, 0.08); } -.headInnerText span { - margin-right: 3px; - background: linear-gradient(to right, #d03e21 40%, #FF8A00); - -webkit-background-clip: text; - color: transparent; +.arrow { + font-size: 0.7rem; } -.eventmember { - height: 100%; +.dropdownMenu { + position: absolute; + top: 110%; + left: 0; width: 100%; - display: flex; + background: #111; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + max-height: 260px; + overflow-y: auto; + z-index: 10; } -.buttons { - display: flex; - justify-content: space-around; - height: auto; - flex-wrap: wrap; - margin: 0 1rem; -} - -.buttons button { - border-radius: 10px; - border: 0.3px #969393 solid; - color: #ffff; - margin-right: 0px; - height: 3rem; - width: 6rem; - font-size: 0.9rem; - margin-left: 10px; +.dropdownItem { + padding: 0.7rem 1rem; cursor: pointer; - text-align: center; + transition: 0.2s ease; } -.teamGrid { - display: grid; - grid-template-columns: repeat(4, minmax(100px, 1fr)); - - justify-content: center; - padding: 10px 20px; - padding-right: 0; - height: 360px; - overflow: hidden scroll; - scrollbar-width: none; +.dropdownItem:hover { + background: rgba(255, 255, 255, 0.07); } -@media (max-width: 1024px) { - .teamGrid { - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - padding: 0; - } +.activeItem { + background: rgba(255, 138, 0, 0.15); + color: #ff8a00; } -@media (max-width: 768px) { - .teamGrid { - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - padding: 0; - } -} +/* Search */ -@media (max-width: 480px) { - .teamGrid { - grid-template-columns: repeat(2, 1fr); - padding: 0; - } +.searchContainer { + flex: 1; + display: flex; + justify-content: flex-end; } -/* Custom styles for TeamCard component */ -.teamMemberCustom { - height: 18rem; - width: 16rem; - padding: 2rem; - padding-top: 0; - -} -.teamMemberBackh5Custom { - font-size: smaller; +.searchInput { + width: 100%; + max-width: 320px; + padding: 0.75rem 1rem; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.05); + color: #fff; + outline: none; + transition: 0.2s ease; } -.socialLinksaCustom { - font-size: 1rem; +.searchInput:focus { + border-color: #ff8a00; + background: rgba(255, 255, 255, 0.08); } -.buttonCustom { - font-size: 1rem; -} +/* Grid */ -.knowParaCustom { - height: 10rem; +.teamGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 1.5rem; + overflow-y: auto; + max-height: calc(100vh - 260px); + scrollbar-width: none; } -.updatebtnCustom{ - display: flex; - width: 70%; +.teamGrid::-webkit-scrollbar { + display: none; } + +/* Responsive */ + +@media (max-width: 768px) { + .header { + flex-direction: column; + } + + .searchContainer { + width: 100%; + } + + .dropdownWrapper { + width: 100%; + } + + .searchInput { + max-width: 100%; + } +} \ No newline at end of file diff --git a/src/sections/Profile/General/CertificatesView/CertificatesView.jsx b/src/sections/Profile/General/CertificatesView/CertificatesView.jsx index 27c706c4..468fafa2 100644 --- a/src/sections/Profile/General/CertificatesView/CertificatesView.jsx +++ b/src/sections/Profile/General/CertificatesView/CertificatesView.jsx @@ -4,19 +4,16 @@ import AuthContext from "../../../../context/AuthContext"; import { Link } from "react-router-dom"; import { api } from "../../../../services"; import { ComponentLoading } from "../../../../microInteraction"; -import { Send } from "lucide-react"; const Events = () => { const authCtx = useContext(AuthContext); const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const viewPath = "/profile/Events"; + const SendCertificatePath = "/profile/events/SendCertificate"; - const analyticsPath = "/profile/events/Analytics"; const createCertificatesPath = "/profile/events/createCertificates"; const viewCertificatesPath = "/profile/events/viewCertificates"; - const analyticsAccessRoles = [ "PRESIDENT", @@ -37,41 +34,24 @@ const Events = () => { if (response.status === 200) { let fetchedEvents = response.data.events; + if (authCtx.user.access !== "USER") { - // Set events for non-users setEvents(sortEventsByDate(fetchedEvents)); } else { - // Filter and then sort events for users const filteredEvents = fetchedEvents.filter((event) => userEvents.includes(event.id) ); setEvents(sortEventsByDate(filteredEvents)); } } else { - console.error("Error fetching event data:", response.data.message); setError({ - message: - "Sorry for the inconvenience, we are having issues fetching your Events", + message: "Sorry for the inconvenience, we are having issues fetching your Events", }); } } catch (error) { setError({ - message: - "Sorry for the inconvenience, we are having issues fetching your Events", + message: "Sorry for the inconvenience, we are having issues fetching your Events", }); - console.error("Error fetching team members:", error); - - // const userEvents = authCtx.user.regForm; - // // using local JSON data - // let localEvents = eventsData.events; - // if (authCtx.user.access !== "USER") { - // setEvents(sortEventsByDate(localEvents)); - // } else { - // const filteredEvents = localEvents.filter((event) => - userEvents.includes(event._id) - // ); - // setEvents(sortEventsByDate(filteredEvents)); - // } } finally { setIsLoading(false); } @@ -81,32 +61,35 @@ const Events = () => { }, [authCtx.user.email]); const sortEventsByDate = (events) => { - return events.sort((a, b) => new Date(b.info.eventDate) - new Date(a.info.eventDate)); + return events.sort( + (a, b) => new Date(b.info.eventDate) - new Date(a.info.eventDate) + ); }; const formatDate = (dateString) => { - const options = { day: "2-digit", month: "2-digit", year: "numeric" }; - return new Date(dateString) - .toLocaleDateString("en-GB", options) - .replace(/\//g, "-"); + const options = { day: "2-digit", month: "short", year: "numeric" }; + return new Date(dateString).toLocaleDateString("en-GB", options); }; - // console.log("Event Access",authCtx.user.access); + const hasAnalyticsAccess = + analyticsAccessRoles.includes(authCtx.user.access) || + authCtx.user.email === "srex@fedkiit.com"; + return (
- {authCtx.user.access !== "USER" ? ( -
-

- Events Timeline -

-
- ) : ( -
-

- Participated Events -

+
+

+ {authCtx.user.access !== "USER" ? ( + <>Events Timeline + ) : ( + <>Participated Events + )} +

+ +
+ {events.length} {events.length === 1 ? "Event" : "Events"}
- )} +
{isLoading ? ( @@ -114,90 +97,65 @@ const Events = () => { <> {error &&
{error.message}
} -
+
{events.length > 0 ? ( - - - - - - - {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( +
+
Event NameEvent DateCertificates
+ + + + + + {hasAnalyticsAccess && ( <> - - - - )} - {/* Add more headers */} - - - - - {events.map((event) => ( - - - - - - {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( - + + + )} - {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( - + + + + {events.map((event) => ( + + + + + + - )} - - - ))} - -
EventDateCertificatesManage MailCreate/Edit
- {event.info.eventTitle} - - {formatDate(event.info.eventDate)} - - - - - - - - - Manage MailCreate / Edit - - +
+ {event.info.eventTitle} + + {formatDate(event.info.eventDate)} + + +
+ + {hasAnalyticsAccess && ( + + + + + + )} + + {hasAnalyticsAccess && ( + + + + + + )} + + ))} + + +
) : ( -

Not participated in any Events

+
+

No events found.

+
)}
@@ -206,4 +164,4 @@ const Events = () => { ); }; -export default Events; +export default Events; \ No newline at end of file diff --git a/src/sections/Profile/General/CertificatesView/styles/CertificatesView.module.scss b/src/sections/Profile/General/CertificatesView/styles/CertificatesView.module.scss index ee19232a..4290e850 100644 --- a/src/sections/Profile/General/CertificatesView/styles/CertificatesView.module.scss +++ b/src/sections/Profile/General/CertificatesView/styles/CertificatesView.module.scss @@ -1,205 +1,146 @@ .participatedEvents { - color: #e2e2e2; - font-family: rubik; - font-weight: 300; - display: flex; - flex-direction: column; - padding-left: 0px; - padding-top: 35px; - padding-bottom: 27px; - height: auto; - - .error { - padding: 30px 70px; - z-index: 1; - text-align: center; - margin-top: 2rem; - font-size: 1.2rem; - font-weight: 400; - margin-bottom: 1rem; + --bg-table: #000000; + --bg-hover: #0a0a0a; + --border-color: #222222; + --text-white: #ffffff; + --text-dim: #71717a; + --brand-orange: #f97316; + + /* Transparent outside, color inside */ + background-color: transparent; + color: var(--text-white); + font-family: "Poppins", sans-serif; + display: flex; + flex-direction: column; + padding: 40px 0; + gap: 24px; +} + +.proHeading { + width: min(1100px, calc(100% - 2rem)); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +.headInnerText { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + span { color: var(--brand-orange); } +} + +.countBadge { + padding: 4px 12px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + border: 1px solid var(--border-color); + color: var(--text-dim); +} + +.tableCard { + width: min(1100px, calc(100% - 2rem)); + margin: 0 auto; + border: 1px solid var(--border-color); + background: var(--bg-table); /* Table is solid black */ + border-radius: 12px; + overflow: hidden; +} + +.tableWrapper { width: 100%; } + +.eventsTable { + width: 100%; + border-collapse: collapse; + background: var(--bg-table); + + thead th { + text-align: left; + padding: 18px 24px; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim); + border-bottom: 1px solid var(--border-color); + background: #050505; + } + + tbody td { + padding: 18px 24px; + font-size: 0.9rem; + border-bottom: 1px solid var(--border-color); + } + + tbody tr { + transition: background 0.2s; + &:hover { background: var(--bg-hover); } + &:last-child td { border-bottom: none; } + } +} + +.eventNameCell { font-weight: 600; } +.eventDateCell { color: var(--text-dim); } + +/* Buttons */ +.primaryButton { + background: var(--brand-orange); + color: white; + border: none; + padding: 8px 16px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 0.8rem; + &:hover { opacity: 0.9; } +} + +.secondaryButton { + background: #111; + color: white; + border: 1px solid var(--border-color); + padding: 8px 16px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 0.8rem; + &:hover { background: #222; } +} + +/* Mobile Responsive */ +@media screen and (max-width: 768px) { + .proHeading { flex-direction: row; } + + .eventsTable { + thead { display: none; } + tbody tr { + display: block; + padding: 16px; + border-bottom: 1px solid var(--border-color); } - - .proHeading { - font-size: 30px; - letter-spacing: 0.02em; - line-height: 40px; - margin-bottom: 50px; - width: 100%; + tbody td { display: flex; - align-items: center; justify-content: space-between; - } - - .headInnerText span { - margin-right: 3px; - background: linear-gradient(to right, #d03e21 40%, #ff8a00); - -webkit-background-clip: text; - color: transparent; - } - - .headInnerText { - margin-left: 10.5rem; - } - - .tables { - display: flex; - justify-content: start; align-items: center; - width: 60%; - margin-left: 10.5rem; - } - - .eventsTable { - width: 100%; - border-collapse: collapse; - - th { - border-bottom: 2px solid grey; - padding-bottom: 10px; - padding-top: 5px; - font-size: 20px; - } - - td { - font-size: 15px; - } - - th, - td { - text-align: center; - width: 34%; + padding: 8px 0; + border: none; + &::before { + content: attr(data-label); + font-size: 0.7rem; + color: var(--text-dim); + text-transform: uppercase; + font-weight: 700; } } - - .eventsTable thead, - .eventsTable tbody tr { - display: table; - width: 100%; - table-layout: fixed; - } - - .eventsTable tbody { - display: block; - max-height: 360px; - overflow-y: scroll; - scrollbar-width: none; - width: 100%; - } - - .mobilewidth { - width: 100%; - overflow-x: auto; - } - - .noEvents { - text-align: center; - font-size: 25px; - font-weight: 700; - color: grey; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - margin: auto; - } } - - @media screen and (max-width: 989px) { - .participatedEvents .tables { - margin-left: 5rem; - } - - .participatedEvents .headInnerText { - margin-left: 5rem; - } + + .eventNameCell { + font-size: 1.1rem; + color: var(--brand-orange); + border-bottom: 1px solid var(--border-color) !important; + margin-bottom: 8px; + padding-bottom: 12px !important; + &::before { display: none; } } - - @media screen and (max-width: 867px) { - .participatedEvents .tables { - margin-left: 2rem; - } - - .participatedEvents .headInnerText { - margin-left: 2rem; - } - } - - @media screen and (max-width: 768px) { - .participatedEvents .headInnerText { - text-align: center; - width: 100%; - margin-left: 10vw; - margin-right: 10vw; - } - - .participatedEvents .eventsTable tbody { - max-height: 250px; - } - - .participatedEvents .proHeading { - margin-bottom: 20px; - } - - .eventsTable { - margin-left: 3vw; - margin-right: 3vw; - } - - .error { - padding: 30px 70px; - z-index: 1; - text-align: center; - margin-top: 10px; - font-size: .9rem; - font-weight: 400; - margin-bottom: 10px; - } - - .participatedEvents .tables { - width: 100%; - margin-left: 0rem; - } - - .noEvents { - font-size: 22px; - height: auto; - } - } - - @media screen and (max-width: 515px) { - .participatedEvents .headInnerText { - margin-left: 5vw; - margin-right: 5vw; - } - - .noEvents { - font-size: 20px; - } - } - - @media screen and (max-width: 480px) { - .participatedEvents .headInnerText { - font-size: 30px; - } - - .participatedEvents .eventsTable th { - font-size: 17px; - } - - .error { - padding: 30px 70px; - z-index: 1; - text-align: center; - margin-top: 10px; - font-size: 0.9rem; - font-weight: 400; - margin-bottom: 10px; - } - - .noEvents { - font-size: 18px; - } - } - - \ No newline at end of file +} \ No newline at end of file diff --git a/src/sections/Profile/General/EventsView/EventsView.jsx b/src/sections/Profile/General/EventsView/EventsView.jsx index 242bc51d..41adf049 100644 --- a/src/sections/Profile/General/EventsView/EventsView.jsx +++ b/src/sections/Profile/General/EventsView/EventsView.jsx @@ -14,7 +14,7 @@ const Events = () => { const [error, setError] = useState(null); const [certificates, setCertificates] = useState([]); const [certMap, setCertMap] = useState({}); - const [loadingCerts, setLoadingCerts] = useState(true); // New state for certificate loading + const [loadingCerts, setLoadingCerts] = useState(true); const viewPath = "/profile/Events"; const analyticsPath = "/profile/events/Analytics"; @@ -39,10 +39,8 @@ const Events = () => { if (response.status === 200) { let fetchedEvents = response.data.events; if (authCtx.user.access !== "USER") { - // Set events for non-users setEvents(sortEventsByDate(fetchedEvents)); } else { - // Filter and then sort events for users const filteredEvents = fetchedEvents.filter((event) => userEvents.includes(event.id) ); @@ -61,18 +59,6 @@ const Events = () => { "Sorry for the inconvenience, we are having issues fetching your Events", }); console.error("Error fetching team members:", error); - - // const userEvents = authCtx.user.regForm; - // // using local JSON data - // let localEvents = eventsData.events; - // if (authCtx.user.access !== "USER") { - // setEvents(sortEventsByDate(localEvents)); - // } else { - // const filteredEvents = localEvents.filter((event) => - // userEvents.includes(event._id); - // ); - // setEvents(sortEventsByDate(filteredEvents)); - // } } finally { setIsLoading(false); } @@ -93,9 +79,8 @@ const Events = () => { headers: { Authorization: `Bearer ${authCtx.token}` }, } ); - // console.log(response); if (response.status === 200) { - setCertificates(response.data.certandevent); // This will be an array of { cert, event } + setCertificates(response.data.certandevent); } } catch (err) { console.error("Error fetching certificates:", err); @@ -107,9 +92,7 @@ const Events = () => { const getCertificateForEvent = async (eventId) => { const eid = await accessOrCreateEventByFormId(eventId, authCtx.token); - // console.log(eid, certificates[0].cert.eventId); const found = certificates.find((item) => item.cert.eventId == eid.id); - // console.log(found); return found ? found.cert : null; }; @@ -128,7 +111,7 @@ const Events = () => { useEffect(() => { const fetchAllCerts = async () => { - setLoadingCerts(true); // Start loading + setLoadingCerts(true); const map = {}; if (events.length > 0) { for (const event of events) { @@ -140,32 +123,30 @@ const Events = () => { } } setCertMap(map); - setLoadingCerts(false); // End loading + setLoadingCerts(false); }; if (events.length > 0 && certificates.length > 0) { fetchAllCerts(); } else if (events.length > 0 && !isLoading) { - // If events are loaded but no certificates found setLoadingCerts(false); } }, [events, certificates]); return (
- {authCtx.user.access !== "USER" ? ( -
+
+ {authCtx.user.access !== "USER" ? (

Events Timeline

-
- ) : ( -
+ ) : (

Participated Events

-
- )} + )} +
{events.length} events
+
{isLoading ? ( @@ -173,60 +154,45 @@ const Events = () => { <> {error &&
{error.message}
} -
+
+
+
{events.length > 0 ? ( - - - - + + + + {authCtx.user.access === "USER" && } {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( - + )} {events.map((event) => ( - - + - {/* View Event Details - accessible to all */} - {/* Certificate - only for USERS */} {authCtx.user.access === "USER" && ( - )} @@ -279,6 +235,7 @@ const Events = () => { ) : (

Not participated in any Events

)} + )} diff --git a/src/sections/Profile/General/EventsView/styles/EventsView.module.scss b/src/sections/Profile/General/EventsView/styles/EventsView.module.scss index 4cc3fb4c..efe7e717 100644 --- a/src/sections/Profile/General/EventsView/styles/EventsView.module.scss +++ b/src/sections/Profile/General/EventsView/styles/EventsView.module.scss @@ -1,204 +1,215 @@ .participatedEvents { - color: #ffffff; - font-family: 'Poppins', sans-serif; - font-weight: 300; + color: #e5e7eb; + font-family: "Poppins", sans-serif; display: flex; flex-direction: column; - padding-left: 0px; - padding-top: 35px; - padding-bottom: 27px; - height: auto; - - .error { - padding: 30px 70px; - z-index: 1; - text-align: center; - margin-top: 2rem; - font-size: 1.2rem; - font-weight: 400; - margin-bottom: 1rem; - } + padding: 34px 0 28px; + gap: 24px; + box-sizing: border-box; +} - .proHeading { - font-size: 30px; - letter-spacing: 0.02em; - line-height: 40px; - margin-bottom: 50px; - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - } +.error { + padding: 16px 20px; + text-align: center; + border: 1px solid rgba(239, 68, 68, 0.4); + background: rgba(127, 29, 29, 0.2); + border-radius: 10px; + font-size: 0.95rem; + width: min(1100px, calc(100% - 2rem)); + margin: 0 auto; +} - .headInnerText span { - margin-right: 3px; - background: linear-gradient(to right, #d03e21 40%, #ff8a00); - -webkit-background-clip: text; - color: transparent; - } +.proHeading { + width: min(1100px, calc(100% - 2rem)); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} - .headInnerText { - margin-left: 10.5rem; - } +.headInnerText { + margin: 0; + font-size: clamp(1.6rem, 2.2vw, 2rem); + letter-spacing: 0.02em; + color: #f8fafc; + font-weight: 600; +} - .tables { - display: flex; - justify-content: start; - align-items: center; - width: 60%; - margin-left: 10.5rem; - } +.headInnerText span { + margin-right: 4px; + background: linear-gradient(to right, #d03e21 40%, #ff8a00); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} - .eventsTable { - width: 100%; - border-collapse: collapse; - - th { - border-bottom: 2px solid grey; - padding-bottom: 10px; - padding-top: 5px; - font-size: 20px; - } - - td { - font-size: 15px; - } - - th, - td { - text-align: center; - width: 34%; - } - } +.countBadge { + padding: 6px 14px; + border-radius: 999px; + font-size: 0.78rem; + letter-spacing: 0.06em; + text-transform: uppercase; + font-weight: 600; + border: 1px solid rgba(251, 146, 60, 0.55); + background: rgba(251, 146, 60, 0.12); + color: #ffe0c2; + white-space: nowrap; +} - .eventsTable thead, - .eventsTable tbody tr { - display: table; - width: 100%; - table-layout: fixed; - } +.tableCard { + width: min(1100px, calc(100% - 2rem)); + margin: 0 auto; + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 16px; + background: linear-gradient(180deg, rgba(17, 17, 20, 0.96), rgba(10, 10, 12, 0.96)); + position: relative; + overflow: hidden; + box-shadow: 0 8px 32px rgba(10, 10, 12, 0.5); +} - .eventsTable tbody { - display: block; - max-height: 360px; - overflow-y: scroll; - scrollbar-width: none; - width: 100%; - } +.timelineLine { + position: absolute; + left: 20px; + top: 54px; + bottom: 20px; + width: 2px; + border-radius: 2px; + background: linear-gradient(180deg, rgba(251, 146, 60, 0.45), rgba(148, 163, 184, 0.08)); + pointer-events: none; + z-index: 3; +} - .mobilewidth { - width: 100%; - overflow-x: auto; - } +.tables { + width: 100%; + overflow-x: auto; + padding-left: 36px; + padding-right: 12px; + box-sizing: border-box; +} - .noEvents { - text-align: center; - font-size: 25px; - font-weight: 700; - color: grey; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - margin: auto; - } +.tables::-webkit-scrollbar { + height: 8px; } -@media screen and (max-width: 989px) { - .participatedEvents .tables { - margin-left: 5rem; - } +.tables::-webkit-scrollbar-track { + background: rgba(148, 163, 184, 0.05); + border-radius: 0 0 16px 16px; +} - .participatedEvents .headInnerText { - margin-left: 5rem; - } +.tables::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.18); + border-radius: 8px; } -@media screen and (max-width: 867px) { - .participatedEvents .tables { - margin-left: 2rem; - } +.eventsTable { + width: 100%; + min-width: 720px; + border-collapse: collapse; +} - .participatedEvents .headInnerText { - margin-left: 2rem; - } +.eventsTable thead th { + text-align: left; + font-size: 0.8rem; + letter-spacing: 0.05em; + text-transform: uppercase; + color: #a7b0bf; + font-weight: 600; + padding: 18px 14px; + position: sticky; + top: 0; + background: rgba(12, 12, 14, 0.85); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 2; + border-bottom: 1px solid rgba(148, 163, 184, 0.18); } -@media screen and (max-width: 768px) { - .participatedEvents .headInnerText { - text-align: center; - width: 100%; - margin-left: 10vw; - margin-right: 10vw; - } +.eventsTable tbody td { + padding: 16px 14px; + border-bottom: 1px solid rgba(148, 163, 184, 0.06); + font-size: 0.92rem; + vertical-align: middle; +} - .participatedEvents .eventsTable tbody { - max-height: 250px; - } +.eventsTable tbody tr:last-child td { + border-bottom: none; +} - .participatedEvents .proHeading { - margin-bottom: 20px; - } +.eventsTable tbody tr:nth-child(even) { + background: rgba(255, 255, 255, 0.015); +} - .eventsTable { - margin-left: 3vw; - margin-right: 3vw; - } +.eventNameCell { + font-weight: 500; + display: flex; + align-items: center; + gap: 12px; +} - .error { - padding: 30px 70px; - z-index: 1; - text-align: center; - margin-top: 10px; - font-size: .9rem; - font-weight: 400; - margin-bottom: 10px; - } +.eventDateCell { + color: #cbd5e1; +} - .participatedEvents .tables { - width: 100%; - margin-left: 0rem; - } +.viewButton { + border: 1px solid rgba(251, 146, 60, 0.5); + background: rgba(251, 146, 60, 0.18); + color: #ffe0c2; + border-radius: 8px; + padding: 8px 16px; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} - .noEvents { - font-size: 22px; - height: auto; - } +.viewButton:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(10, 10, 12, 0.96), 0 0 0 4px rgba(251, 146, 60, 0.8); } -@media screen and (max-width: 515px) { - .participatedEvents .headInnerText { - margin-left: 5vw; - margin-right: 5vw; - } +.viewButton:disabled { + opacity: 0.45; + cursor: not-allowed; +} - .noEvents { - font-size: 20px; - } +.loadingContainer { + min-height: 80px; + display: flex; + align-items: center; + justify-content: center; } -@media screen and (max-width: 480px) { - .participatedEvents .headInnerText { - font-size: 30px; +.noEvents { + text-align: center; + font-size: 1.1rem; + font-weight: 500; + color: #94a3b8; + padding: 60px 20px; +} + +@media screen and (max-width: 768px) { + .participatedEvents { + padding-top: 20px; + gap: 16px; } - .participatedEvents .eventsTable th { - font-size: 17px; + .proHeading { + width: calc(100% - 1.5rem); + flex-wrap: wrap; } - .error { - padding: 30px 70px; - z-index: 1; - text-align: center; - margin-top: 10px; - font-size: 0.9rem; - font-weight: 400; - margin-bottom: 10px; + .tableCard { + width: calc(100% - 1.5rem); + border-radius: 12px; } - .noEvents { - font-size: 18px; + .timelineLine { + left: 12px; } -} + .tables { + padding-left: 24px; + padding-right: 8px; + } +} \ No newline at end of file diff --git a/src/sections/Profile/General/ProfileView/ProfileView.jsx b/src/sections/Profile/General/ProfileView/ProfileView.jsx index 18ed4a63..7d31325f 100644 --- a/src/sections/Profile/General/ProfileView/ProfileView.jsx +++ b/src/sections/Profile/General/ProfileView/ProfileView.jsx @@ -109,7 +109,14 @@ const Profile = ({ editmodal }) => { ) : ( diff --git a/src/sections/Profile/General/ProfileView/styles/ProfileView.module.scss b/src/sections/Profile/General/ProfileView/styles/ProfileView.module.scss index 7e1b0f4e..735637c7 100644 --- a/src/sections/Profile/General/ProfileView/styles/ProfileView.module.scss +++ b/src/sections/Profile/General/ProfileView/styles/ProfileView.module.scss @@ -1,229 +1,139 @@ #profile { color: #ffffff; - font-family: 'Poppins', sans-serif; + font-family: "Poppins", sans-serif; font-weight: 300; display: flex; flex-direction: column; - padding-left: 0px; - padding-top: 35px; - padding-bottom: 27px; + box-sizing: border-box; + width: 100%; + min-height: 100%; + padding: 1.2rem 1rem 1.6rem; } - + .proHeading { - font-size: 30px; + font-size: 2rem; letter-spacing: 0.02em; - line-height: 40px; - padding-bottom: 50px; + line-height: 1.2; + padding-bottom: 1.5rem; width: 100%; display: flex; align-items: center; justify-content: space-between; } -.headInnerText{ - margin-left: 10rem; +.headInnerText { + margin-left: 0; } - -.headInnerText span{ + +.headInnerText span { margin-right: 3px; - background: linear-gradient(to right , #d03e21 40%, #FF8A00 ); + background: linear-gradient(to right, #d03e21 40%, #ff8a00); -webkit-background-clip: text; color: transparent; - } - -.details{ + +.details { display: flex; - justify-content: start; - align-items: center; + justify-content: flex-start; + align-items: flex-start; width: 100%; - // background-color: red; - margin-left: 10rem; - + margin-left: 0; + min-width: 0; } -// .profileBox{ - -// width: 700px; -// display: flex; -// } +.profileBox { + width: min(100%, 900px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 18px; + padding: 1.2rem 1.25rem 0.35rem; + background: rgba(255, 255, 255, 0.02); +} .profileTable { table-layout: fixed; - width: 70%; + width: 100%; + border-collapse: collapse; } .vals { font-family: "Open Sans", sans-serif; - font-size: 20px; + font-size: 1.15rem; padding-bottom: 27px; &.highlight { - max-width: 50%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &.highlight:hover{ - overflow: scroll; - text-overflow: clip; - } - - &.highlight::-webkit-scrollbar{ - display: none; + overflow-wrap: anywhere; + word-break: break-word; + white-space: normal; } } -.dets{ +.dets { font-family: "Open Sans", sans-serif; - font-size: 20px; + font-size: 1.15rem; padding-bottom: 27px; - width:35%; + width: 34%; font-weight: 700; } +.detailLink { + color: #ffffff; + font-weight: 500; + text-decoration: none; + overflow-wrap: anywhere; + word-break: break-word; +} - .dets { - padding-right: 100px; - } +.dets { + padding-right: 2rem; +} - .editbtn{ - font-size: 1.6rem; - margin-right: 13rem; - cursor: pointer; - display: flex; - align-items: end; - } - - @media screen and (max-width:989px) { - .details{ - width: 90%; - margin-left: 5rem; - } - - .headInnerText{ - margin-left: 5rem; - } - - .editbtn{ - margin-right: 5rem; - } - } - - @media screen and (max-width:867px) { - .details{ - width: 90%; - margin-left: 2rem; - } - - .headInnerText{ - margin-left: 2rem; - } - - .editbtn{ - margin-right: 2rem; - } - } +.editbtn { + font-size: 1.5rem; + margin-right: 0; + cursor: pointer; + display: flex; + align-items: center; +} - @media screen and (max-width:769px) { - .proHeading{ - display: grid; - grid-template-columns: 15% 70% 15%; - } - - .headInnerText{ - grid-column-start: 2; - grid-column-end: 3; - text-align: center; - margin-left: 0px; - } - - .editbtn{ - margin-right: 0rem; - margin-left: 20%; - } - - .details{ - justify-content: center; - width: 95%; - margin-left: 1rem; - margin-right: 1rem; - } - - .dets{ - width:50%; - padding-left: 2rem; - } +@media screen and (max-width: 900px) { + .profileBox { + width: 100%; + } +} +@media screen and (max-width: 769px) { + #profile { + padding: 0.9rem 0.4rem 1rem; } - - @media screen and (max-width:480px) { - #profile { - padding-left: 15px; - padding-right: 15px; - } - - .proHeading{ - display: grid; - grid-template-columns: 12.5% 65% 10% 12.5%; - margin-left: -1.1rem; - } - - .headInnerText{ - font-size: 30px; - text-align: start; - grid-column-start: 2; - grid-column-end: 3; - } - - .editbtn{ - font-size: 20px; - text-align: end; - grid-column-start: 3; - grid-column-end: 4; - } - - .dets { - padding-right: 50px; - } - - .profileTable{ - width: 100%; - margin-left: -2%; - } + .proHeading { + font-size: 1.65rem; + padding-bottom: 1.1rem; } - @media screen and (max-width:430px) { .dets, .vals { - font-size: 16px; + font-size: 1rem; } - .proHeading{ - display: grid; - grid-template-columns: 7.5% 70% 15% 7.5%; - margin-left: 0.8rem; + .dets { + width: 44%; + padding-right: 1rem; } + + .profileBox { + border-radius: 14px; + padding: 1rem 0.8rem 0.2rem; } +} - @media screen and (max-width:380px) { - - .dets { - padding-right: 30px; - } - - .proHeading{ - display: grid; - grid-template-columns: 5% 80% 15%; - margin-left: 10px; - } - - .profileTable{ - margin-left: -20px; - } - +@media screen and (max-width: 430px) { + .dets, + .vals { + font-size: 16px; + } + .dets { + width: 46%; + padding-right: 0.7rem; } - - \ No newline at end of file +} diff --git a/src/services/client/axiosClient.js b/src/services/client/axiosClient.js index dfde5df3..1253a172 100644 --- a/src/services/client/axiosClient.js +++ b/src/services/client/axiosClient.js @@ -7,4 +7,17 @@ const axiosClient = axios.create({ withCredentials: true // Send cookies with requests for authentication }); -export default axiosClient; \ No newline at end of file +// Attach auth token from localStorage when available +axiosClient.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +export default axiosClient; diff --git a/vite.config.js b/vite.config.js index a25f2355..35fef7c4 100644 --- a/vite.config.js +++ b/vite.config.js @@ -12,6 +12,7 @@ export default defineConfig({ // } // }, // }, + appType: 'spa', // Always serve index.html for deep routes (fixes new-tab 404) plugins: [react()], css: { preprocessorOptions: {
Event NameEvent DateDetailsCertificateEventDateDetailsCertificate - Registrations - Registrations
+
+ {event.info.eventTitle} + {formatDate(event.info.eventDate)} + - + + {loadingCerts ? (
@@ -245,7 +211,7 @@ const Events = () => { @@ -256,19 +222,9 @@ const Events = () => { {/* Analytics - only for admins and specific roles */} {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email === "srex@fedkiit.com") && ( -
+ - +
{detail.label} - {detail.value} + + {detail.value} +