api registration done

This commit is contained in:
Meenadeveloper 2026-03-01 19:47:06 +05:30
parent fd0a327b13
commit 653055debc
38 changed files with 3411 additions and 1385 deletions

203
package-lock.json generated
View File

@ -8,15 +8,12 @@
"name": "thirukalyanam", "name": "thirukalyanam",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@date-io/date-fns": "^3.2.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@files-ui/react": "^1.2.5", "@files-ui/react": "^1.2.5",
"@lottiefiles/dotlottie-react": "^0.17.8",
"@mui/icons-material": "^7.3.5", "@mui/icons-material": "^7.3.5",
"@mui/lab": "^7.0.1-beta.19", "@mui/lab": "^7.0.1-beta.19",
"@mui/material": "^7.3.5", "@mui/material": "^7.3.5",
"@mui/styled-engine-sc": "^7.3.5",
"@mui/x-date-pickers": "^8.19.0", "@mui/x-date-pickers": "^8.19.0",
"@reduxjs/toolkit": "^2.11.0", "@reduxjs/toolkit": "^2.11.0",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
@ -34,14 +31,11 @@
"react-lazy-load-image-component": "^1.6.3", "react-lazy-load-image-component": "^1.6.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"styled-components": "^6.1.19",
"swiper": "^12.0.3", "swiper": "^12.0.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
@ -92,6 +86,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -343,29 +338,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@date-io/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-3.2.0.tgz",
"integrity": "sha512-hqwXvY8/YBsT9RwQITG868ZNb1MVFFkF7W1Ecv4P472j/ZWa7EFcgSmxy8PUElNVZfvhdvfv+a8j6NWJqOX5mA==",
"license": "MIT"
},
"node_modules/@date-io/date-fns": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-3.2.1.tgz",
"integrity": "sha512-CtXgTOAamkImI+CmbWRNdBi4ljj9xm/tdoPa+eeeiygduzubJTsXp18vYz+Vs/9yLho1zUOXlxpsfsF7PsXSWQ==",
"license": "MIT",
"dependencies": {
"@date-io/core": "^3.2.0"
},
"peerDependencies": {
"date-fns": "^3.2.0 || ^4.1.0"
},
"peerDependenciesMeta": {
"date-fns": {
"optional": true
}
}
},
"node_modules/@dimforge/rapier3d-compat": { "node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
@ -452,6 +424,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
@ -495,6 +468,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
@ -1197,6 +1171,7 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz",
"integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/component": "0.7.1", "@firebase/component": "0.7.1",
"@firebase/logger": "0.5.0", "@firebase/logger": "0.5.0",
@ -1263,6 +1238,7 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz",
"integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==", "integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@firebase/app": "0.14.9", "@firebase/app": "0.14.9",
"@firebase/component": "0.7.1", "@firebase/component": "0.7.1",
@ -1278,7 +1254,8 @@
"version": "0.9.3", "version": "0.9.3",
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
"license": "Apache-2.0" "license": "Apache-2.0",
"peer": true
}, },
"node_modules/@firebase/auth-compat": { "node_modules/@firebase/auth-compat": {
"version": "0.6.3", "version": "0.6.3",
@ -1735,6 +1712,7 @@
"integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
@ -1886,24 +1864,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@lottiefiles/dotlottie-react": {
"version": "0.17.8",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.8.tgz",
"integrity": "sha512-Hk0bISNURSqL7t+H7S5lW2NQVa1hiibnqRRg6kOWZpswBxfQk+/6WBPc9EfuetdoZmiMoDsmcI0HR4I20oTBRg==",
"license": "MIT",
"dependencies": {
"@lottiefiles/dotlottie-web": "0.57.0"
},
"peerDependencies": {
"react": "^17 || ^18 || ^19"
}
},
"node_modules/@lottiefiles/dotlottie-web": {
"version": "0.57.0",
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.57.0.tgz",
"integrity": "sha512-gcgvu9T21YzeY3JjHCZrxftucsxzMH6e9h+8NMv8mbfo1y1M9/jdcsdu40S+pnSLz9/OyiSBQ/EjDsbSOHZy0w==",
"license": "MIT"
},
"node_modules/@mediapipe/tasks-vision": { "node_modules/@mediapipe/tasks-vision": {
"version": "0.10.17", "version": "0.10.17",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
@ -2024,6 +1984,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5", "@mui/core-downloads-tracker": "^7.3.5",
@ -2129,34 +2090,12 @@
} }
} }
}, },
"node_modules/@mui/styled-engine-sc": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@mui/styled-engine-sc/-/styled-engine-sc-7.3.5.tgz",
"integrity": "sha512-tm2KcMF9Y4vhi38ITxhTkeGhux/sCB+AuBz2AyjXKOxuZ9iSUsixAKvzeH/HGbQaih8qVF3ocbq95PQZ/bd/gQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@types/hoist-non-react-statics": "^3.3.7",
"csstype": "^3.1.3",
"hoist-non-react-statics": "^3.3.2",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"styled-components": "^6.0.0"
}
},
"node_modules/@mui/system": { "node_modules/@mui/system": {
"version": "7.3.5", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz",
"integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.5", "@mui/private-theming": "^7.3.5",
@ -3113,7 +3052,8 @@
} }
], ],
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/@tsparticles/interaction-external-attract": { "node_modules/@tsparticles/interaction-external-attract": {
"version": "3.9.1", "version": "3.9.1",
@ -3731,20 +3671,11 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==", "integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-reconciler": { "node_modules/@types/react-reconciler": {
"version": "0.26.7", "version": "0.26.7",
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
@ -3780,6 +3711,7 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.181.0.tgz", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.181.0.tgz",
"integrity": "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==", "integrity": "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0", "@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3", "@tweenjs/tween.js": "~23.1.3",
@ -4052,6 +3984,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -4336,6 +4269,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@ -4767,7 +4701,8 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
"version": "3.2.4", "version": "3.2.4",
@ -4895,6 +4830,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@ -5009,7 +4945,8 @@
"version": "8.6.0", "version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/embla-carousel-react": { "node_modules/embla-carousel-react": {
"version": "8.6.0", "version": "8.6.0",
@ -5192,6 +5129,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -6925,6 +6863,7 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
"integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.17.8", "@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.26.7", "@types/react-reconciler": "^0.26.7",
@ -7007,6 +6946,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@ -7614,6 +7554,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7722,6 +7663,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -7998,6 +7940,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -8007,6 +7950,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -8104,6 +8048,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@ -8330,7 +8275,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@ -8569,6 +8515,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -8789,89 +8736,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/styled-components": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz",
"integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==",
"license": "MIT",
"dependencies": {
"@emotion/is-prop-valid": "1.2.2",
"@emotion/unitless": "0.8.1",
"@types/stylis": "4.2.5",
"css-to-react-native": "3.2.0",
"csstype": "3.1.3",
"postcss": "8.4.49",
"shallowequal": "1.1.0",
"stylis": "4.3.2",
"tslib": "2.6.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/@emotion/is-prop-valid": {
"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==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/styled-components/node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
"license": "MIT"
},
"node_modules/styled-components/node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/styled-components/node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/styled-components/node_modules/stylis": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
"integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
"license": "MIT"
},
"node_modules/stylis": { "node_modules/stylis": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
@ -9336,6 +9200,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -9488,6 +9353,7 @@
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
"integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@discoveryjs/json-ext": "^0.5.0", "@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^1.2.0", "@webpack-cli/configtest": "^1.2.0",
@ -9726,6 +9592,7 @@
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -10,15 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@date-io/date-fns": "^3.2.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@files-ui/react": "^1.2.5", "@files-ui/react": "^1.2.5",
"@lottiefiles/dotlottie-react": "^0.17.8",
"@mui/icons-material": "^7.3.5", "@mui/icons-material": "^7.3.5",
"@mui/lab": "^7.0.1-beta.19", "@mui/lab": "^7.0.1-beta.19",
"@mui/material": "^7.3.5", "@mui/material": "^7.3.5",
"@mui/styled-engine-sc": "^7.3.5",
"@mui/x-date-pickers": "^8.19.0", "@mui/x-date-pickers": "^8.19.0",
"@reduxjs/toolkit": "^2.11.0", "@reduxjs/toolkit": "^2.11.0",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
@ -36,14 +33,11 @@
"react-lazy-load-image-component": "^1.6.3", "react-lazy-load-image-component": "^1.6.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"styled-components": "^6.1.19",
"swiper": "^12.0.3", "swiper": "^12.0.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",

View File

@ -7,8 +7,18 @@ import { generateToken, listenToMessages } from "./notifications/firebase";
function App() { function App() {
useEffect(()=>{ useEffect(()=>{
generateToken(); const run = () => {
listenToMessages(); // foreground notifications generateToken();
listenToMessages(); // foreground notifications
};
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
const handle = window.requestIdleCallback(run);
return () => window.cancelIdleCallback(handle);
}
const timeout = setTimeout(run, 800);
return () => clearTimeout(timeout);
},[]); },[]);
return ( return (

View File

@ -24,5 +24,7 @@ REGISTER_STEP2:"update_educational_details", // educational details updated ap
REGSITER_STEP3:"update_family_details", // family details updated api REGSITER_STEP3:"update_family_details", // family details updated api
REGISTER_STEP4:"update_lifestyle_details", // lifestyle details updated api REGISTER_STEP4:"update_lifestyle_details", // lifestyle details updated api
REGISTER_STEP5:"update_preferred_details", // partner preference details updated api REGISTER_STEP5:"update_preferred_details", // partner preference details updated api
PREVIEW_DETAILS: "get_preview_details",
REVIEWS: "reviews",
}; };

View File

@ -21,9 +21,10 @@ export const getSubCasteMasters = async (caste_id) => {
}; };
export const getCityMasters = async (state_id) => { export const getCityMasters = async (state_id) => {
const res = await axiosInstance.get(API_ENDPOINTS.CITY_MASTER, { const params = Array.isArray(state_id)
params: { state_id }, ? { state_id: `[${state_id.join(",")}]` }
}); : { state_id };
const res = await axiosInstance.get(API_ENDPOINTS.CITY_MASTER, { params });
return res.data; return res.data;
}; };
@ -39,6 +40,13 @@ export const getEducationMasters = async () => {
return res.data; return res.data;
}; };
export const getEducationList = async (study_field_id) => {
const res = await axiosInstance.get(API_ENDPOINTS.EDUCATION_LIST_API, {
params: { study_field_id },
});
return res.data;
};
export const getFamilyMasters = async () => { export const getFamilyMasters = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.FAMILY_DETAILS_MASTER); const res = await axiosInstance.get(API_ENDPOINTS.FAMILY_DETAILS_MASTER);
return res.data; return res.data;

7
src/api/preview.api.js Normal file
View File

@ -0,0 +1,7 @@
import axiosInstance from "./axiosInstance";
import { API_ENDPOINTS } from "./apiEndpoints";
export const getPreviewDetails = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.PREVIEW_DETAILS);
return res.data;
};

View File

@ -32,3 +32,11 @@ export const registerStep4API = async (payload) => {
const res = await axiosInstance.post(API_ENDPOINTS.REGISTER_STEP4, payload); const res = await axiosInstance.post(API_ENDPOINTS.REGISTER_STEP4, payload);
return res.data; return res.data;
}; };
/**
* STEP 5 Partner Preferences
*/
export const registerStep5API = async (payload) => {
const res = await axiosInstance.post(API_ENDPOINTS.REGISTER_STEP5, payload);
return res.data;
};

7
src/api/reviews.api.js Normal file
View File

@ -0,0 +1,7 @@
import axiosInstance from "./axiosInstance";
import { API_ENDPOINTS } from "./apiEndpoints";
export const getReviews = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.REVIEWS);
return res.data;
};

9
src/api/terms.api.js Normal file
View File

@ -0,0 +1,9 @@
import axiosInstance from "./axiosInstance";
import { API_ENDPOINTS } from "./apiEndpoints";
export const getTermsAndPolicies = async (type) => {
const res = await axiosInstance.get(API_ENDPOINTS.TERMS_AND_POLICIES_PRIVACY, {
params: { type },
});
return res.data;
};

View File

@ -1,158 +1,23 @@
import React, { useState } from "react"; import React from "react";
import { Users, Grid3x3, Heart, Crown, Bookmark } from "lucide-react"; import ProfileCardItem from "../profiledashboard/ProfileCardItem";
import LazyImage from "./LazyImage";
import CakeIcon from "@mui/icons-material/Cake";
import HeightIcon from "@mui/icons-material/Height";
import GroupsIcon from "@mui/icons-material/Groups";
import TempleHinduIcon from "@mui/icons-material/TempleHindu";
import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import { motion } from 'framer-motion';
export default function ProfileCard() { export default function ProfileCard() {
const [isLiked, setIsLiked] = useState(false); const profile = {
id: 1,
name: "Jerome Bell",
userId: "JB2847593",
age: 22,
height: "5.2",
religion: "Hindu / Agamudiyar/thular",
education: "BCA / Data analyst",
location: "Chennai",
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=600&h=800&fit=crop&crop=faces,top",
isPremium: true,
};
return ( return (
<div className=" flex items-center justify-center p-6"> <div className="flex items-center justify-center p-6">
<div className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200"> <ProfileCardItem profile={profile} />
{/* Profile Image Section */}
<div className="relative">
{/* Premium Badge */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring' }}
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
{/* Shortlist Button */}
<motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
>
<Bookmark className="w-4 h-4" />
<span className="text-[12px] font-medium">Shortlist</span>
</motion.button>
<img
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=600&h=800&fit=crop&crop=faces,top"
alt="Profile"
className="w-full h-90 object-cover"
/>
{/* <LazyImage
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=600&h=800&fit=crop&crop=faces,top"
alt="Profile"
className="w-full h-90 object-cover"
/> */}
{/* White Gradient Overlay at bottom of image */}
<div
className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none"
style={{
background:
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
}}
></div>
{/* Profile Info Overlay - positioned at bottom */}
<div className="absolute bottom-0 left-0 right-0 p-6 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-2">
Jerome bell
</h1>
<p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: JB2847593
</p>
</div>
</div>
{/* Stats and Follow Section */}
<div
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2 "
style={{
background: "rgb(255, 255, 255)",
}}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<CakeIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
Age : 22
</span>
</div>
<div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
Height: 5.2
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<GroupsIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
Hindu / Agamudiyar/thular
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<SchoolIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
BCA / Data analyst
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
Chennai
</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3 my-2 justify-between mx-4">
<button className="w-[60px] h-[60px] bg-[#A70710] hover:bg-red-300 text-white
font-semibold text-base py-2 rounded-full shadow-md
hover:shadow-lg transition-all duration-300 flex items-center justify-center gap-2 transform hover:scale-90">
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<button className="w-[60px] h-[60px] bg-[#034E08] hover:from-green-600
hover:to-green-600 text-white font-semibold text-base
rounded-full shadow-lg hover:shadow-xl transition-all duration-300
transform hover:scale-105 flex items-center justify-center gap-2"
onClick={() => setIsLiked(!isLiked)}
>
{isLiked ? (
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#EF4444"/>
</svg>
) : (
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</button>
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,72 @@
import React, { useEffect } from "react";
const STYLE_ID = "skeleton-shimmer-styles";
const injectSkeletonStyles = () => {
if (typeof document === "undefined") return;
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
.skeleton-shimmer {
position: relative;
overflow: hidden;
background: #e5e7eb;
}
.skeleton-shimmer::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.7) 50%, rgba(255,255,255,0) 100%);
animation: skeleton-shimmer 1.6s infinite;
}
@keyframes skeleton-shimmer {
100% { transform: translateX(100%); }
}
`;
document.head.appendChild(style);
};
export const Skeleton = ({
width = "100%",
height = 12,
rounded = 6,
className = "",
}) => {
useEffect(() => {
injectSkeletonStyles();
}, []);
return (
<div
className={`skeleton-shimmer ${className}`}
style={{ width, height, borderRadius: rounded }}
/>
);
};
export const SkeletonText = ({
lines = 3,
gap = 10,
height = 12,
className = "",
}) => {
useEffect(() => {
injectSkeletonStyles();
}, []);
return (
<div className={className} style={{ display: "grid", gap }}>
{Array.from({ length: lines }).map((_, idx) => (
<Skeleton
key={idx}
height={height}
width={idx === lines - 1 ? "70%" : "100%"}
/>
))}
</div>
);
};
export default Skeleton;

View File

@ -1,64 +1,39 @@
import ThreeDScrollTriggerRow, { import ThreeDScrollTriggerRow, {
ThreeDScrollTriggerContainer, ThreeDScrollTriggerContainer,
} from "../lightswind/ThreeDScrollTrigger"; } from "../lightswind/ThreeDScrollTrigger";
import React, { useState } from "react"; import React, { memo, useCallback, useMemo, useState } from "react";
import { useReviews } from "../../hooks/useReviews";
const reviews = [ import { Skeleton } from "./Skeleton";
{
name: "Aria Thompson",
company: "CreativePixel Studio",
image:
"https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=facearea&w=96&h=96&facepad=2",
review:
"Lightswind UI has completely changed how we design modern interfaces. Every component feels elegant, responsive, and intuitive.",
},
{
name: "Ethan Rivera",
company: "CodeLoom Technologies",
image:
"https://images.unsplash.com/photo-1465101162946-4377e57745c3?auto=format&fit=facearea&w=96&h=96&facepad=2",
review:
"The customization options in Lightswind UI are unmatched. We built an entire SaaS dashboard in days instead of weeks.",
},
{
name: "Liam Patel",
company: "NextGen Interfaces",
image:
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=facearea&w=96&h=96&facepad=2",
review:
"From landing pages to complex dashboards, Lightswind UI makes building visually consistent UIs effortless and fun.",
},
{
name: "Sarah Mitchell",
company: "DesignFlow Co",
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=facearea&w=96&h=96&facepad=2",
review:
"The component library is extensive and well-documented. Saved us months of development time.",
},
{
name: "James Chen",
company: "TechVision Labs",
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=facearea&w=96&h=96&facepad=2",
review:
"Outstanding support and continuous updates. Lightswind UI keeps getting better every release.",
},
{
name: "Emma Davis",
company: "PixelPerfect Studios",
image:
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=facearea&w=96&h=96&facepad=2",
review:
"Responsive by default and looks amazing on all devices. This is exactly what we needed.",
},
];
export default function ThreeScrollTrigger() { export default function ThreeScrollTrigger() {
// Split reviews into 2 rows with 3 cards each const { data, isLoading } = useReviews();
const row1 = reviews.slice(0, 3);
const row2 = reviews.slice(3, 6);
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
const handlePause = useCallback(() => setPaused(true), []);
const handleResume = useCallback(() => setPaused(false), []);
const reviews = useMemo(() => {
const list = data?.data || [];
if (!Array.isArray(list)) return [];
return list.map((item) => ({
name: item.name,
company: item.about,
image: item.profile_image,
review: item.content,
rating: item.rating,
}));
}, [data]);
const row1 = useMemo(() => reviews.slice(0, 3), [reviews]);
const row2 = useMemo(() => reviews.slice(3, 6), [reviews]);
const row3 = useMemo(() => reviews.slice(6, 9), [reviews]);
if (!isLoading && reviews.length === 0) {
return (
<div className="w-full mx-auto max-w-[1400px] py-10 text-center text-gray-600">
No reviews available.
</div>
);
}
return ( return (
<div className="w-full mx-auto max-w-[1400px] overflow-hidden"> <div className="w-full mx-auto max-w-[1400px] overflow-hidden">
@ -66,44 +41,75 @@ export default function ThreeScrollTrigger() {
{/* Row 1 - 3 Cards scrolling right */} {/* Row 1 - 3 Cards scrolling right */}
<ThreeDScrollTriggerRow baseVelocity={2} direction={1} paused={paused}> <ThreeDScrollTriggerRow baseVelocity={2} direction={1} paused={paused}>
<div className="flex flex-row gap-6 px-3 py-1"> <div className="flex flex-row gap-6 px-3 py-1">
{row1.map((rev, idx) => ( {isLoading
<ReviewCard ? Array.from({ length: 3 }).map((_, idx) => (
key={idx} <ReviewSkeleton key={`row1-skel-${idx}`} />
{...rev} ))
onMouseEnter={() => setPaused(true)} : row1.map((rev, idx) => (
onMouseLeave={() => setPaused(false)} <ReviewCard
/> key={`${rev.name}-${idx}`}
))} {...rev}
onMouseEnter={handlePause}
onMouseLeave={handleResume}
/>
))}
</div> </div>
</ThreeDScrollTriggerRow> </ThreeDScrollTriggerRow>
{/* Row 2 - 3 Cards scrolling left */} {/* Row 2 - 3 Cards scrolling left */}
<ThreeDScrollTriggerRow baseVelocity={2} direction={-1} paused={paused}> <ThreeDScrollTriggerRow baseVelocity={2} direction={-1} paused={paused}>
<div className="flex flex-row gap-6 px-3 py-1"> <div className="flex flex-row gap-6 px-3 py-1">
{row2.map((rev, idx) => ( {isLoading
<ReviewCard ? Array.from({ length: 3 }).map((_, idx) => (
key={idx} <ReviewSkeleton key={`row2-skel-${idx}`} />
{...rev} ))
onMouseEnter={() => setPaused(true)} : row2.map((rev, idx) => (
onMouseLeave={() => setPaused(false)} <ReviewCard
/> key={`${rev.name}-${idx}`}
))} {...rev}
onMouseEnter={handlePause}
onMouseLeave={handleResume}
/>
))}
</div> </div>
</ThreeDScrollTriggerRow> </ThreeDScrollTriggerRow>
{row3.length > 0 && (
<ThreeDScrollTriggerRow baseVelocity={2} direction={1} paused={paused}>
<div className="flex flex-row gap-6 px-3 py-1">
{row3.map((rev, idx) => (
<ReviewCard
key={`${rev.name}-${idx}`}
{...rev}
onMouseEnter={handlePause}
onMouseLeave={handleResume}
/>
))}
</div>
</ThreeDScrollTriggerRow>
)}
</ThreeDScrollTriggerContainer> </ThreeDScrollTriggerContainer>
</div> </div>
); );
} }
// Card component for clarity and reuse // Card component for clarity and reuse
function ReviewCard({ const ReviewCard = memo(function ReviewCard({
name, name,
company, company,
image, image,
review, review,
rating,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
}) { }) {
const stars = useMemo(() => {
if (!rating) return null;
return Array.from({ length: 5 }, (_, i) => (
<span key={i} className="text-[24px] ">{i < rating ? "★" : "☆"}</span>
));
}, [rating]);
return ( return (
<div <div
className="bg-[#fbfbfb] rounded-xl shadow-md p-6 w-80 min-w-80 flex-shrink-0 hover:shadow-lg transition-shadow" className="bg-[#fbfbfb] rounded-xl shadow-md p-6 w-80 min-w-80 flex-shrink-0 hover:shadow-lg transition-shadow"
@ -111,15 +117,20 @@ function ReviewCard({
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<img {image ? (
src={image} <img
alt={name} src={image}
className="w-12 h-12 rounded-full object-cover flex-shrink-0" alt={name}
loading="lazy" className="w-12 h-12 rounded-full object-cover flex-shrink-0"
/> loading="lazy"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex-shrink-0" />
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 truncate">{name}</div> <div className="font-semibold text-gray-900 truncate">{name}</div>
<div className="text-xs text-gray-500 truncate">{company}</div> <div className="text-xs text-gray-500 truncate">{company}</div>
{stars && <div className="text-[11px] text-amber-500">{stars}</div>}
</div> </div>
</div> </div>
<div className="text-sm text-gray-700 line-clamp-4"> <div className="text-sm text-gray-700 line-clamp-4">
@ -127,4 +138,21 @@ function ReviewCard({
</div> </div>
</div> </div>
); );
});
function ReviewSkeleton() {
return (
<div className="bg-[#fbfbfb] rounded-xl shadow-md p-6 w-80 min-w-80 flex-shrink-0">
<div className="flex items-center gap-3 mb-4">
<Skeleton width={48} height={48} rounded={999} />
<div className="flex-1 space-y-2">
<Skeleton width="60%" height={12} />
<Skeleton width="40%" height={10} />
</div>
</div>
<Skeleton width="100%" height={10} />
<Skeleton width="90%" height={10} className="mt-2" />
<Skeleton width="70%" height={10} className="mt-2" />
</div>
);
} }

View File

@ -103,66 +103,53 @@ const DailyRecommendedCard = () => {
} }
]; ];
// Profile Card Component const ProfileCard = ({ profile }) => {
const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false); const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
return (
<motion.div return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
onClick={() => navigate(`/profile-details/${profile.id}`)}
onClick={() => navigate(`/profile-details/${profile.id}`)} className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200"
>
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200">
{/* Profile Image Section */}
<div className="relative"> <div className="relative">
{profile.isPremium && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring' }}
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
)}
{/* Premium Badge */} <motion.button
{profile.isPremium && ( whileHover={{ scale: 1 }}
<motion.div whileTap={{ scale: 0.9 }}
initial={{ scale: 0 }} className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
animate={{ scale: 1 }} onClick={(e) => {
transition={{ delay: 0.2, type: 'spring' }} e.stopPropagation();
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg" }}
> >
<Crown className="w-5 h-5 text-white" /> <Bookmark className="w-4 h-4" />
</motion.div> <span className="text-[12px] font-medium">Shortlist</span>
)} </motion.button>
{/* Shortlist Button */} <div
<motion.button className=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
whileHover={{ scale: 1 }} style={{ height: "300px" }}
whileTap={{ scale: 0.9 }} >
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors" <img
onClick={(e) => { src={profile.image}
e.stopPropagation(); alt={profile.name}
// shortlist logic className="w-full h-full object-cover"
}} />
> </div>
<Bookmark className="w-4 h-4" />
<span className="text-[12px] font-medium">Shortlist</span>
</motion.button>
<div classname=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]" style={{height:"300px"}}>
<img
src={profile.image}
alt={profile.name}
className="w-full h-full object-cover"
/>
</div>
{/* <LazyImage
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=600&h=800&fit=crop&crop=faces,top"
alt="Profile"
className="w-full h-90 object-cover"
/> */}
{/* White Gradient Overlay at bottom of image */}
<div <div
className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none" className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none"
style={{ style={{
@ -171,115 +158,127 @@ className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 bor
}} }}
></div> ></div>
{/* Profile Info Overlay - positioned at bottom */}
<div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900"> <div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-2"> <h1 className="text-[18px] text-green-900 font-bold mb-2">
{profile.name} {profile.name}
</h1> </h1>
<p className="text-[14px] text-gray-700 leading-relaxed"> <p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: {profile.userId} Matrimony ID: {profile.userId}
</p> </p>
</div> </div>
</div> </div>
{/* Stats and Follow Section */}
<div <div
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2 " className="px-4 pt-[-2px] pb-4 flex flex-col gap-2"
style={{ style={{ background: "rgb(255, 255, 255)" }}
background: "rgb(255, 255, 255)",
}}
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CakeIcon className="w-4 h-4 text-gray-700" /> <CakeIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
Age : {profile.age} Age : {profile.age}
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" /> <AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
Height: {profile.height} Height: {profile.height}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GroupsIcon className="w-4 h-4 text-gray-700" /> <GroupsIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
{profile.religion} {profile.religion}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SchoolIcon className="w-4 h-4 text-gray-700" /> <SchoolIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
{profile.education} {profile.education}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" /> <LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
{profile.location} {profile.location}
</span> </span>
</div> </div>
</div> </div>
{/* Action Buttons */} <div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<div className="flex gap-3 my-2 justify-between w-full px-[0px]"> <button
<button onClick={(e) => {
onClick={(e) => { e.stopPropagation();
e.stopPropagation(); }}
// your decline logic className="gap-2 px-3 w-[fit-content] bg-[#A70710] hover:bg-red-600 text-white
}} font-semibold text-base py-2 rounded-[20px] shadow-md
className="gap-2 px-3 w-[fit-content] bg-[#A70710] hover:bg-red-600 text-white hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95"
font-semibold text-base py-2 rounded-[20px] shadow-md >
hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95"> <svg
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> className="w-4 h-4"
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round"/> viewBox="0 0 24 24"
</svg> fill="none"
Decline stroke="currentColor"
</button> strokeWidth="2"
>
<path
d="M18 6L6 18M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Decline
</button>
<button <button
className="w-[fit-content] bg-[#034E08] hover:bg-green-700 text-white font-semibold text-base className="w-[fit-content] bg-[#034E08] hover:bg-green-700 text-white font-semibold text-base
rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300 rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300
transform hover:scale-105 flex items-center justify-center" transform hover:scale-105 flex items-center justify-center"
onClick={(e) =>{ onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setIsLiked(!isLiked); setIsLiked(!isLiked);
} } }}
> >
{isLiked ? ( {isLiked ? (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"> <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#EF4444"/> <path
</svg> d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
) : ( fill="#EF4444"
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> />
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/> </svg>
</svg> ) : (
)} <svg
className="w-4 h-4"
Interest viewBox="0 0 24 24"
</button> fill="none"
</div> stroke="currentColor"
strokeWidth="2"
>
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
Interest
</button>
</div>
</div> </div>
</motion.div> </motion.div>
); );
}; };
return ( return (
<div className=" py-10"> <div className=" py-10">
<div className="w-[100%] max-w-[1400px] mx-auto"> <div className="w-[100%] max-w-[1400px] mx-auto">

View File

@ -16,11 +16,13 @@ import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import 'swiper/css/pagination'; import 'swiper/css/pagination';
import 'swiper/css/effect-coverflow'; import 'swiper/css/effect-coverflow';
import { useNavigate } from 'react-router-dom';
const NewJoinedProfile = () => { const NewJoinedProfile = () => {
const swiperRef = useRef(null); const swiperRef = useRef(null);
const navigate = useNavigate();
// Sample profile data // Sample profile data
const profiles = [ const profiles = [
@ -111,123 +113,173 @@ const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false); const [isLiked, setIsLiked] = useState(false);
return ( return (
<motion.div
<motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-2 border-gray-200"> className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200"
{/* Profile Image Section */}
<div className="relative">
{/* Premium Badge */}
{profile.isPremium && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring' }}
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
)}
{/* Shortlist Button */}
<motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
> >
<Bookmark className="w-4 h-4" /> <div className="relative">
<span className="text-[12px] font-medium">Shortlist</span> {profile.isPremium && (
</motion.button> <motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring' }}
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
)}
<motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
onClick={(e) => {
e.stopPropagation();
}}
>
<Bookmark className="w-4 h-4" />
<span className="text-[12px] font-medium">Shortlist</span>
</motion.button>
<img
src={profile.image}
alt={profile.name}
className="w-full h-65 object-cover"
/>
{/* <LazyImage
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=600&h=800&fit=crop&crop=faces,top"
alt="Profile"
className="w-full h-90 object-cover"
/> */}
{/* White Gradient Overlay at bottom of image */}
<div <div
className="absolute bottom-0 left-0 right-0 h-25 pointer-events-none" className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
style={{ height: "300px" }}
>
<img
src={profile.image}
alt={profile.name}
className="w-full h-full object-cover"
/>
</div>
<div
className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none"
style={{ style={{
background: background:
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.75) 50%, rgb(255, 255, 255) 100%)", "linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
}} }}
></div> ></div>
{/* Profile Info Overlay - positioned at bottom */} <div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900">
<div className="absolute bottom-0 left-0 right-0 p-6 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-2"> <h1 className="text-[18px] text-green-900 font-bold mb-2">
{profile.name} {profile.name}
</h1> </h1>
<p className="text-[14px] text-gray-700 leading-relaxed"> <p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: {profile.userId} Matrimony ID: {profile.userId}
</p> </p>
</div> </div>
</div> </div>
{/* Stats and Follow Section */}
<div <div
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2 " className="px-4 pt-[-2px] pb-4 flex flex-col gap-2"
style={{ style={{ background: "rgb(255, 255, 255)" }}
background: "rgb(255, 255, 255)",
}}
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CakeIcon className="w-4 h-4 text-gray-700" /> <CakeIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
Age : {profile.age} Age : {profile.age}
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" /> <AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
Height: {profile.height} Height: {profile.height}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GroupsIcon className="w-4 h-4 text-gray-700" /> <GroupsIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
{profile.religion} {profile.religion}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SchoolIcon className="w-4 h-4 text-gray-700" /> <SchoolIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
{profile.education} {profile.education}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" /> <LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-600 text-gray-900">
{profile.location} {profile.location}
</span> </span>
</div> </div>
</div> </div>
<div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<button
onClick={(e) => {
e.stopPropagation();
}}
className="gap-2 px-3 w-[fit-content] bg-[#A70710] hover:bg-red-600 text-white
font-semibold text-base py-2 rounded-[20px] shadow-md
hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95"
>
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path
d="M18 6L6 18M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Decline
</button>
<button
className="w-[fit-content] bg-[#034E08] hover:bg-green-700 text-white font-semibold text-base
rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300
transform hover:scale-105 flex items-center justify-center"
onClick={(e) => {
e.stopPropagation();
setIsLiked(!isLiked);
}}
>
{isLiked ? (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
fill="#EF4444"
/>
</svg>
) : (
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
Interest
</button>
</div>
</div> </div>
</motion.div> </motion.div>

View File

@ -0,0 +1,235 @@
import { useState } from "react";
import { motion } from "framer-motion";
import { Crown, Bookmark } from "lucide-react";
import { useNavigate } from "react-router-dom";
import CakeIcon from "@mui/icons-material/Cake";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import GroupsIcon from "@mui/icons-material/Groups";
import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
const buildDefaultRows = (profile) => [
[
{
icon: <CakeIcon className="w-4 h-4 text-gray-700" />,
text: `Age: ${profile?.age ?? "-"}`,
},
{
icon: <AccessibilityNewIcon className="w-4 h-4 text-gray-700" />,
text: `Height: ${profile?.height ?? "-"}`,
},
],
[
{
icon: <GroupsIcon className="w-4 h-4 text-gray-700" />,
text:
profile?.religion ||
profile?.caste ||
profile?.community ||
"-",
},
],
[
{
icon: <SchoolIcon className="w-4 h-4 text-gray-700" />,
text: profile?.education || profile?.qualification || "-",
},
],
[
{
icon: <LocationOnIcon className="w-4 h-4 text-gray-700" />,
text: profile?.location || "-",
},
],
];
const getProfileIdText = (profile) =>
profile?.userId ||
profile?.idNumber ||
profile?.matrimonyId ||
profile?.id ||
"-";
const ProfileCardItem = ({
profile,
metaRows,
showActions = true,
showShortlist = true,
shortlistLabel = "Shortlist",
declineLabel = "Decline",
interestLabel = "Interest",
onDecline,
onInterest,
onShortlist,
onCardClick,
detailsPath,
className = "",
}) => {
const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
const rows = metaRows?.length ? metaRows : buildDefaultRows(profile);
const handleCardClick = () => {
if (onCardClick) {
onCardClick(profile);
return;
}
const target = detailsPath || `/profile-details/${profile?.id ?? ""}`;
if (target) {
navigate(target);
}
};
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
onClick={handleCardClick}
className={`w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200 ${className}`}
>
<div className="relative">
{profile?.isPremium && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
)}
{showShortlist && (
<motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
onClick={(e) => {
e.stopPropagation();
onShortlist?.(profile);
}}
>
<Bookmark className="w-4 h-4" />
<span className="text-[12px] font-medium">{shortlistLabel}</span>
</motion.button>
)}
<div
className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
style={{ height: "300px" }}
>
<img
src={profile?.image}
alt={profile?.name || "Profile"}
className="w-full h-full object-cover"
loading="lazy"
decoding="async"
/>
</div>
<div
className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none"
style={{
background:
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
}}
></div>
<div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-2">
{profile?.name || "-"}
</h1>
<p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: {getProfileIdText(profile)}
</p>
</div>
</div>
<div
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2"
style={{ background: "rgb(255, 255, 255)" }}
>
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="flex items-center gap-4">
{row.map((item, itemIndex) => (
<div key={itemIndex} className="flex items-center gap-2">
{item.icon}
<span className="text-[14px] font-600 text-gray-900">
{item.text}
</span>
</div>
))}
</div>
))}
{showActions && (
<div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<button
onClick={(e) => {
e.stopPropagation();
onDecline?.(profile);
}}
className="gap-2 px-3 w-[fit-content] bg-[#A70710] hover:bg-red-600 text-white
font-semibold text-base py-2 rounded-[20px] shadow-md
hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95"
>
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path
d="M18 6L6 18M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{declineLabel}
</button>
<button
className="w-[fit-content] bg-[#034E08] hover:bg-green-700 text-white font-semibold text-base
rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300
transform hover:scale-105 flex items-center justify-center"
onClick={(e) => {
e.stopPropagation();
setIsLiked((prev) => !prev);
onInterest?.(profile);
}}
>
{isLiked ? (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
fill="#EF4444"
/>
</svg>
) : (
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{interestLabel}
</button>
</div>
)}
</div>
</motion.div>
);
};
export default ProfileCardItem;

View File

@ -1,11 +1,17 @@
import React from "react"; import React from "react";
import { Heart, X, Crown, Bookmark, Eye } from "lucide-react";
// Import your images // Import your images
import Profile1 from "../../assets/images/bride1.jpg"; import Profile1 from "../../assets/images/bride1.jpg";
import Profile2 from "../../assets/images/bride2.jpg"; import Profile2 from "../../assets/images/bride2.jpg";
import Profile3 from "../../assets/images/bride3.jpg"; import Profile3 from "../../assets/images/bride3.jpg";
import Profile4 from "../../assets/images/bride4.jpg"; import Profile4 from "../../assets/images/bride4.jpg";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import ProfileCardItem from "../profiledashboard/ProfileCardItem";
import CakeIcon from "@mui/icons-material/Cake";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import GroupsIcon from "@mui/icons-material/Groups";
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
export default function ProfileCard() { export default function ProfileCard() {
@ -69,96 +75,75 @@ export default function ProfileCard() {
}, },
]; ];
return ( const buildProfileRows = (profile) => [
<div className="h-auto py-8 px-4"> [
{
icon: <CakeIcon className="w-4 h-4 text-gray-700" />,
text: profile?.age || "-",
},
{
icon: <AccessibilityNewIcon className="w-4 h-4 text-gray-700" />,
text: profile?.height || "-",
},
],
[
{
icon: <AccountBalanceWalletIcon className="w-4 h-4 text-gray-700" />,
text: profile?.salary ? `${profile.salary} LPA` : "-",
},
{
icon: <LocationOnIcon className="w-4 h-4 text-gray-700" />,
text: profile?.location || "-",
},
],
[
{
icon: <GroupsIcon className="w-4 h-4 text-gray-700" />,
text: profile?.caste || "-",
},
{
icon: <AutoAwesomeIcon className="w-4 h-4 text-gray-700" />,
text: profile?.zodiac1 || "-",
},
],
[
{
icon: <AutoAwesomeIcon className="w-4 h-4 text-gray-700" />,
text: profile?.zodiac2 || "-",
},
],
];
{/* HEADING */} return (
<motion.div <div className="h-auto py-8 px-4">
initial={{ opacity: 0, y: -20 }} {/* HEADING */}
animate={{ opacity: 1, y: 0 }} <motion.div
transition={{ duration: 0.5 }} initial={{ opacity: 0, y: -20 }}
className="text-center mb-12" animate={{ opacity: 1, y: 0 }}
> transition={{ duration: 0.5 }}
<h1 className="text-[20px] text-[#000000] sm:text-[22px] lg:text-[24px] font-semibold mb-3"> className="text-center mb-12"
Daily Recommended >
</h1> <h1 className="text-[20px] text-[#000000] sm:text-[22px] lg:text-[24px] font-semibold mb-3">
<p className="text-gray-900 text-[12px]"> Daily Recommended
Find your perfect match today </h1>
</p> <p className="text-gray-900 text-[12px]">
</motion.div> Find your perfect match today
</p>
</motion.div>
{/* CARDS GRID */} {/* CARDS GRID */}
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full max-w-[1400px] grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="w-full max-w-[1400px] grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{profiles.map((profile) => ( {profiles.map((profile) => (
<div <ProfileCardItem
key={profile.id} key={profile.id}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md" profile={profile}
> metaRows={buildProfileRows(profile)}
{/* IMAGE SECTION */} />
<div className="relative"> ))}
<img </div>
src={profile.image}
alt="profile"
className="w-full h-[320px] object-cover"
/>
<div className="absolute top-4 left-4 w-9 h-9 rounded-full bg-[#8b0000] flex items-center justify-center">
<Crown size={18} color="#fff" />
</div>
<div className="absolute top-4 right-4 px-3 py-1.5 rounded-[20px] bg-white flex items-center gap-1.5 text-[13px] font-medium shadow-lg">
<Bookmark size={14} /> Shortlist
</div>
</div>
{/* CONTENT */}
<div className="px-4 py-4 -mt-[60px] bg-white/65 backdrop-blur-[25px] rounded-t-[15px] shadow-[0_-10px_30px_rgba(0,0,0,0.15)] relative z-[2]">
<h2 className="text-center text-[22px] font-semibold mb-1">
{profile.name}
</h2>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8">
<p>ID: {profile.idNumber}</p>
<p className="flex items-center gap-0.5">
<Eye size={12} /> {profile.lastSeen}
</p>
</div>
<div className="flex flex-wrap justify-center gap-2">
{[
profile.age,
profile.height,
profile.salary + " LPA",
profile.location,
profile.caste,
profile.zodiac1,
profile.zodiac2,
].map((v, i) => (
<span
key={i}
className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
>
{v}
</span>
))}
</div>
<div className="flex gap-4 mt-[15px] justify-center">
<button className="px-2 py-1 rounded-[20px] border border-red-200 bg-red-50 flex items-center gap-1.5 font-semibold text-red-900">
<X size={18} /> Decline
</button>
<button className="px-2 py-1 rounded-[20px] border border-green-200 bg-green-50 text-green-900 flex items-center gap-1.5 font-semibold">
<Heart size={18} /> Interest
</button>
</div>
</div>
</div>
))}
</div> </div>
</div> </div>
</div> );
);
} }

View File

@ -1,19 +1,102 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateEducationalDetails } from "../redux/registrationFormSlice"; import { updateEducationalDetails } from "../redux/registrationFormSlice";
import { TextField, Button, Grid } from "@mui/material"; import {
TextField,
Button,
Grid,
FormControl,
InputLabel,
Select,
MenuItem,
} from "@mui/material";
import { useEducationMasters, useEducationList } from "../hooks/useMasters";
const EducationalDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => { const EducationalDetailsForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.educationalDetails); const data = useSelector((state) => state.registerform.educationalDetails);
const inputRef = useRef(null); const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: educationMasters, isLoading: isEducationMastersLoading } =
useEducationMasters();
const educationListQuery = useEducationList(data.fieldOfStudy);
const studyFieldOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.studyFields || raw.study_fields || raw.fieldOfStudy || [];
}, [educationMasters]);
const qualificationOptions = useMemo(() => {
const raw = educationListQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.education || raw.data || [];
}, [educationListQuery.data]);
const occupationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [educationMasters]);
const employeeTypeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.employeeType || raw.employee_type || [];
}, [educationMasters]);
const annualIncomeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.annualIncome || raw.annual_income || [];
}, [educationMasters]);
const workLocationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.workLocation || raw.work_location || raw.workLocations || [];
}, [educationMasters]);
const getOptionLabel = (item, fallback = "") => {
if (!item) return fallback;
if (typeof item === "string") return item;
return (
item.study_field_name ||
item.education_name ||
item.occupation_name ||
item.employee_type_name ||
item.annual_income_name ||
item.work_location_name ||
item.name ||
fallback
);
};
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updateEducationalDetails({ [field]: value })); const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "fieldOfStudy") {
updates.qualification = "";
fieldsToClear.push("qualification");
}
dispatch(updateEducationalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
}; };
const handleSubmit = () => { const handleSubmit = () => {
@ -26,58 +109,168 @@ const EducationalDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
<div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
{/* Highest Qualification */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Highest Qualification</label>
<TextField
fullWidth
inputRef={inputRef}
name="qualification"
label="Highest Qualification"
value={data.qualification}
onChange={(e) => handleChange("qualification", e.target.value)}
error={Boolean(errors.qualification)}
helperText={errors.qualification}
placeholder="Enter Highest Qualification"
variant="outlined"
/>
</div>
{/* Field of Study */} {/* Field of Study */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Field of Study</label> <label className="text-gray-900 text-[15px]">
Field of Study{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.fieldOfStudy)}
>
<InputLabel id="fieldOfStudy-label">
Select Field of Study
</InputLabel>
<Select
labelId="fieldOfStudy-label"
label="Select Field of Study"
name="fieldOfStudy"
value={data.fieldOfStudy}
onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
inputRef={inputRef}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{studyFieldOptions.map((field) => (
<MenuItem key={field.id ?? field} value={field.id ?? field}>
{getOptionLabel(field, "Field of Study")}
</MenuItem>
))}
</Select>
{errors.fieldOfStudy && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.fieldOfStudy}
</p>
)}
</FormControl>
</div>
{/* Highest Qualification */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Highest Educational Qualification{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.qualification)}
>
<InputLabel id="qualification-label">
Select Highest Qualification
</InputLabel>
<Select
labelId="qualification-label"
label="Select Highest Qualification"
name="qualification"
value={data.qualification}
onChange={(e) => handleChange("qualification", e.target.value)}
disabled={
!data.fieldOfStudy || educationListQuery.isLoading
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{qualificationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Qualification")}
</MenuItem>
))}
</Select>
{errors.qualification && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.qualification}
</p>
)}
</FormControl>
</div>
{/* College Name */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
College Name
</label>
<TextField <TextField
fullWidth fullWidth
name="fieldOfStudy" name="collegeName"
label="Field of Study" label="College Name"
value={data.fieldOfStudy} value={data.collegeName}
onChange={(e) => handleChange("fieldOfStudy", e.target.value)} onChange={(e) => handleChange("collegeName", e.target.value)}
error={Boolean(errors.fieldOfStudy)} error={Boolean(errors.collegeName)}
helperText={errors.fieldOfStudy} helperText={errors.collegeName}
placeholder="Enter Field of Study" placeholder="Enter College Name"
variant="outlined" variant="outlined"
/> />
</div> </div>
{/* Occupation */} {/* Occupation */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Occupation</label> <label className="text-gray-900 text-[15px]">
<TextField Occupation{requiredMark}
</label>
<FormControl
fullWidth fullWidth
name="occupation"
label="Occupation"
value={data.occupation}
onChange={(e) => handleChange("occupation", e.target.value)}
error={Boolean(errors.occupation)}
helperText={errors.occupation}
placeholder="Enter Occupation"
variant="outlined" variant="outlined"
/> error={Boolean(errors.occupation)}
>
<InputLabel id="occupation-label">Select Occupation</InputLabel>
<Select
labelId="occupation-label"
label="Select Occupation"
name="occupation"
value={data.occupation}
onChange={(e) => handleChange("occupation", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{occupationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Occupation")}
</MenuItem>
))}
</Select>
{errors.occupation && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.occupation}
</p>
)}
</FormControl>
</div> </div>
{/* Company / Organization Name */} {/* Company / Organization Name */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Company / Organization Name</label> <label className="text-gray-900 text-[15px]">
Company / Organization Name{requiredMark}
</label>
<TextField <TextField
fullWidth fullWidth
name="organization" name="organization"
@ -91,36 +284,156 @@ const EducationalDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
/> />
</div> </div>
{/* Employee Type */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Employee Type{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.employeeType)}
>
<InputLabel id="employeeType-label">
Select Employee Type
</InputLabel>
<Select
labelId="employeeType-label"
label="Select Employee Type"
name="employeeType"
value={data.employeeType}
onChange={(e) => handleChange("employeeType", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{employeeTypeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Employee Type")}
</MenuItem>
))}
</Select>
{errors.employeeType && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.employeeType}
</p>
)}
</FormControl>
</div>
{/* Annual Income */} {/* Annual Income */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Annual Income</label> <label className="text-gray-900 text-[15px]">
<TextField Annual Income{requiredMark}
</label>
<FormControl
fullWidth fullWidth
name="income"
label="Annual Income"
value={data.income}
onChange={(e) => handleChange("income", e.target.value)}
error={Boolean(errors.income)}
helperText={errors.income}
placeholder="Enter Annual Income"
variant="outlined" variant="outlined"
/> error={Boolean(errors.income)}
>
<InputLabel id="income-label">Select Annual Income</InputLabel>
<Select
labelId="income-label"
label="Select Annual Income"
name="income"
value={data.income}
onChange={(e) => handleChange("income", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{annualIncomeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Annual Income")}
</MenuItem>
))}
</Select>
{errors.income && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.income}
</p>
)}
</FormControl>
</div> </div>
{/* Work Location */} {/* Work Location */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Work Location</label> <label className="text-gray-900 text-[15px]">
<TextField Work Location
fullWidth </label>
name="workLocation" {workLocationOptions.length > 0 ? (
label="Work Location" <FormControl
value={data.workLocation} fullWidth
onChange={(e) => handleChange("workLocation", e.target.value)} variant="outlined"
error={Boolean(errors.workLocation)} error={Boolean(errors.workLocation)}
helperText={errors.workLocation} >
placeholder="Enter Work Location" <InputLabel id="workLocation-label">
variant="outlined" Select Work Location
/> </InputLabel>
<Select
labelId="workLocation-label"
label="Select Work Location"
name="workLocation"
value={data.workLocation}
onChange={(e) =>
handleChange("workLocation", e.target.value)
}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{workLocationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Work Location")}
</MenuItem>
))}
</Select>
{errors.workLocation && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.workLocation}
</p>
)}
</FormControl>
) : (
<TextField
fullWidth
name="workLocation"
label="Work Location"
value={data.workLocation}
onChange={(e) => handleChange("workLocation", e.target.value)}
error={Boolean(errors.workLocation)}
helperText={errors.workLocation}
placeholder="Enter Work Location"
variant="outlined"
/>
)}
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateFamilyDetails } from "../redux/registrationFormSlice"; import { updateFamilyDetails } from "../redux/registrationFormSlice";
import { import {
@ -9,19 +9,91 @@ import {
Select, Select,
MenuItem, MenuItem,
Button, Button,
Box,
} from "@mui/material"; } from "@mui/material";
import { useFamilyMasters } from "../hooks/useMasters";
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => { const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.familyDetails); const data = useSelector((state) => state.registerform.familyDetails);
const inputRef = useRef(null); const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: familyMasters, isLoading: isFamilyMastersLoading } =
useFamilyMasters();
const occupationOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [familyMasters]);
const maritalStatusOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.maritalStatus || raw.marital_status || [];
}, [familyMasters]);
const familyStatusOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.familyStatus || raw.family_status || [];
}, [familyMasters]);
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
const createSibling = () => ({
name: "",
occupation: "",
maritalStatus: "",
haveChildrens: "",
});
const syncSiblingArray = (arr, count) => {
const target = Math.max(0, Number(count) || 0);
const next = [...(arr || [])];
while (next.length < target) {
next.push(createSibling());
}
if (next.length > target) {
next.length = target;
}
return next;
};
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updateFamilyDetails({ [field]: value })); const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "brotherCount") {
const count = Number(value) || 0;
updates.brotherCount = count;
updates.brothers = syncSiblingArray(data.brothers, count);
fieldsToClear.push("brothers");
}
if (field === "sisterCount") {
const count = Number(value) || 0;
updates.sisterCount = count;
updates.sisters = syncSiblingArray(data.sisters, count);
fieldsToClear.push("sisters");
}
dispatch(updateFamilyDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
};
const handleSiblingChange = (type, index, field, value) => {
const list = [...(data[type] || [])];
if (!list[index]) list[index] = createSibling();
list[index] = { ...list[index], [field]: value };
dispatch(updateFamilyDetails({ [type]: list }));
if (onFieldChange) onFieldChange(type);
}; };
const handleSubmit = () => { const handleSubmit = () => {
@ -29,155 +101,329 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
onSubmitStep(); onSubmitStep();
}; };
return ( const countOptions = Array.from({ length: 11 }, (_, i) => i);
<> const renderSiblingCard = (type, index) => {
const sibling = (data[type] || [])[index] || createSibling();
<div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8"> const labelPrefix = type === "brothers" ? "Brother" : "Sister";
<form noValidate autoComplete="off" style={{ padding: 16 }}> return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <Box
{/* Highest Qualification */} key={`${type}-${index}`}
<div className="flex flex-col gap-4"> sx={{
<label className="text-gray-900 text-[15px]">Enter Father Name</label> border: "1px solid #e5e7eb",
<TextField borderRadius: 2,
padding: 2,
backgroundColor: "#fff",
}}
>
<div className="text-gray-900 text-[14px] font-semibold mb-3">
{labelPrefix} {index + 1}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<TextField
fullWidth fullWidth
inputRef={inputRef} label="Name"
name="fatherName" value={sibling.name}
label="Father Name" onChange={(e) =>
value={data.fatherName} handleSiblingChange(type, index, "name", e.target.value)
onChange={(e) => handleChange("fatherName", e.target.value)} }
error={Boolean(errors.fatherName)}
helperText={errors.fatherName}
placeholder="Enter Father Name"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Father Occupation</label>
<TextField
fullWidth
name="fatherOccupation"
label="Father Occupation"
value={data.fatherOccupation}
onChange={(e) => handleChange("fatherOccupation", e.target.value)}
error={Boolean(errors.fatherOccupation)}
helperText={errors.fatherOccupation}
placeholder="Enter Father Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Mother Name</label>
<TextField
fullWidth
name="motherName"
label="Mother Name"
value={data.motherName}
onChange={(e) => handleChange("motherName", e.target.value)}
error={Boolean(errors.motherName)}
helperText={errors.motherName}
placeholder="Enter Mother Name"
variant="outlined" variant="outlined"
/> />
</div> <FormControl fullWidth variant="outlined">
<div className="flex flex-col gap-4"> <InputLabel id={`${type}-${index}-occupation-label`}>
<label className="text-gray-900 text-[15px]">Enter Mother Occupation</label> Occupation
<TextField
fullWidth
name="motherOccupation"
label="Mother Occupation"
value={data.motherOccupation}
onChange={(e) => handleChange("motherOccupation", e.target.value)}
error={Boolean(errors.motherOccupation)}
helperText={errors.motherOccupation}
placeholder="Enter Mother Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Enter Number of Brothers / Sisters</label>
<TextField
fullWidth
name="siblings"
label="Number of Brothers / Sisters"
value={data.siblings}
onChange={(e) => handleChange("siblings", e.target.value)}
error={Boolean(errors.siblings)}
helperText={errors.siblings}
placeholder="Enter Number of Brothers / Sisters"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]"> Brothers / Sisters (Married / Unmarried)</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.siblingsStatus)}
>
<InputLabel id="siblingsStatus-label">
Brothers / Sisters (Married / Unmarried)
</InputLabel> </InputLabel>
<Select <Select
labelId="siblingsStatus-label" labelId={`${type}-${index}-occupation-label`}
label="Brothers / Sisters (Married / Unmarried)" label="Occupation"
name="siblingsStatus" value={sibling.occupation}
value={data.siblingsStatus} onChange={(e) =>
onChange={(e) => handleChange("siblingsStatus", e.target.value)} handleSiblingChange(type, index, "occupation", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{occupationOptions.map((opt) => (
<MenuItem value="All Married">All Married</MenuItem> <MenuItem key={opt} value={opt}>
<MenuItem value="All Unmarried">All Unmarried</MenuItem> {opt}
<MenuItem value="Mixed">Mixed</MenuItem> </MenuItem>
))}
</Select> </Select>
{errors.siblingsStatus && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.siblingsStatus}
</p>
)}
</FormControl> </FormControl>
</div> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-marital-label`}>
Marital Status
</InputLabel>
<Select
labelId={`${type}-${index}-marital-label`}
label="Marital Status"
value={sibling.maritalStatus}
onChange={(e) =>
handleSiblingChange(type, index, "maritalStatus", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{maritalStatusOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-children-label`}>
Have Children
</InputLabel>
<Select
labelId={`${type}-${index}-children-label`}
label="Have Children"
value={sibling.haveChildrens}
onChange={(e) =>
handleSiblingChange(
type,
index,
"haveChildrens",
e.target.value
)
}
>
<MenuItem value={1}>Yes</MenuItem>
<MenuItem value={0}>No</MenuItem>
</Select>
</FormControl>
</div>
</Box>
);
};
return (
<div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Father Name{requiredMark}
</label>
<TextField
fullWidth
inputRef={inputRef}
name="fatherName"
label="Father Name"
value={data.fatherName}
onChange={(e) => handleChange("fatherName", e.target.value)}
error={Boolean(errors.fatherName)}
helperText={errors.fatherName}
placeholder="Enter Father Name"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Father Occupation
</label>
<TextField
fullWidth
name="fatherOccupation"
label="Father Occupation"
value={data.fatherOccupation}
onChange={(e) => handleChange("fatherOccupation", e.target.value)}
error={Boolean(errors.fatherOccupation)}
helperText={errors.fatherOccupation}
placeholder="Enter Father Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Mother Name{requiredMark}
</label>
<TextField
fullWidth
name="motherName"
label="Mother Name"
value={data.motherName}
onChange={(e) => handleChange("motherName", e.target.value)}
error={Boolean(errors.motherName)}
helperText={errors.motherName}
placeholder="Enter Mother Name"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Mother Occupation
</label>
<TextField
fullWidth
name="motherOccupation"
label="Mother Occupation"
value={data.motherOccupation}
onChange={(e) => handleChange("motherOccupation", e.target.value)}
error={Boolean(errors.motherOccupation)}
helperText={errors.motherOccupation}
placeholder="Enter Mother Occupation"
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Brother Count
</label>
<FormControl fullWidth variant="outlined">
<InputLabel id="brotherCount-label">Select Brother Count</InputLabel>
<Select
labelId="brotherCount-label"
label="Select Brother Count"
name="brotherCount"
value={data.brotherCount}
onChange={(e) => handleChange("brotherCount", e.target.value)}
>
{countOptions.map((count) => (
<MenuItem key={count} value={count}>
{count}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Sister Count
</label>
<FormControl fullWidth variant="outlined">
<InputLabel id="sisterCount-label">Select Sister Count</InputLabel>
<Select
labelId="sisterCount-label"
label="Select Sister Count"
name="sisterCount"
value={data.sisterCount}
onChange={(e) => handleChange("sisterCount", e.target.value)}
>
{countOptions.map((count) => (
<MenuItem key={count} value={count}>
{count}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Family Status{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.familyStatus)}
>
<InputLabel id="familyStatus-label">Select Family Status</InputLabel>
<Select
labelId="familyStatus-label"
label="Select Family Status"
name="familyStatus"
value={data.familyStatus}
onChange={(e) => handleChange("familyStatus", e.target.value)}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{familyStatusOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{item.family_type_name || item.name || item}
</MenuItem>
))}
</Select>
{errors.familyStatus && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.familyStatus}
</p>
)}
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Native Place</label>
<TextField
fullWidth
name="nativePlace"
label="Native Place"
value={data.nativePlace}
onChange={(e) => handleChange("nativePlace", e.target.value)}
error={Boolean(errors.nativePlace)}
helperText={errors.nativePlace}
placeholder="Enter Native Place"
variant="outlined"
/>
</div>
</div>
{Number(data.brotherCount) > 0 && (
<div className="mt-6">
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Brother Details
</div> </div>
<div className="grid grid-cols-1 gap-4">
{Array.from({ length: Number(data.brotherCount) }).map(
(_, index) => renderSiblingCard("brothers", index)
)}
</div>
</div>
)}
<Grid item {Number(data.sisterCount) > 0 && (
xs={12} sx={{ marginTop: 10, display: "flex", gap: 4 , justifyContent: "center"}}> <div className="mt-6">
<Button variant="outlined" onClick={onSkipStep}> <div className="text-gray-900 text-[16px] font-semibold mb-3">
Sister Details
</div>
<div className="grid grid-cols-1 gap-4">
{Array.from({ length: Number(data.sisterCount) }).map(
(_, index) => renderSiblingCard("sisters", index)
)}
</div>
</div>
)}
<Grid
item
xs={12}
sx={{ marginTop: 10, display: "flex", gap: 4, justifyContent: "center" }}
>
<Button variant="outlined" onClick={onSkipStep}>
Skip Skip
</Button> </Button>
<Button variant="contained" color="primary" onClick={handleSubmit}> <Button variant="contained" color="primary" onClick={handleSubmit}>
Submit Submit
</Button> </Button>
</Grid> </Grid>
</form>
</div>
</form>
</div>
</>
); );
}; };

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateLifestyleDetails } from "../redux/registrationFormSlice"; import { updateLifestyleDetails } from "../redux/registrationFormSlice";
import { import {
@ -8,12 +8,50 @@ import {
Select, Select,
MenuItem, MenuItem,
Button, Button,
TextField,
Checkbox,
ListItemText,
} from "@mui/material"; } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { useLifestyleMasters } from "../hooks/useMasters";
const LifestyleDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => { const LifestyleDetailsForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.lifestyleDetails); const data = useSelector((state) => state.registerform.lifestyleDetails);
const inputRef = useRef(null); const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: lifestyleMasters, isLoading: isLifestyleMastersLoading } =
useLifestyleMasters();
const dietOptions = useMemo(() => {
const raw = lifestyleMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.diet || raw.diets || [];
}, [lifestyleMasters]);
const hobbyOptions = useMemo(() => {
const raw = lifestyleMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.hobbies || raw.hobby || [];
}, [lifestyleMasters]);
const grahaOptions = useMemo(() => {
const raw = lifestyleMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.grahas || raw.graha || [];
}, [lifestyleMasters]);
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
@ -21,6 +59,27 @@ const LifestyleDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updateLifestyleDetails({ [field]: value })); dispatch(updateLifestyleDetails({ [field]: value }));
if (onFieldChange) onFieldChange(field);
};
const handleMultiChange = (field, value) => {
const nextValue = Array.isArray(value) ? value : String(value).split(",");
dispatch(updateLifestyleDetails({ [field]: nextValue }));
if (onFieldChange) onFieldChange(field);
};
const handleGrahaChange = (house, value) => {
const next = { ...(data.graha || {}) };
next[house] = value;
dispatch(updateLifestyleDetails({ graha: next }));
if (onFieldChange) onFieldChange("graha");
};
const handleAmsamChange = (house, value) => {
const next = { ...(data.amsam || {}) };
next[house] = value;
dispatch(updateLifestyleDetails({ amsam: next }));
if (onFieldChange) onFieldChange("amsam");
}; };
const handleSubmit = () => { const handleSubmit = () => {
@ -28,67 +87,302 @@ const LifestyleDetailsForm = ({ onSubmitStep, onSkipStep, errors }) => {
onSubmitStep(); onSubmitStep();
}; };
const fields = [ const parseDateValue = (value) => {
{ name: "diet", label: "Diet", options: ["Veg", "Non-Veg", "Eggetarian"] }, if (!value) return null;
// { const date = new Date(value);
// name: "drinking", return Number.isNaN(date.getTime()) ? null : date;
// label: "Drinking Habits", };
// options: ["No", "Occasionally", "Regularly"],
// }, const parseTimeValue = (value) => {
// { if (!value || typeof value !== "string") return null;
// name: "smoking", const [hours, minutes] = value.split(":").map(Number);
// label: "Smoking Habits", if (Number.isNaN(hours) || Number.isNaN(minutes)) return null;
// options: ["No", "Occasionally", "Regularly"], const date = new Date();
// }, date.setHours(hours, minutes, 0, 0);
{ return date;
name: "hobbies", };
label: "Hobbies & Interests",
options: ["Song", "Reading", "Sports", "Travel"], const formatDate = (value) => {
}, if (!(value instanceof Date) || Number.isNaN(value)) return "";
]; const y = value.getFullYear();
const m = String(value.getMonth() + 1).padStart(2, "0");
const d = String(value.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
};
const formatTime = (value) => {
if (!(value instanceof Date) || Number.isNaN(value)) return "";
const h = String(value.getHours()).padStart(2, "0");
const m = String(value.getMinutes()).padStart(2, "0");
return `${h}:${m}`;
};
const renderChartCell = (label, value, onChange) => (
<div className="bg-white border border-gray-300 rounded-lg p-2 flex flex-col items-center justify-center min-h-[70px]">
<span className="text-[10px] font-medium text-gray-700 text-center">
{label}
</span>
<FormControl fullWidth variant="standard" sx={{ mt: 0.5 }}>
<Select
multiple
displayEmpty
value={value}
onChange={(e) => onChange(e.target.value)}
renderValue={(selected) => {
if (!selected || selected.length === 0) return `+ Add ${label}`;
return selected.join(", ");
}}
MenuProps={{
PaperProps: { style: { maxHeight: 280 } },
}}
>
{grahaOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
<Checkbox checked={value.indexOf(opt) > -1} />
<ListItemText primary={opt} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
const renderChartGrid = (type) => {
const getValue = (house) =>
(type === "graha" ? data.graha : data.amsam)?.[house] || [];
const onChange = (house, value) =>
type === "graha"
? handleGrahaChange(house, value)
: handleAmsamChange(house, value);
const label = type === "graha" ? "Rasi" : "Navamsam";
return (
<div className="grid grid-cols-4 gap-2">
{renderChartCell(label, getValue(1), (value) => onChange(1, value))}
{renderChartCell(label, getValue(2), (value) => onChange(2, value))}
{renderChartCell(label, getValue(3), (value) => onChange(3, value))}
{renderChartCell(label, getValue(4), (value) => onChange(4, value))}
{renderChartCell(label, getValue(5), (value) => onChange(5, value))}
<div className="col-span-2 row-span-2 flex items-center justify-center">
<div className="w-full aspect-square rounded-full overflow-hidden border-4 border-gray-200 shadow-lg bg-white" />
</div>
{renderChartCell(label, getValue(6), (value) => onChange(6, value))}
{renderChartCell(label, getValue(7), (value) => onChange(7, value))}
{renderChartCell(label, getValue(8), (value) => onChange(8, value))}
{renderChartCell(label, getValue(9), (value) => onChange(9, value))}
{renderChartCell(label, getValue(10), (value) => onChange(10, value))}
{renderChartCell(label, getValue(11), (value) => onChange(11, value))}
{renderChartCell(label, getValue(12), (value) => onChange(12, value))}
</div>
);
};
return ( return (
<div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
{fields.map(({ name, label, options }) => ( <div className="flex flex-col gap-2">
<div key={name} className="flex flex-col gap-2"> <label className="text-gray-900 text-[15px]">
<label className="text-gray-900 text-[15px]">{label}</label> Diet (Multi-select){requiredMark}
<FormControl </label>
fullWidth <FormControl fullWidth variant="outlined" error={Boolean(errors.diets)}>
variant="outlined" <InputLabel id="diets-label">Select Diet</InputLabel>
error={Boolean(errors[name])} <Select
labelId="diets-label"
label="Select Diet"
name="diets"
multiple
value={data.diets}
onChange={(e) => handleMultiChange("diets", e.target.value)}
inputRef={inputRef}
disabled={isLifestyleMastersLoading}
renderValue={(selected) =>
selected
.map((id) => {
const item = dietOptions.find(
(opt) => (opt.id ?? opt) === id
);
return item?.diet_name || item?.name || id;
})
.join(", ")
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
<InputLabel id={`${name}-label`}>Select {label}</InputLabel> {dietOptions.map((opt) => {
<Select const value = opt.id ?? opt;
labelId={`${name}-label`} const label = opt.diet_name || opt.name || String(opt);
label={`Select ${label}`} return (
name={name} <MenuItem key={value} value={value}>
value={data[name]} <Checkbox checked={data.diets.indexOf(value) > -1} />
onChange={(e) => handleChange(name, e.target.value)} <ListItemText primary={label} />
inputRef={name === "diet" ? inputRef : null}
>
{options.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem> </MenuItem>
))} );
</Select> })}
{errors[name] && ( </Select>
<p {errors.diets && (
style={{ <p
color: "#d32f2f", style={{
margin: "3px 14px 0 14px", color: "#d32f2f",
fontSize: "0.75rem", margin: "3px 14px 0 14px",
}} fontSize: "0.75rem",
> }}
{errors[name]} >
</p> {errors.diets}
)} </p>
</FormControl> )}
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Hobbies & Interests (Multi-select){requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.hobbies)}
>
<InputLabel id="hobbies-label">
Select Hobbies & Interests
</InputLabel>
<Select
labelId="hobbies-label"
label="Select Hobbies & Interests"
name="hobbies"
multiple
value={data.hobbies}
onChange={(e) => handleMultiChange("hobbies", e.target.value)}
disabled={isLifestyleMastersLoading}
renderValue={(selected) =>
selected
.map((id) => {
const item = hobbyOptions.find(
(opt) => (opt.id ?? opt) === id
);
return item?.hobby_name || item?.name || id;
})
.join(", ")
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{hobbyOptions.map((opt) => {
const value = opt.id ?? opt;
const label = opt.hobby_name || opt.name || String(opt);
return (
<MenuItem key={value} value={value}>
<Checkbox checked={data.hobbies.indexOf(value) > -1} />
<ListItemText primary={label} />
</MenuItem>
);
})}
</Select>
{errors.hobbies && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.hobbies}
</p>
)}
</FormControl>
</div>
</div>
<div className="text-center py-4">
<h2 className="text-[18px] font-semibold text-gray-800">
Astrology / Horoscope
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[900px] mx-auto">
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Date of Birth{requiredMark}
</label>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
format="dd/MM/yyyy"
value={parseDateValue(data.dob)}
onChange={(value) => handleChange("dob", formatDate(value))}
slotProps={{
textField: {
name: "dob",
fullWidth: true,
error: Boolean(errors.dob),
helperText: errors.dob,
},
}}
/>
</LocalizationProvider>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Time of Birth{requiredMark}
</label>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<TimePicker
value={parseTimeValue(data.tob)}
onChange={(value) => handleChange("tob", formatTime(value))}
slotProps={{
textField: {
name: "tob",
fullWidth: true,
error: Boolean(errors.tob),
helperText: errors.tob,
},
}}
/>
</LocalizationProvider>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Place of Birth</label>
<TextField
fullWidth
name="placeOfBirth"
value={data.placeOfBirth}
onChange={(e) => handleChange("placeOfBirth", e.target.value)}
error={Boolean(errors.placeOfBirth)}
helperText={errors.placeOfBirth}
placeholder="Enter Place of Birth"
variant="outlined"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-[950px] mx-auto mt-6">
<div>
<div className="py-4">
<h3 className="text-base font-semibold text-gray-800 mb-3">
Add Rasi
</h3>
{renderChartGrid("graha")}
</div> </div>
))} </div>
<div>
<div className="py-4">
<h3 className="text-base font-semibold text-gray-800 mb-3">
Add Navamsam
</h3>
{renderChartGrid("amsam")}
</div>
</div>
</div> </div>
<Grid <Grid

View File

@ -1,27 +1,137 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updatePartnerPreferences } from "../redux/registrationFormSlice"; import { updatePartnerPreferences } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
TextField,
FormControl, FormControl,
InputLabel, InputLabel,
Select, Select,
MenuItem, MenuItem,
Button, Button,
Checkbox,
ListItemText,
} from "@mui/material"; } from "@mui/material";
import { usePartnerPreferenceMasters } from "../hooks/useMasters";
import { useCityMasters, useSubCasteMasters } from "../hooks/useDependentMasters";
const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => { const PartnerPreferencesForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.partnerPreferences); const data = useSelector((state) => state.registerform.partnerPreferences);
const inputRef = useRef(null); const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: masters, isLoading: isPartnerMastersLoading } =
usePartnerPreferenceMasters();
const subCasteQuery = useSubCasteMasters(data.castes);
const cityQuery = useCityMasters(data.states);
const ageRangeOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.ageRange || raw.age_range || [];
}, [masters]);
const casteOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.caste || raw.castes || [];
}, [masters]);
const occupationOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [masters]);
const educationOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.education || raw.educations || [];
}, [masters]);
const hobbyOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.hobbies || raw.hobby || [];
}, [masters]);
const annualIncomeOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.annual_income || raw.annualIncome || [];
}, [masters]);
const stateOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.state || raw.states || [];
}, [masters]);
const subCasteOptions = useMemo(() => {
const raw = subCasteQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) {
const merged = raw.flatMap((entry) => {
if (!entry) return [];
if (Array.isArray(entry)) return entry;
return entry.sub_caste || entry.subCaste || entry.data || [];
});
return merged;
}
return raw.sub_caste || raw.subCaste || raw.data || [];
}, [subCasteQuery.data]);
const cityOptions = useMemo(() => {
const raw = cityQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.subCaste || raw.district || raw.data || [];
}, [cityQuery.data]);
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updatePartnerPreferences({ [field]: value })); const arrayFields = new Set([
"castes",
"subCastes",
"occupations",
"educations",
"hobbies",
"states",
"districts",
]);
const nextValue =
arrayFields.has(field) && typeof value === "string"
? value.split(",").filter(Boolean)
: value;
const updates = { [field]: nextValue };
const fieldsToClear = [field];
if (field === "castes") {
updates.subCastes = [];
fieldsToClear.push("subCastes");
}
if (field === "states") {
updates.districts = [];
fieldsToClear.push("districts");
}
dispatch(updatePartnerPreferences(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
}; };
const handleSubmit = () => { const handleSubmit = () => {
@ -29,36 +139,98 @@ const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
onSubmitStep(); onSubmitStep();
}; };
const renderMultiSelect = ({
name,
label,
options,
value,
getLabel,
getValue,
disabled,
}) => (
<FormControl fullWidth variant="outlined" error={Boolean(errors[name])}>
<InputLabel id={`${name}-label`}>Select {label}</InputLabel>
<Select
labelId={`${name}-label`}
label={`Select ${label}`}
name={name}
multiple
value={value}
onChange={(e) => handleChange(name, e.target.value)}
disabled={disabled}
renderValue={(selected) =>
selected
.map((id) => {
const item = options.find((opt) => getValue(opt) === id);
return item ? getLabel(item) : id;
})
.join(", ")
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{options.map((opt) => {
const optValue = getValue(opt);
const optLabel = getLabel(opt);
return (
<MenuItem key={optValue} value={optValue}>
<Checkbox checked={value.indexOf(optValue) > -1} />
<ListItemText primary={optLabel} />
</MenuItem>
);
})}
</Select>
{errors[name] && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors[name]}
</p>
)}
</FormControl>
);
return ( return (
<div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto bg-[#fff2f2] py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
{/* Preferred Age Range */} {/* Age Range */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Preferred Age Range Age Range{requiredMark}
</label> </label>
<FormControl <FormControl
fullWidth fullWidth
variant="outlined" variant="outlined"
error={Boolean(errors.ageRange)} error={Boolean(errors.ageRange)}
> >
<InputLabel id="ageRange-label"> <InputLabel id="ageRange-label">Select Age Range</InputLabel>
Select Preferred Age Range
</InputLabel>
<Select <Select
labelId="ageRange-label" labelId="ageRange-label"
label="Select Preferred Age Range" label="Select Age Range"
name="ageRange" name="ageRange"
value={data.ageRange} value={data.ageRange}
onChange={(e) => handleChange("ageRange", e.target.value)} onChange={(e) => handleChange("ageRange", e.target.value)}
inputRef={inputRef} inputRef={inputRef}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{ageRangeOptions.map((opt) => (
<MenuItem value="25 - 28">25 - 28</MenuItem> <MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
<MenuItem value="29 - 32">29 - 32</MenuItem> {opt.name || opt}
<MenuItem value="33 - 36">33 - 36</MenuItem> </MenuItem>
))}
</Select> </Select>
{errors.ageRange && ( {errors.ageRange && (
<p <p
@ -74,36 +246,120 @@ const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
</FormControl> </FormControl>
</div> </div>
{/* Religion / Caste Preference */} {/* Caste */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Religion / Caste Preference Caste{requiredMark}
</label>
{renderMultiSelect({
name: "castes",
label: "Caste",
options: casteOptions,
value: data.castes,
getLabel: (opt) => opt.caste_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Sub Caste */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Sub Caste{requiredMark}
</label>
{renderMultiSelect({
name: "subCastes",
label: "Sub Caste",
options: subCasteOptions,
value: data.subCastes,
getLabel: (opt) =>
opt.sub_caste_name || opt.subCaste_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: data.castes.length === 0 || subCasteQuery.isLoading,
})}
</div>
{/* Occupation */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Occupation{requiredMark}
</label>
{renderMultiSelect({
name: "occupations",
label: "Occupation",
options: occupationOptions,
value: data.occupations,
getLabel: (opt) => opt.occupation_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Qualification{requiredMark}
</label>
{renderMultiSelect({
name: "educations",
label: "Qualification",
options: educationOptions,
value: data.educations,
getLabel: (opt) => opt.education_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Lifestyle and Hobbies */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Lifestyle & Hobbies{requiredMark}
</label>
{renderMultiSelect({
name: "hobbies",
label: "Lifestyle & Hobbies",
options: hobbyOptions,
value: data.hobbies,
getLabel: (opt) => opt.hobby_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Annual Income */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Annual Income{requiredMark}
</label> </label>
<FormControl <FormControl
fullWidth fullWidth
variant="outlined" variant="outlined"
error={Boolean(errors.religionCaste)} error={Boolean(errors.annualIncome)}
> >
<InputLabel id="religionCaste-label"> <InputLabel id="annualIncome-label">
Select Religion / Caste Preference Select Annual Income
</InputLabel> </InputLabel>
<Select <Select
labelId="religionCaste-label" labelId="annualIncome-label"
label="Select Religion / Caste Preference" label="Select Annual Income"
name="religionCaste" name="annualIncome"
value={data.religionCaste} value={data.annualIncome}
onChange={(e) => onChange={(e) => handleChange("annualIncome", e.target.value)}
handleChange("religionCaste", e.target.value) disabled={isPartnerMastersLoading}
} sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{annualIncomeOptions.map((opt) => (
<MenuItem value="Hindu">Hindu</MenuItem> <MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
<MenuItem value="Muslim">Muslim</MenuItem> {opt.annual_income_name || opt.name || opt}
<MenuItem value="Christian">Christian</MenuItem> </MenuItem>
<MenuItem value="Any">Any</MenuItem> ))}
</Select> </Select>
{errors.religionCaste && ( {errors.annualIncome && (
<p <p
style={{ style={{
color: "#d32f2f", color: "#d32f2f",
@ -111,167 +367,43 @@ const PartnerPreferencesForm = ({ onSubmitStep, onSkipStep, errors }) => {
fontSize: "0.75rem", fontSize: "0.75rem",
}} }}
> >
{errors.religionCaste} {errors.annualIncome}
</p> </p>
)} )}
</FormControl> </FormControl>
</div> </div>
{/* Occupation Preference */} {/* State */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Occupation Preference State{requiredMark}
</label> </label>
<FormControl {renderMultiSelect({
fullWidth name: "states",
variant="outlined" label: "State",
error={Boolean(errors.occupationPref)} options: stateOptions,
> value: data.states,
<InputLabel id="occupationPref-label"> getLabel: (opt) => opt.state_name || opt.name || String(opt),
Select Occupation Preference getValue: (opt) => opt.id ?? opt,
</InputLabel> disabled: isPartnerMastersLoading,
<Select })}
labelId="occupationPref-label"
label="Select Occupation Preference"
name="occupationPref"
value={data.occupationPref}
onChange={(e) =>
handleChange("occupationPref", e.target.value)
}
>
<MenuItem value="IT, Business">IT, Business</MenuItem>
<MenuItem value="Doctor">Doctor</MenuItem>
<MenuItem value="Engineer">Engineer</MenuItem>
<MenuItem value="Any">Any</MenuItem>
</Select>
{errors.occupationPref && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.occupationPref}
</p>
)}
</FormControl>
</div> </div>
{/* Lifestyle & Habits Preference */} {/* City */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Lifestyle & Habits Preference City{requiredMark}
</label> </label>
<FormControl {renderMultiSelect({
fullWidth name: "districts",
variant="outlined" label: "City",
error={Boolean(errors.lifestylePref)} options: cityOptions,
> value: data.districts,
<InputLabel id="lifestylePref-label"> getLabel: (opt) =>
Select Lifestyle & Habits Preference opt.district_name || opt.city_name || opt.name || String(opt),
</InputLabel> getValue: (opt) => opt.id ?? opt,
<Select disabled: data.states.length === 0 || cityQuery.isLoading,
labelId="lifestylePref-label" })}
label="Select Lifestyle & Habits Preference"
name="lifestylePref"
value={data.lifestylePref}
onChange={(e) =>
handleChange("lifestylePref", e.target.value)
}
>
<MenuItem value="Non-Smoking, Non-Drinking">
Non-Smoking, Non-Drinking
</MenuItem>
<MenuItem value="Social Drinker">Social Drinker</MenuItem>
<MenuItem value="Any">Any</MenuItem>
</Select>
{errors.lifestylePref && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.lifestylePref}
</p>
)}
</FormControl>
</div>
{/* Prefer Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Prefer Qualification
</label>
<TextField
fullWidth
name="qualificationPref"
label="Enter prefer Qualification"
value={data.qualificationPref}
onChange={(e) =>
handleChange("qualificationPref", e.target.value)
}
error={Boolean(errors.qualificationPref)}
helperText={errors.qualificationPref}
variant="outlined"
/>
</div>
{/* Occupation (text) */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Occupation</label>
<TextField
fullWidth
name="occupationText"
label="Enter Occupation"
value={data.occupationText}
onChange={(e) =>
handleChange("occupationText", e.target.value)
}
error={Boolean(errors.occupationText)}
helperText={errors.occupationText}
variant="outlined"
/>
</div>
{/* Prefer Annual Income */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Prefer Annual Income
</label>
<TextField
fullWidth
name="incomePref"
label="Enter Annual Income"
value={data.incomePref}
onChange={(e) => handleChange("incomePref", e.target.value)}
error={Boolean(errors.incomePref)}
helperText={errors.incomePref}
variant="outlined"
/>
</div>
{/* Prefer Location */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Prefer Location
</label>
<TextField
fullWidth
name="locationPref"
label="Enter Prefer Location"
value={data.locationPref}
onChange={(e) => handleChange("locationPref", e.target.value)}
error={Boolean(errors.locationPref)}
helperText={errors.locationPref}
variant="outlined"
/>
</div> </div>
</div> </div>

View File

@ -35,7 +35,7 @@ import { useSendOtp, useVerifyOtp } from "../hooks/useAuth";
const OTP_LENGTH = 4; const OTP_LENGTH = 4;
const OTP_TIMER_SEC = 120; // 2 minutes const OTP_TIMER_SEC = 120; // 2 minutes
const PersonalDetailsForm = ({ onSubmitStep, errors }) => { const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.personalDetails); const data = useSelector((state) => state.registerform.personalDetails);
const nameInputRef = useRef(null); const nameInputRef = useRef(null);
@ -141,6 +141,52 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
setOtpTimer(OTP_TIMER_SEC); setOtpTimer(OTP_TIMER_SEC);
}, []); }, []);
const getApiErrorMessage = useCallback((error, fallback) => {
const data = error?.response?.data ?? error?.data ?? error;
if (!data) return fallback;
if (typeof data === "string") return data;
const directMessage =
data.message || data.error || data.detail || data.msg;
if (directMessage) return directMessage;
if (Array.isArray(data.errors)) {
const first = data.errors[0];
if (typeof first === "string") return first;
if (first && typeof first === "object") {
return (
first.message ||
first.msg ||
first.error ||
first.detail ||
fallback
);
}
}
if (data.errors && typeof data.errors === "object") {
const firstValue = Object.values(data.errors)[0];
if (Array.isArray(firstValue)) {
const joined = firstValue.filter(Boolean).join(" ");
if (joined) return joined;
}
if (typeof firstValue === "string") return firstValue;
if (firstValue && typeof firstValue === "object") {
return (
firstValue.message ||
firstValue.msg ||
firstValue.error ||
firstValue.detail ||
fallback
);
}
}
if (data.otp) return String(data.otp);
if (error?.message) return error.message;
return fallback;
}, []);
useEffect(() => { useEffect(() => {
if (otpTimer <= 0) return; if (otpTimer <= 0) return;
const timerId = setInterval(() => { const timerId = setInterval(() => {
@ -207,18 +253,18 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
resetOtp(); resetOtp();
setMobileOtpVerified(false); setMobileOtpVerified(false);
} catch (error) { } catch (error) {
setMobileNumberError("Failed to send OTP. Please try again."); const message = getApiErrorMessage(
const message = error,
error?.response?.data?.message || "Failed to send OTP. Please try again."
error?.response?.data?.otp || );
error?.message || setMobileNumberError(message);
"Failed to send OTP";
toast.error(message, { position: "top-right" }); toast.error(message, { position: "top-right" });
} }
}; };
const handleChange = (field, value) => { const handleChange = (field, value) => {
const updates = { [field]: value }; const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "mobileNumber") { if (field === "mobileNumber") {
setMobileNumberError(""); setMobileNumberError("");
setShowOtp(false); setShowOtp(false);
@ -230,21 +276,61 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
if (field === "religion") { if (field === "religion") {
updates.caste = ""; updates.caste = "";
updates.subCaste = ""; updates.subCaste = "";
fieldsToClear.push("caste", "subCaste");
} }
if (field === "caste") { if (field === "caste") {
updates.subCaste = ""; updates.subCaste = "";
fieldsToClear.push("subCaste");
} }
if (field === "state") { if (field === "state") {
updates.city = ""; updates.city = "";
fieldsToClear.push("city");
} }
if (field === "raasi") { if (field === "raasi") {
updates.star = ""; updates.star = "";
fieldsToClear.push("star");
}
if (field === "password") {
fieldsToClear.push("confirmPassword");
} }
dispatch(updatePersonalDetails(updates)); dispatch(updatePersonalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
}; };
const isOtpComplete = otp.every((digit) => digit !== ""); const isOtpComplete = otp.every((digit) => digit !== "");
const passwordStrength = useMemo(() => {
const value = data.password || "";
if (!value) return null;
const rules = [
{ key: "length", label: "At least 8 characters", ok: value.length >= 8 },
{ key: "upper", label: "Uppercase letter", ok: /[A-Z]/.test(value) },
{ key: "lower", label: "Lowercase letter", ok: /[a-z]/.test(value) },
{ key: "number", label: "Number", ok: /\d/.test(value) },
{ key: "symbol", label: "Symbol", ok: /[^A-Za-z0-9]/.test(value) },
{ key: "length12", label: "12+ characters (recommended)", ok: value.length >= 12 },
];
const score = rules.filter((rule) => rule.ok).length;
const percent = Math.round((score / rules.length) * 100);
let label = "Weak";
let color = "#d32f2f";
if (score >= 5) {
label = "Strong";
color = "#2e7d32";
} else if (score >= 3) {
label = "Medium";
color = "#ed6c02";
}
return {
label,
color,
percent,
rules,
};
}, [data.password]);
const handleOtpSubmit = async () => { const handleOtpSubmit = async () => {
if (!isOtpComplete) { if (!isOtpComplete) {
setOtpError("Complete OTP is required"); setOtpError("Complete OTP is required");
@ -265,11 +351,10 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
setMobileNumberError(""); setMobileNumberError("");
setOtpError(""); setOtpError("");
} catch (error) { } catch (error) {
const message = const message = getApiErrorMessage(
error?.response?.data?.message || error,
error?.response?.data?.otp || "Invalid or expired OTP"
error?.message || );
"Invalid or expired OTP";
setOtpError(message); setOtpError(message);
toast.error(message, { position: "top-right" }); toast.error(message, { position: "top-right" });
} }
@ -576,6 +661,7 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
}} }}
slotProps={{ slotProps={{
textField: { textField: {
name: "dob",
fullWidth: true, fullWidth: true,
error: Boolean(errors.dob), error: Boolean(errors.dob),
helperText: errors.dob, helperText: errors.dob,
@ -954,6 +1040,64 @@ const PersonalDetailsForm = ({ onSubmitStep, errors }) => {
}} }}
variant="outlined" variant="outlined"
/> />
{passwordStrength && (
<div className="rounded-lg border border-gray-200 bg-white/70 p-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Password strength</span>
<span
style={{
color: passwordStrength.color,
fontWeight: 600,
}}
>
{passwordStrength.label}
</span>
</div>
<div className="mt-2 h-2 w-full rounded-full bg-gray-200 overflow-hidden">
<div
style={{
width: `${passwordStrength.percent}%`,
height: "100%",
background: passwordStrength.color,
transition: "width 200ms ease",
}}
/>
</div>
<div className="mt-2 text-xs text-gray-500">
Recommended: 12+ characters with a mix of upper/lowercase,
numbers, and symbols.
</div>
<div className="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-y-1 gap-x-3 text-xs">
{passwordStrength.rules.map((rule) => (
<div
key={rule.key}
className="flex items-center gap-2"
>
{rule.ok ? (
<CheckCircleIcon sx={{ fontSize: 14, color: "#2e7d32" }} />
) : (
<span
style={{
width: 12,
height: 12,
borderRadius: "999px",
border: "1px solid #d1d5db",
display: "inline-block",
}}
/>
)}
<span
style={{
color: rule.ok ? "#111827" : "#6b7280",
}}
>
{rule.label}
</span>
</div>
))}
</div>
</div>
)}
</div> </div>
{/* Confirm Password */} {/* Confirm Password */}

View File

@ -1,4 +1,4 @@
import React from 'react'; import React from "react";
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Edit2,Info } from 'lucide-react'; import { Edit2,Info } from 'lucide-react';
import { import {
@ -12,44 +12,154 @@ import {
Button, Button,
Grid, Grid,
} from '@mui/material'; } from '@mui/material';
import { usePreviewDetails } from "../hooks/usePreview";
const PreviewScreen = ({ onEdit, onSubmit }) => { const PreviewScreen = ({ onEdit, onSubmit }) => {
const formData = useSelector((state) => state.registerform); const formData = useSelector((state) => state.registerform);
const { data: previewData, isLoading, isError } = usePreviewDetails();
const sections = [ const sections = previewData?.personal_details
{ ? [
title: 'Personal Details', {
step: 1, title: "Personal Details",
data: formData.personalDetails, step: 1,
}, data: previewData.personal_details,
{ },
title: 'Educational & Professional Details', {
step: 2, title: "Educational & Professional Details",
data: formData.educationalDetails, step: 2,
}, data: previewData.educational_details,
{ },
title: 'Family Details', {
step: 3, title: "Family Details",
data: formData.familyDetails, step: 3,
}, data: previewData.family_details,
{ },
title: 'Lifestyle & Habits', {
step: 4, title: "Lifestyle & Habits",
data: formData.lifestyleDetails, step: 4,
}, data: previewData.lifestyle_details,
{ },
title: 'Partner Preferences', {
step: 5, title: "Partner Preferences",
data: formData.partnerPreferences, step: 5,
}, data: previewData.partner_preferences,
]; },
]
: [
{
title: 'Personal Details',
step: 1,
data: formData.personalDetails,
},
{
title: 'Educational & Professional Details',
step: 2,
data: formData.educationalDetails,
},
{
title: 'Family Details',
step: 3,
data: formData.familyDetails,
},
{
title: 'Lifestyle & Habits',
step: 4,
data: formData.lifestyleDetails,
},
{
title: 'Partner Preferences',
step: 5,
data: formData.partnerPreferences,
},
];
const renderValue = (key, value) => {
if (key === "profiles" || key === "profile") {
const list = Array.isArray(value) ? value : [];
return (
<Box display="flex" gap={0} flexWrap="wrap">
{list.length > 0 ? (
list.map((imgObj, index) => {
const src =
typeof imgObj === "string"
? imgObj
: imgObj?.preview ||
imgObj?.url ||
(imgObj?.file ? URL.createObjectURL(imgObj.file) : null);
if (!src) return null;
return (
<img
key={index}
src={src}
alt="Profile"
style={{
width: "90px",
height: "90px",
objectFit: "cover",
borderRadius: "50%",
border: "1px solid #ccc",
marginLeft: "-20px",
}}
/>
);
})
) : (
<Box display="flex" alignItems="center" gap={1} sx={{ color: "#888" }}>
<Info size={16} />
<Typography>No photos uploaded</Typography>
</Box>
)}
</Box>
);
}
if (Array.isArray(value)) {
if (value.length === 0) return "-";
if (typeof value[0] === "object") {
return value.map((item, idx) => (
<div key={idx}>{JSON.stringify(item)}</div>
));
}
return value.join(", ");
}
if (value && typeof value === "object") {
return "-";
}
return value || "-";
};
return ( return (
<Box p={3} sx={{maxWidth:"1400px", width:"100%"}} mx="auto" > <Box p={3} sx={{maxWidth:"1400px", width:"100%"}} mx="auto" >
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 my-10'> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 my-10'>
{sections.map((section) => ( {isLoading && (
<Box
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#666" }}
>
Loading preview...
</Box>
)}
{isError && (
<Box
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#d32f2f" }}
>
Failed to load preview.
</Box>
)}
{!isLoading &&
sections.map((section) => (
<Card key={section.title} variant="outlined" sx={{ borderRadius: 2, background:"#fff5ed" }}> <Card key={section.title} variant="outlined" sx={{ borderRadius: 2, background:"#fff5ed" }}>
<CardHeader <CardHeader
@ -72,7 +182,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
/> />
<Divider /> <Divider />
<CardContent sx={{ pt: 1, }}> <CardContent sx={{ pt: 1, }}>
{Object.entries(section.data).map(([key, value]) => { {Object.entries(section.data || {}).map(([key, value]) => {
// if (value && key !== 'profiles') { // if (value && key !== 'profiles') {
@ -82,9 +192,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
.replace(/_/g, ' ') .replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase()) .replace(/\b\w/g, (l) => l.toUpperCase())
.trim(); .trim();
const isImage = const content = renderValue(key, value);
typeof value === "string" &&
(value.startsWith("http") || value.startsWith("data:image"));
return ( return (
<Box <Box
@ -106,56 +214,28 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
</Typography> </Typography>
{/* Special Case: Profiles Image Preview */} {/* Special Case: Profiles Image Preview */}
{key === "profiles" ? ( {key === "profiles" || key === "profile" ? (
<Box display="flex" gap={0} flexWrap="wrap"> content
{value && value.length > 0 ? ( ) : value ? (
value.map((imgObj, index) => ( <Typography
<img sx={{
key={index} fontWeight: 600,
src={URL.createObjectURL(imgObj.file)} wordBreak: "break-word",
alt="Profile" textAlign: "left",
style={{ }}
width: "90px", >
height: "90px", {content}
objectFit: "cover", </Typography>
borderRadius: "50%",
border: "1px solid #ccc",
marginLeft:"-20px"
}}
/>
))
) : (
<Box display="flex" alignItems="center" gap={1} sx={{ color: "#888" }}>
<Info size={16} />
<Typography>No photos uploaded</Typography>
</Box>
)}
</Box>
) : ( ) : (
// Text OR No Data <Box
<> display="flex"
{value ? ( alignItems="center"
<Typography gap={1}
sx={{ sx={{ color: "#888", fontStyle: "italic" }}
fontWeight: 600, >
wordBreak: "break-word", <Info size={16} />
textAlign: "left", <Typography>No data available</Typography>
}} </Box>
>
{value}
</Typography>
) : (
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No data available</Typography>
</Box>
)}
</>
)} )}
</Box> </Box>

View File

@ -234,8 +234,10 @@ const ProfilePreviewPage = () => {
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Qualification", educationalDetails.qualification, 2)} {renderField("Qualification", educationalDetails.qualification, 2)}
{renderField("Field of Study", educationalDetails.fieldOfStudy, 2)} {renderField("Field of Study", educationalDetails.fieldOfStudy, 2)}
{renderField("College Name", educationalDetails.collegeName, 2)}
{renderField("Occupation", educationalDetails.occupation, 2)} {renderField("Occupation", educationalDetails.occupation, 2)}
{renderField("Organization", educationalDetails.organization, 2)} {renderField("Organization", educationalDetails.organization, 2)}
{renderField("Employee Type", educationalDetails.employeeType, 2)}
{renderField("Income", educationalDetails.income, 2)} {renderField("Income", educationalDetails.income, 2)}
{renderField("Work Location", educationalDetails.workLocation, 2)} {renderField("Work Location", educationalDetails.workLocation, 2)}
</CardContent> </CardContent>
@ -269,8 +271,10 @@ const ProfilePreviewPage = () => {
{renderField("Father Occupation", familyDetails.fatherOccupation)} {renderField("Father Occupation", familyDetails.fatherOccupation)}
{renderField("Mother Name", familyDetails.motherName)} {renderField("Mother Name", familyDetails.motherName)}
{renderField("Mother Occupation", familyDetails.motherOccupation)} {renderField("Mother Occupation", familyDetails.motherOccupation)}
{renderField("Number of Siblings", familyDetails.siblings)} {renderField("Brother Count", familyDetails.brotherCount)}
{renderField("Siblings Marital status", familyDetails.siblingsStatus)} {renderField("Sister Count", familyDetails.sisterCount)}
{renderField("Family Status", familyDetails.familyStatus)}
{renderField("Native Place", familyDetails.nativePlace)}
</CardContent> </CardContent>
</Card> </Card>
); );
@ -298,11 +302,21 @@ const ProfilePreviewPage = () => {
<Divider /> <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Diet", lifestyleDetails.diet)} {renderField(
{/* {renderField("Drinking", lifestyleDetails.drinking)} "Diet",
{renderField("Smoking", lifestyleDetails.smoking)} */} Array.isArray(lifestyleDetails.diets)
{renderField("Hobbies", lifestyleDetails.hobbies)} ? lifestyleDetails.diets.join(", ")
: lifestyleDetails.diets
)}
{renderField(
"Hobbies",
Array.isArray(lifestyleDetails.hobbies)
? lifestyleDetails.hobbies.join(", ")
: lifestyleDetails.hobbies
)}
{renderField("Date of Birth", lifestyleDetails.dob)}
{renderField("Time of Birth", lifestyleDetails.tob)}
{renderField("Place of Birth", lifestyleDetails.placeOfBirth)}
</CardContent> </CardContent>
</Card> </Card>
); );
@ -331,14 +345,50 @@ const ProfilePreviewPage = () => {
<Divider /> <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Age", partnerPreferences.ageRange)} {renderField("Age Range", partnerPreferences.ageRange)}
{renderField("Religion", partnerPreferences.religionCaste)} {renderField(
{renderField("Occupation", partnerPreferences.occupationPref)} "Caste",
{renderField("Life style", partnerPreferences.lifestylePref)} Array.isArray(partnerPreferences.castes)
{renderField("Qualification", partnerPreferences.qualificationPref)} ? partnerPreferences.castes.join(", ")
{renderField("Occupation", partnerPreferences.occupationText)} : partnerPreferences.castes
{renderField("Income", partnerPreferences.incomePref)} )}
{renderField("Location", partnerPreferences.locationPref)} {renderField(
"Sub Caste",
Array.isArray(partnerPreferences.subCastes)
? partnerPreferences.subCastes.join(", ")
: partnerPreferences.subCastes
)}
{renderField(
"Occupation",
Array.isArray(partnerPreferences.occupations)
? partnerPreferences.occupations.join(", ")
: partnerPreferences.occupations
)}
{renderField(
"Qualification",
Array.isArray(partnerPreferences.educations)
? partnerPreferences.educations.join(", ")
: partnerPreferences.educations
)}
{renderField(
"Lifestyle & Hobbies",
Array.isArray(partnerPreferences.hobbies)
? partnerPreferences.hobbies.join(", ")
: partnerPreferences.hobbies
)}
{renderField("Annual Income", partnerPreferences.annualIncome)}
{renderField(
"State",
Array.isArray(partnerPreferences.states)
? partnerPreferences.states.join(", ")
: partnerPreferences.states
)}
{renderField(
"City",
Array.isArray(partnerPreferences.districts)
? partnerPreferences.districts.join(", ")
: partnerPreferences.districts
)}
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,4 +1,4 @@
import React, { useState,useEffect } from "react"; import React, { useState, useEffect } from "react";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
@ -16,7 +16,105 @@ import LifestyleDetailsForm from "./LifestyleDetailsForm";
import PartnerPreferencesForm from "./PartnerPreferencesForm"; import PartnerPreferencesForm from "./PartnerPreferencesForm";
import PreviewScreen from "./PreviewScreen"; import PreviewScreen from "./PreviewScreen";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useRegisterStep1 } from "../hooks/useRegister"; import {
useRegisterStep1,
useRegisterStep2,
useRegisterStep3,
useRegisterStep4,
useRegisterStep5,
} from "../hooks/useRegister";
import { setAccessToken } from "../api/axiosInstance";
import toast from "react-hot-toast";
const STEP_FIELD_ORDER = {
1: [
"name",
"gender",
"mobileNumber",
"dob",
"height",
"weight",
"maritalStatus",
"religion",
"profileFor",
"caste",
"subCaste",
"gothram",
"raasi",
"star",
"email",
"password",
"confirmPassword",
"state",
"city",
"pincode",
"profiles",
],
2: [
"fieldOfStudy",
"qualification",
"collegeName",
"occupation",
"organization",
"employeeType",
"income",
"workLocation",
],
3: [
"fatherName",
"fatherOccupation",
"motherName",
"motherOccupation",
"brotherCount",
"sisterCount",
"familyStatus",
"nativePlace",
],
4: ["diets", "hobbies", "dob", "tob", "placeOfBirth"],
5: [
"ageRange",
"castes",
"subCastes",
"occupations",
"educations",
"hobbies",
"annualIncome",
"states",
"districts",
],
};
const STEP1_SERVER_FIELD_MAP = {
name: "name",
mobile: "mobileNumber",
mobile_number: "mobileNumber",
mobileNumber: "mobileNumber",
phone: "mobileNumber",
email: "email",
gender: "gender",
dob: "dob",
height: "height",
weight: "weight",
marital_status: "maritalStatus",
maritalStatus: "maritalStatus",
religion: "religion",
profile_for: "profileFor",
profileFor: "profileFor",
caste: "caste",
sub_caste: "subCaste",
subCaste: "subCaste",
gothram: "gothram",
raasi: "raasi",
star: "star",
state: "state",
district: "city",
city: "city",
pincode: "pincode",
password: "password",
confirm_password: "confirmPassword",
confirmPassword: "confirmPassword",
profiles: "profiles",
};
const Stepper = ({ currentStep, onStepClick }) => { const Stepper = ({ currentStep, onStepClick }) => {
@ -84,6 +182,136 @@ const StepperForm = () => {
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const registerStep1 = useRegisterStep1(); const registerStep1 = useRegisterStep1();
const registerStep2 = useRegisterStep2();
const registerStep3 = useRegisterStep3();
const registerStep4 = useRegisterStep4();
const registerStep5 = useRegisterStep5();
const normalizeStep1Field = (key) => {
if (!key) return key;
const trimmed = String(key).trim();
if (!trimmed) return trimmed;
return (
STEP1_SERVER_FIELD_MAP[trimmed] ||
STEP1_SERVER_FIELD_MAP[trimmed.toLowerCase()] ||
trimmed
);
};
const coerceErrorMessage = (value) => {
if (Array.isArray(value)) {
return value.filter(Boolean).join(" ");
}
if (typeof value === "string") return value;
if (value && typeof value === "object") {
return (
value.message ||
value.msg ||
value.error ||
value.detail ||
""
);
}
if (value === null || value === undefined) return "";
return String(value);
};
const mapServerErrors = (error) => {
const data = error?.response?.data ?? error?.data ?? error;
if (!data) return {};
const payload = data.errors ?? data.error ?? data.data ?? data;
if (typeof payload === "string") {
return { _form: payload };
}
if (Array.isArray(payload)) {
const out = {};
payload.forEach((item) => {
if (!item) return;
if (typeof item === "string") {
out._form = out._form ? `${out._form} ${item}` : item;
return;
}
const key = item.field || item.name || item.param || item.key;
const message =
item.message || item.msg || item.error || item.detail || "";
if (key) {
out[normalizeStep1Field(key)] =
String(message || "Invalid value");
}
});
return out;
}
if (typeof payload === "object") {
const out = {};
Object.entries(payload).forEach(([key, value]) => {
if (
(key === "message" || key === "error" || key === "detail") &&
typeof value === "string"
) {
out._form = out._form ? `${out._form} ${value}` : value;
return;
}
const normalizedKey = normalizeStep1Field(key);
const message = coerceErrorMessage(value);
if (!normalizedKey) return;
out[normalizedKey] = message || "Invalid value";
});
return out;
}
return {};
};
const focusFirstError = (errorMap, fieldOrder = []) => {
if (!errorMap || Object.keys(errorMap).length === 0) return;
const order = fieldOrder.filter((key) => errorMap[key]);
const firstKey =
order[0] ||
Object.keys(errorMap).find((key) => key !== "_form");
if (!firstKey) return;
setTimeout(() => {
const byAria = document.querySelector(
`[aria-labelledby~="${firstKey}-label"]`
);
if (byAria && typeof byAria.focus === "function") {
byAria.focus();
return;
}
const byName = document.querySelector(`[name="${firstKey}"]`);
if (byName && typeof byName.focus === "function") {
byName.focus();
return;
}
const byId = document.getElementById(firstKey);
if (byId && typeof byId.focus === "function") {
byId.focus();
}
}, 0);
};
const clearFieldErrors = (fields) => {
if (!fields) return;
const list = Array.isArray(fields) ? fields : [fields];
setErrors((prev) => {
if (!prev || Object.keys(prev).length === 0) return prev;
let changed = false;
const next = { ...prev };
list.forEach((field) => {
if (field in next) {
delete next[field];
changed = true;
}
});
if (next._form) {
delete next._form;
changed = true;
}
return changed ? next : prev;
});
};
useEffect(() => { useEffect(() => {
@ -154,8 +382,8 @@ const StepperForm = () => {
"fieldOfStudy", "fieldOfStudy",
"occupation", "occupation",
"organization", "organization",
"employeeType",
"income", "income",
"workLocation",
]; ];
required.forEach((field) => { required.forEach((field) => {
if (!educationalDetails[field]) { if (!educationalDetails[field]) {
@ -165,12 +393,8 @@ const StepperForm = () => {
} else if (step === 3) { } else if (step === 3) {
const required = [ const required = [
"fatherName", "fatherName",
"fatherOccupation",
"motherName", "motherName",
"motherOccupation", "familyStatus",
"siblings",
"siblingsStatus",
]; ];
required.forEach((field) => { required.forEach((field) => {
if (!familyDetails[field]) { if (!familyDetails[field]) {
@ -178,25 +402,36 @@ const StepperForm = () => {
} }
}); });
} else if (step === 4) { } else if (step === 4) {
const required = ["diet", "drinking", "smoking", "hobbies"]; const required = ["diets", "hobbies", "dob", "tob"];
required.forEach((field) => { required.forEach((field) => {
if (!lifestyleDetails[field]) { const value = lifestyleDetails[field];
if (Array.isArray(value)) {
if (value.length === 0) newErrors[field] = "This field is required";
} else if (!value) {
newErrors[field] = "This field is required"; newErrors[field] = "This field is required";
} }
}); });
} else if (step === 5) { } else if (step === 5) {
const required = [ const required = [
"ageRange", "ageRange",
"religionCaste", "castes",
"occupationPref", "subCastes",
"lifestylePref", "occupations",
"qualificationPref", "educations",
"occupationText", "hobbies",
"incomePref", "annualIncome",
"locationPref", "states",
"districts",
]; ];
required.forEach((field) => { required.forEach((field) => {
if (!partnerPreferences[field]) { const value = partnerPreferences[field];
if (Array.isArray(value)) {
if (value.length === 0) {
newErrors[field] = "This field is required";
}
return;
}
if (!value) {
newErrors[field] = "This field is required"; newErrors[field] = "This field is required";
} }
}); });
@ -204,11 +439,8 @@ const StepperForm = () => {
setErrors(newErrors); setErrors(newErrors);
// Autofocus first invalid field
if (Object.keys(newErrors).length > 0) { if (Object.keys(newErrors).length > 0) {
const firstKey = Object.keys(newErrors)[0]; focusFirstError(newErrors, STEP_FIELD_ORDER[step] || []);
const el = document.querySelector(`[name="${firstKey}"]`);
if (el) el.focus();
} }
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
@ -237,6 +469,132 @@ const StepperForm = () => {
fcm_token: localStorage.getItem("fcm_token") || "", fcm_token: localStorage.getItem("fcm_token") || "",
}); });
const buildRegisterStep2Payload = () => ({
college_name: educationalDetails.collegeName || "",
study_field: educationalDetails.fieldOfStudy,
education: educationalDetails.qualification,
occupation: educationalDetails.occupation,
company_name: educationalDetails.organization || "",
employee_type: educationalDetails.employeeType,
annual_income: educationalDetails.income,
work_location: educationalDetails.workLocation || "",
});
const buildRegisterStep3Payload = () => {
const payload = {
father_name: familyDetails.fatherName,
father_occupation: familyDetails.fatherOccupation || "",
mother_name: familyDetails.motherName,
mother_occupation: familyDetails.motherOccupation || "",
family_status: familyDetails.familyStatus,
native_place: familyDetails.nativePlace || "",
brother_count: familyDetails.brotherCount || 0,
sister_count: familyDetails.sisterCount || 0,
};
(familyDetails.brothers || []).forEach((brother, index) => {
payload[`brothers[${index}][name]`] = brother?.name || "";
payload[`brothers[${index}][occupation]`] = brother?.occupation || "";
payload[`brothers[${index}][marital_status]`] =
brother?.maritalStatus || "";
payload[`brothers[${index}][have_childrens]`] =
brother?.haveChildrens ?? "";
});
(familyDetails.sisters || []).forEach((sister, index) => {
payload[`sisters[${index}][name]`] = sister?.name || "";
payload[`sisters[${index}][occupation]`] = sister?.occupation || "";
payload[`sisters[${index}][marital_status]`] =
sister?.maritalStatus || "";
payload[`sisters[${index}][have_childrens]`] =
sister?.haveChildrens ?? "";
});
return payload;
};
const buildRegisterStep4Payload = () => {
const payload = {
dob: lifestyleDetails.dob,
tob: lifestyleDetails.tob,
};
(lifestyleDetails.diets || []).forEach((id, index) => {
payload[`diets[${index}]`] = id;
});
(lifestyleDetails.hobbies || []).forEach((id, index) => {
payload[`hobbies[${index}]`] = id;
});
const graha = lifestyleDetails.graha || {};
Object.keys(graha).forEach((house) => {
const values = graha[house] || [];
values.forEach((value, index) => {
payload[`graha_${house}[${index}]`] = value;
});
});
const amsam = lifestyleDetails.amsam || {};
Object.keys(amsam).forEach((house) => {
const values = amsam[house] || [];
values.forEach((value, index) => {
payload[`amsam_${house}[${index}]`] = value;
});
});
return payload;
};
const buildRegisterStep5Payload = () => {
const payload = {
age_range: partnerPreferences.ageRange,
annual_income: partnerPreferences.annualIncome,
};
(partnerPreferences.castes || []).forEach((id, index) => {
payload[`castes[${index}]`] = id;
});
(partnerPreferences.subCastes || []).forEach((id, index) => {
payload[`sub_castes[${index}]`] = id;
});
(partnerPreferences.occupations || []).forEach((id, index) => {
payload[`occupations[${index}]`] = id;
});
(partnerPreferences.educations || []).forEach((id, index) => {
payload[`educations[${index}]`] = id;
});
(partnerPreferences.hobbies || []).forEach((id, index) => {
payload[`hobbies[${index}]`] = id;
});
(partnerPreferences.states || []).forEach((id, index) => {
payload[`states[${index}]`] = id;
});
(partnerPreferences.districts || []).forEach((id, index) => {
payload[`districts[${index}]`] = id;
});
return payload;
};
const extractAccessToken = (res) =>
res?.access_token ||
res?.accessToken ||
res?.token ||
res?.data?.access_token ||
res?.data?.accessToken ||
res?.data?.token ||
res?.result?.access_token ||
res?.result?.accessToken ||
res?.result?.token ||
null;
const handleStepSubmit = async () => { const handleStepSubmit = async () => {
const isValid = validateStep(currentStep); const isValid = validateStep(currentStep);
if (!isValid) return; if (!isValid) return;
@ -244,11 +602,45 @@ const StepperForm = () => {
try { try {
if (currentStep === 1) { if (currentStep === 1) {
const payload = buildRegisterStep1Payload(); const payload = buildRegisterStep1Payload();
await registerStep1.mutateAsync(payload); const res = await registerStep1.mutateAsync(payload);
const token = extractAccessToken(res);
if (token) {
setAccessToken(token);
}
} else if (currentStep === 2) {
const payload = buildRegisterStep2Payload();
await registerStep2.mutateAsync(payload);
} else if (currentStep === 3) {
const payload = buildRegisterStep3Payload();
await registerStep3.mutateAsync(payload);
} else if (currentStep === 4) {
const payload = buildRegisterStep4Payload();
await registerStep4.mutateAsync(payload);
} else if (currentStep === 5) {
const payload = buildRegisterStep5Payload();
await registerStep5.mutateAsync(payload);
} }
setErrors({});
setCurrentStep((prev) => Math.min(prev + 1, 6)); setCurrentStep((prev) => Math.min(prev + 1, 6));
window.scrollTo(0, 0); window.scrollTo(0, 0);
} catch (e) { } catch (e) {
if (currentStep === 1) {
const serverErrors = mapServerErrors(e);
if (Object.keys(serverErrors).length > 0) {
const hasFieldErrors = Object.keys(serverErrors).some(
(key) => key !== "_form"
);
if (hasFieldErrors) {
setErrors(serverErrors);
focusFirstError(serverErrors, STEP_FIELD_ORDER[1]);
return;
}
if (serverErrors._form) {
toast.error(serverErrors._form, { position: "top-right" });
return;
}
}
}
alert("Failed to submit step. Please try again."); alert("Failed to submit step. Please try again.");
} }
}; };
@ -296,6 +688,7 @@ const StepperForm = () => {
<PersonalDetailsForm <PersonalDetailsForm
onSubmitStep={handleStepSubmit} onSubmitStep={handleStepSubmit}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors}
/> />
); );
case 2: case 2:
@ -304,6 +697,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit} onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip} onSkipStep={handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors}
/> />
); );
case 3: case 3:
@ -312,6 +706,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit} onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip} onSkipStep={handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors}
/> />
); );
case 4: case 4:
@ -320,6 +715,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit} onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip} onSkipStep={handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors}
/> />
); );
case 5: case 5:
@ -328,6 +724,7 @@ const StepperForm = () => {
onSubmitStep={handleStepSubmit} onSubmitStep={handleStepSubmit}
onSkipStep={handleSkip} onSkipStep={handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors}
/> />
); );
case 6: case 6:

View File

@ -27,8 +27,16 @@ export const useCasteMasters = (religion_id) =>
export const useSubCasteMasters = (caste_id) => export const useSubCasteMasters = (caste_id) =>
useQuery({ useQuery({
queryKey: ["sub-caste-masters", caste_id], queryKey: ["sub-caste-masters", caste_id],
queryFn: () => getSubCasteMasters(caste_id), queryFn: async () => {
enabled: !!caste_id, if (Array.isArray(caste_id)) {
const results = await Promise.all(
caste_id.map((id) => getSubCasteMasters(id))
);
return results;
}
return getSubCasteMasters(caste_id);
},
enabled: Array.isArray(caste_id) ? caste_id.length > 0 : !!caste_id,
}); });
/** City depends on state */ /** City depends on state */
@ -36,7 +44,7 @@ export const useCityMasters = (state_id) =>
useQuery({ useQuery({
queryKey: ["city-masters", state_id], queryKey: ["city-masters", state_id],
queryFn: () => getCityMasters(state_id), queryFn: () => getCityMasters(state_id),
enabled: !!state_id, enabled: Array.isArray(state_id) ? state_id.length > 0 : !!state_id,
}); });
/** Star depends on raasi */ /** Star depends on raasi */

View File

@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { import {
getPersonalDetailsMasters, getPersonalDetailsMasters,
getEducationMasters, getEducationMasters,
getEducationList,
getFamilyMasters, getFamilyMasters,
getLifestyleMasters, getLifestyleMasters,
getPartnerPreferenceMasters, getPartnerPreferenceMasters,
@ -21,6 +22,13 @@ export const useEducationMasters = () =>
queryFn: getEducationMasters, queryFn: getEducationMasters,
}); });
export const useEducationList = (studyFieldId) =>
useQuery({
queryKey: ["education-list", studyFieldId],
queryFn: () => getEducationList(studyFieldId),
enabled: Boolean(studyFieldId),
});
/** Family master */ /** Family master */
export const useFamilyMasters = () => export const useFamilyMasters = () =>
useQuery({ useQuery({

8
src/hooks/usePreview.js Normal file
View File

@ -0,0 +1,8 @@
import { useQuery } from "@tanstack/react-query";
import { getPreviewDetails } from "../api/preview.api";
export const usePreviewDetails = () =>
useQuery({
queryKey: ["preview-details"],
queryFn: getPreviewDetails,
});

View File

@ -4,6 +4,7 @@ import {
registerStep2API, registerStep2API,
registerStep3API, registerStep3API,
registerStep4API, registerStep4API,
registerStep5API,
} from "../api/register.api"; } from "../api/register.api";
/** /**
@ -37,3 +38,11 @@ export const useRegisterStep4 = () =>
useMutation({ useMutation({
mutationFn: registerStep4API, mutationFn: registerStep4API,
}); });
/**
* Register - Step 5
*/
export const useRegisterStep5 = () =>
useMutation({
mutationFn: registerStep5API,
});

10
src/hooks/useReviews.js Normal file
View File

@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { getReviews } from "../api/reviews.api";
export const useReviews = () =>
useQuery({
queryKey: ["reviews"],
queryFn: getReviews,
staleTime: 1000 * 60 * 10,
refetchOnWindowFocus: false,
});

View File

@ -0,0 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { getTermsAndPolicies } from "../api/terms.api";
export const useTermsAndPolicies = (type) =>
useQuery({
queryKey: ["terms-and-policies", type],
queryFn: () => getTermsAndPolicies(type),
enabled: Boolean(type),
staleTime: 1000 * 60 * 60,
refetchOnWindowFocus: false,
});

View File

@ -1,13 +1,22 @@
import React, { useMemo } from "react";
import React from 'react'; import { Security, Shield, VerifiedUser } from "@mui/icons-material";
import { ArrowBackIosNew, Security, Shield, VerifiedUser } from '@mui/icons-material'; import { useSearchParams } from "react-router-dom";
import { useNavigate } from 'react-router-dom'; import { motion } from "framer-motion";
import { motion } from 'framer-motion'; import { useTermsAndPolicies } from "../hooks/useTermsAndPolicies";
import { Skeleton, SkeletonText } from "../components/common/Skeleton";
const text = `It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy.`;
export default function PoliciesPage() { export default function PoliciesPage() {
const navigate = useNavigate(); const [searchParams] = useSearchParams();
const type = searchParams.get("type") || "privacy";
const { data, isLoading, isError } = useTermsAndPolicies(type);
const content = useMemo(() => {
return data?.data?.message || "";
}, [data]);
const title = useMemo(() => {
return data?.data?.name || "Privacy Policy";
}, [data]);
return ( return (
<div className="min-h-screen relative z-20"> <div className="min-h-screen relative z-20">
@ -19,40 +28,66 @@ export default function PoliciesPage() {
> >
<div className="absolute inset-0 bg-black/10" /> <div className="absolute inset-0 bg-black/10" />
<div className="relative flex items-center justify-between px-6 py-8 safe-area-top"> <div className="relative flex items-center justify-between px-6 py-8 safe-area-top">
<div className="text-center flex-1"> <div className="text-center flex-1">
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.3 }} <motion.div
className="inline-block p-4 bg-white/20 rounded-2xl mb-3"> initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3 }}
className="inline-block p-4 bg-white/20 rounded-2xl mb-3"
>
<Shield className="text-4xl text-[#A70710]" /> <Shield className="text-4xl text-[#A70710]" />
</motion.div> </motion.div>
<h1 className="text-3xl font-bold tracking-wider">Policies</h1> <h1 className="text-3xl font-bold tracking-wider">{title}</h1>
<p className="text-green-100 text-sm mt-1">Your Safety Is Our First Priority</p> <p className="text-green-100 text-sm mt-1">
Your Safety Is Our First Priority
</p>
</div> </div>
<div className="w-12" /> <div className="w-12" />
</div> </div>
</motion.header> </motion.header>
<main className="md:px-4 py-10 max-w-[1400px] mx-auto space-y-8"> <main className="md:px-4 py-10 max-w-[1400px] mx-auto space-y-8">
<motion.div whileHover={{ scale: 1.02 }} className="bg-white/90 backdrop-blur rounded-3xl p-8 shadow-xl border border-green-100"> <motion.div
<div className="flex items-start gap-5"> whileHover={{ scale: 1.01 }}
<div className="p-4 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl shadow-lg"> className="bg-white/90 backdrop-blur rounded-3xl p-8 shadow-xl border border-green-100 mx-auto max-w-[1100px] overflow-hidden"
>
<div>
{/* <div className="p-4 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl shadow-lg">
<Security className="text-white text-3xl" /> <Security className="text-white text-3xl" />
</div> </div> */}
<div> <div>
<h3 className="text-xl font-bold text-green-900 mb-3">Privacy Policy</h3> {/* <h3 className="text-xl font-bold text-green-900 mb-3">{title}</h3> */}
<p className="text-gray-800 text-lg leading-8 font-light tracking-wide">{text}</p> {isLoading && (
<div className="space-y-4">
<Skeleton height={22} width="45%" />
<SkeletonText lines={6} height={14} />
<SkeletonText lines={4} height={14} />
</div>
)}
{isError && (
<p className="text-red-600 text-lg leading-8 font-light tracking-wide">
Failed to load policy.
</p>
)}
{!isLoading && !isError && (
<div
className="text-gray-800 text-[16px] leading-7"
dangerouslySetInnerHTML={{ __html: content }}
/>
)}
</div> </div>
</div> </div>
</motion.div> </motion.div>
<motion.div whileHover={{ scale: 1.02 }} className="bg-white/90 backdrop-blur rounded-3xl p-8 shadow-xl border border-green-100"> <motion.div
<h3 className="text-xl font-bold text-emerald-900 mb-3">Safety Guidelines</h3> initial={{ scale: 0.9 }}
<p className="text-gray-800 text-lg leading-8 font-light tracking-wide">{text}</p> animate={{ scale: 1 }}
</motion.div> className="bg-[#f5fbff] text-black rounded-3xl p-10 text-center shadow-2xl"
>
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="bg-[#f5fbff] text-black rounded-3xl p-10 text-center shadow-2xl">
<h2 className="text-2xl font-bold mb-3">Protected With Love & Care</h2> <h2 className="text-2xl font-bold mb-3">Protected With Love & Care</h2>
<p className="text-black text-lg">Every profile, every chat, every dream safeguarded 24×7.</p> <p className="text-black text-lg">
Every profile, every chat, every dream â safeguarded 24Ã7.
</p>
<div className="flex justify-center gap-3 mt-6"> <div className="flex justify-center gap-3 mt-6">
<VerifiedUser className="text-4xl animate-ping text-[#A70710]" /> <VerifiedUser className="text-4xl animate-ping text-[#A70710]" />
<Shield className="text-5xl text-[#A70710]" /> <Shield className="text-5xl text-[#A70710]" />

View File

@ -1,12 +1,22 @@
import React from 'react'; import React, { useMemo } from 'react';
import { ArrowBackIosNew, Favorite, AutoStories } from '@mui/icons-material'; import { Favorite, AutoStories } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTermsAndPolicies } from '../hooks/useTermsAndPolicies';
const loremText = `It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy.`; import { Skeleton, SkeletonText } from '../components/common/Skeleton';
export default function TermsAndCondition() { export default function TermsAndCondition() {
const navigate = useNavigate(); const [searchParams] = useSearchParams();
const type = searchParams.get("type") || "terms";
const { data, isLoading, isError } = useTermsAndPolicies(type);
const content = useMemo(() => {
return data?.data?.message || "";
}, [data]);
const title = useMemo(() => {
return data?.data?.name || "Terms & Conditions";
}, [data]);
return ( return (
<div className="min-h-screen overflow-hidden relative z-20"> <div className="min-h-screen overflow-hidden relative z-20">
@ -33,7 +43,7 @@ export default function TermsAndCondition() {
<Favorite className="text-4xl text-[#A70710] drop-shadow-lg" /> <Favorite className="text-4xl text-[#A70710] drop-shadow-lg" />
</div> </div>
</motion.div> </motion.div>
<h1 className="text-3xl font-bold tracking-wider">Terms & Conditions</h1> <h1 className="text-3xl font-bold tracking-wider">{title}</h1>
<p className="text-[#ffff] text-sm mt-1 font-medium">Your Trust, Our Promise</p> <p className="text-[#ffff] text-sm mt-1 font-medium">Your Trust, Our Promise</p>
</div> </div>
@ -49,37 +59,38 @@ export default function TermsAndCondition() {
transition={{ delay: 0.4 }} transition={{ delay: 0.4 }}
className="space-y-8" className="space-y-8"
> >
{/* Card 1 */}
<motion.div <motion.div
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.01 }}
className="bg-white/90 backdrop-blur-lg rounded-3xl p-8 shadow-xl border border-amber-100 relative overflow-hidden" className="bg-white/90 backdrop-blur-lg rounded-3xl p-8 shadow-xl border border-amber-100 relative overflow-hidden mx-auto max-w-[1100px]"
> >
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-amber-200 to-transparent rounded-full blur-3xl opacity-40" /> <div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-amber-200 to-transparent rounded-full blur-3xl opacity-40" />
<div className="flex items-start gap-4"> <div className="">
<div className="p-3 bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl shadow-lg"> {/* <div className="p-3 bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl shadow-lg">
<AutoStories className="text-white text-2xl" /> <AutoStories className="text-white text-2xl" />
</div> </div> */}
<div className="flex-1"> <div className="">
<p className="text-gray-800 text-lg leading-8 font-light tracking-wide"> {isLoading && (
{loremText} <div className="space-y-4">
</p> <Skeleton height={22} width="45%" />
<SkeletonText lines={6} height={14} />
<SkeletonText lines={4} height={14} />
</div>
)}
{isError && (
<p className="text-red-600 text-lg leading-8 font-light tracking-wide">
Failed to load terms.
</p>
)}
{!isLoading && !isError && (
<div
className="text-gray-800 text-[16px] leading-7"
dangerouslySetInnerHTML={{ __html: content }}
/>
)}
</div> </div>
</div> </div>
</motion.div> </motion.div>
{/* Card 2 */}
<motion.div
whileHover={{ scale: 1.02 }}
className="bg-gradient-to-r from-pink-50 to-rose-50 rounded-3xl p-8 shadow-xl border border-pink-200 relative overflow-hidden"
>
<div className="absolute -top-10 -right-10 w-40 h-40">
<Favorite className="text-pink-200 text-9xl opacity-50" />
</div>
<p className="text-gray-800 text-lg leading-8 font-light tracking-wide relative z-10">
{loremText}
</p>
</motion.div>
{/* Final Trust Message */} {/* Final Trust Message */}
<motion.div <motion.div
initial={{ scale: 0.9 }} initial={{ scale: 0.9 }}

View File

@ -1,25 +1,22 @@
import { Suspense, lazy } from "react";
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Autoplay } from 'swiper/modules'; import { Navigation, Autoplay } from 'swiper/modules';
import { ChevronLeft, ChevronRight } from 'lucide-react'; import { ChevronLeft, ChevronRight } from 'lucide-react';
import 'swiper/css'; import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import ProfileCompletion from '../components/profiledashboard/ProfileCompletion'; import Skeleton from "../components/common/Skeleton";
import DailyRecommendedCard from '../components/profiledashboard/DailyRecommendedCard';
import MatrimonyArticles from '../components/profiledashboard/MatrimonyArticles';
import MatchingList from '../components/profiledashboard/MatchingList';
import PaidMemberCard from '../components/profiledashboard/PaidMemberCard';
import LazyImage from '../components/common/LazyImage';
import weddingpromo1 from "../assets/images/weddingpromo1.jpg"; import weddingpromo1 from "../assets/images/weddingpromo1.jpg";
import weddingpromo2 from "../assets/images/weddingpromo2.jpg"; import weddingpromo2 from "../assets/images/weddingpromo2.jpg";
import weddingpromo3 from "../assets/images/weddingpromo3.jpg"; import weddingpromo3 from "../assets/images/weddingpromo3.jpg";
import weddingpromo4 from "../assets/images/weddingpromo4.jpg"; import weddingpromo4 from "../assets/images/weddingpromo4.jpg";
import ProfileCard from '../components/common/ProfileCard';
import NewJoinedProfile from '../components/profiledashboard/NewJoinedProfile'; const ProfileCompletion = lazy(() => import("../components/profiledashboard/ProfileCompletion"));
import CustomerSupportCard from '../components/profiledashboard/CustomerSupportCard'; const MatrimonyArticles = lazy(() => import("../components/profiledashboard/MatrimonyArticles"));
import VideoSwiperGallery from '../components/profiledashboard/VideoSwiperGallery'; const MatchingList = lazy(() => import("../components/profiledashboard/MatchingList"));
import Profilecardemo from '../components/ui/ProfileCardDemo'; const VideoSwiperGallery = lazy(() => import("../components/profiledashboard/VideoSwiperGallery"));
const Profilecardemo = lazy(() => import("../components/ui/ProfileCardDemo"));
const images = [ const images = [
@ -35,6 +32,20 @@ const images = [
weddingpromo3 // couple full shot weddingpromo3 // couple full shot
]; ];
const SectionFallback = ({ height = 280 }) => (
<div className="py-10 px-4">
<div className="w-full max-w-[1400px] mx-auto space-y-4">
<Skeleton height={26} width="40%" rounded={10} />
<Skeleton height={14} width="30%" rounded={8} />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, idx) => (
<Skeleton key={idx} height={height} rounded={16} />
))}
</div>
</div>
</div>
);
const UserDashboardHome = () => { const UserDashboardHome = () => {
return ( return (
@ -88,6 +99,8 @@ const UserDashboardHome = () => {
src={img} src={img}
alt={`Slide ${idx + 1}`} alt={`Slide ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
loading={idx === 0 ? "eager" : "lazy"}
decoding="async"
/> />
{/* <LazyImage {/* <LazyImage
@ -192,21 +205,30 @@ const UserDashboardHome = () => {
`}</style> `}</style>
</div> </div>
<Profilecardemo/> <Suspense fallback={<SectionFallback height={320} />}>
<Profilecardemo />
</Suspense>
{/* <DailyRecommendedCard/> */}
{/* <DailyRecommendedCard/> */} <Suspense fallback={<SectionFallback height={220} />}>
<ProfileCompletion />
</Suspense>
{/* <DailyRecommendedCard/> */}
<ProfileCompletion/> <Suspense fallback={<SectionFallback height={280} />}>
{/* <DailyRecommendedCard/> */} <MatrimonyArticles />
</Suspense>
<Suspense fallback={<SectionFallback height={320} />}>
<MatchingList />
</Suspense>
{/* <PaidMemberCard/> */}
<MatrimonyArticles/> {/* <NewJoinedProfile/> */}
<MatchingList/> {/* <CustomerSupportCard/> */}
{/* <PaidMemberCard/> */} <Suspense fallback={<SectionFallback height={240} />}>
<VideoSwiperGallery />
{/* <NewJoinedProfile/> */} </Suspense>
{/* <CustomerSupportCard/> */}
<VideoSwiperGallery/>
</> </>
) )
} }

View File

@ -28,6 +28,8 @@ const registrationformSlice = createSlice({
profiles: [], profiles: [],
}, },
educationalDetails: { educationalDetails: {
collegeName: "",
employeeType: "",
qualification: "", qualification: "",
fieldOfStudy: "", fieldOfStudy: "",
occupation: "", occupation: "",
@ -40,25 +42,58 @@ const registrationformSlice = createSlice({
fatherOccupation: "", fatherOccupation: "",
motherName: "", motherName: "",
motherOccupation: "", motherOccupation: "",
siblings: "", brotherCount: 0,
siblingsStatus: "", sisterCount: 0,
brothers: [],
sisters: [],
familyStatus: "",
nativePlace: "",
}, },
lifestyleDetails: { lifestyleDetails: {
diet: "", diets: [],
// drinking: "", hobbies: [],
// smoking: "", dob: "",
hobbies: "", tob: "",
placeOfBirth: "",
graha: {
1: [],
2: [],
3: [],
4: [],
5: [],
6: [],
7: [],
8: [],
9: [],
10: [],
11: [],
12: [],
},
amsam: {
1: [],
2: [],
3: [],
4: [],
5: [],
6: [],
7: [],
8: [],
9: [],
10: [],
11: [],
12: [],
},
}, },
partnerPreferences: { partnerPreferences: {
ageRange: "", ageRange: "",
religionCaste: "", castes: [],
occupationPref: "", subCastes: [],
lifestylePref: "", occupations: [],
qualificationPref: "", educations: [],
occupationText: "", hobbies: [],
incomePref: "", annualIncome: "",
locationPref: "", states: [],
districts: [],
}, },
}, },
@ -148,8 +183,10 @@ preloadDummyProfile: (state) => {
...state.educationalDetails, ...state.educationalDetails,
qualification: "B.E", qualification: "B.E",
fieldOfStudy: "CSE", fieldOfStudy: "CSE",
collegeName: "ABC Engineering College",
occupation: "Software Engineer", occupation: "Software Engineer",
organization: "ABC Pvt Ltd", organization: "ABC Pvt Ltd",
employeeType: "Private",
income: "10 LPA", income: "10 LPA",
workLocation: "Chennai", workLocation: "Chennai",
}; };
@ -159,26 +196,74 @@ preloadDummyProfile: (state) => {
fatherOccupation: "Business", fatherOccupation: "Business",
motherName: "Mother Name", motherName: "Mother Name",
motherOccupation: "Homemaker", motherOccupation: "Homemaker",
siblings: "1", brotherCount: 1,
siblingsStatus: "Married", sisterCount: 1,
brothers: [
{
name: "Rahul",
occupation: "Engineer",
maritalStatus: "Married",
haveChildrens: 1,
},
],
sisters: [
{
name: "Rohini",
occupation: "Teacher",
maritalStatus: "Married",
haveChildrens: 0,
},
],
familyStatus: 3,
nativePlace: "Chennai",
}; };
state.lifestyleDetails = { state.lifestyleDetails = {
...state.lifestyleDetails, ...state.lifestyleDetails,
diet: "Veg", diets: [1],
// drinking: "No", hobbies: [1, 3],
// smoking: "No", dob: "1995-05-01",
hobbies: "Reading, Music", tob: "09:30",
placeOfBirth: "Chennai",
graha: {
1: ["LAGNAM", "SURIYAN"],
2: [],
3: [],
4: ["CHANDRAN", "CHEVVAI"],
5: [],
6: ["BUDHAN", "GURU"],
7: [],
8: ["SUKRAN", "SANI"],
9: [],
10: ["RAHU", "KETHU"],
11: [],
12: ["MANDHI"],
},
amsam: {
1: ["LAGNAM", "SURIYAN"],
2: [],
3: [],
4: ["CHANDRAN", "CHEVVAI"],
5: [],
6: ["BUDHAN", "GURU"],
7: [],
8: ["SUKRAN", "SANI"],
9: [],
10: ["RAHU", "KETHU"],
11: [],
12: ["MANDHI"],
},
}; };
state.partnerPreferences = { state.partnerPreferences = {
...state.partnerPreferences, ...state.partnerPreferences,
ageRange: "24-30", ageRange: 2,
religionCaste: "Hindu / Brahmin", castes: [1],
occupationPref: "IT / Software", subCastes: [1],
lifestylePref: "Non-smoker, Non-drinker", occupations: [57],
qualificationPref: "Degree", educations: [14],
occupationText: "IT, Banking", hobbies: [1, 3],
incomePref: "10-20 LPA", annualIncome: 1,
locationPref: "Chennai, Bengaluru", states: [31],
districts: [1],
}; };
}, },

View File

@ -1,8 +1,21 @@
import { Suspense } from 'react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import UserRoutes from './UserRoutes'; import UserRoutes from './UserRoutes';
import PublicRoutes from './PublicRoutes'; import PublicRoutes from './PublicRoutes';
import ScrollToTop from '../components/common/ScrollToTop'; import ScrollToTop from '../components/common/ScrollToTop';
import Skeleton from "../components/common/Skeleton";
const RouteFallback = () => (
<div className="min-h-[60vh] flex items-center justify-center px-6">
<div className="w-full max-w-3xl space-y-4">
<Skeleton height={32} rounded={12} />
<Skeleton height={220} rounded={16} />
<Skeleton height={16} />
<Skeleton height={16} width="80%" />
<Skeleton height={16} width="60%" />
</div>
</div>
);
const AppRoutes = () => { const AppRoutes = () => {
return ( return (
@ -10,11 +23,12 @@ const AppRoutes = () => {
<ScrollToTop /> <ScrollToTop />
{/* Wrap UserRoutes inside AuthProvider separately */} {/* Wrap UserRoutes inside AuthProvider separately */}
<Routes> <Suspense fallback={<RouteFallback />}>
{UserRoutes()} <Routes>
{PublicRoutes()} {UserRoutes()}
{PublicRoutes()}
</Routes> </Routes>
</Suspense>
</> </>
); );
}; };

View File

@ -1,13 +1,12 @@
import { lazy } from "react";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import HomePage from "../pages/HomePage";
import LandingLayout from "../layout/LandingLayout";
import LoginPage from "../pages/auth/LoginPage";
import ForgotPasswordPage from "../pages/auth/ForgotPasswordPage";
import ChangePasswordPage from "../components/auth/ChangePasswordForm";
import ProfileLayout from "../layout/ProfileLayout";
import HomeLayout from "../layout/HomeLayout"; import HomeLayout from "../layout/HomeLayout";
import StepperForm from "../feature/StepperForm";
import NotFound from "../pages/NotFound"; const HomePage = lazy(() => import("../pages/HomePage"));
const LoginPage = lazy(() => import("../pages/auth/LoginPage"));
const ForgotPasswordPage = lazy(() => import("../pages/auth/ForgotPasswordPage"));
const StepperForm = lazy(() => import("../feature/StepperForm"));
const NotFound = lazy(() => import("../pages/NotFound"));
const PublicRoutes = () => { const PublicRoutes = () => {
return ( return (
<> <>

View File

@ -1,26 +1,28 @@
import { Route, useNavigate } from "react-router-dom"; import { lazy } from "react";
import ProfileLayout from "../layout/ProfileLayout"; import { Route } from "react-router-dom";
import UserDashboardHome from "../pages/UserDashboardHome"; const ProfileLayout = lazy(() => import("../layout/ProfileLayout"));
import PoliciesPage from '../pages/PoliciesPage'; const MatchesLayout = lazy(() => import("../layout/MatchesLayout"));
import TermsAndConditionPage from '../pages/TermsAndCondition';
import SafetyCentrePage from '../pages/SafetyCentre'; const UserDashboardHome = lazy(() => import("../pages/UserDashboardHome"));
import SubscriptionPlanPage from '../pages/SubscriptionPlan'; const PoliciesPage = lazy(() => import("../pages/PoliciesPage"));
import SubscriptionHistoryPage from '../pages/SubscriptionHistory' const TermsAndConditionPage = lazy(() => import("../pages/TermsAndCondition"));
import MatchesPage from "../pages/MatchesPage"; const SafetyCentrePage = lazy(() => import("../pages/SafetyCentre"));
import InterestSendPage from "../pages/InterestSendPage"; const SubscriptionPlanPage = lazy(() => import("../pages/SubscriptionPlan"));
import BlockedProfileListPage from "../pages/BlockedProfileListPage"; const SubscriptionHistoryPage = lazy(() => import("../pages/SubscriptionHistory"));
import AccountSettingPage from "../pages/AccountSettingsPage"; const MatchesPage = lazy(() => import("../pages/MatchesPage"));
import ProfileDetailPage from "../pages/ProfileDetailPage"; const InterestSendPage = lazy(() => import("../pages/InterestSendPage"));
import ChatUI from "../pages/ChatPage"; const BlockedProfileListPage = lazy(() => import("../pages/BlockedProfileListPage"));
import HoroscopeGenerator from "../pages/HoroscoperGeneratePage"; const AccountSettingPage = lazy(() => import("../pages/AccountSettingsPage"));
import ContactUsPage from "../pages/ContactUsPage"; const ProfileDetailPage = lazy(() => import("../pages/ProfileDetailPage"));
import ChangePasswordPage from "../components/auth/ChangePasswordForm"; const ChatUI = lazy(() => import("../pages/ChatPage"));
import StepperForm from "../feature/StepperForm"; const HoroscopeGenerator = lazy(() => import("../pages/HoroscoperGeneratePage"));
import FilterForm from "../feature/FilterForm"; const ContactUsPage = lazy(() => import("../pages/ContactUsPage"));
import ProfilePreviewPage from "../feature/ProfilePreviewPage"; const ChangePasswordPage = lazy(() => import("../components/auth/ChangePasswordForm"));
import NotificationPage from "../pages/NotificationPage"; const StepperForm = lazy(() => import("../feature/StepperForm"));
import MatchesLayout from "../layout/MatchesLayout"; const FilterForm = lazy(() => import("../feature/FilterForm"));
import ProfileCardDemo from "../components/ui/ProfileCardDemo"; const ProfilePreviewPage = lazy(() => import("../feature/ProfilePreviewPage"));
const NotificationPage = lazy(() => import("../pages/NotificationPage"));
const ProfileCardDemo = lazy(() => import("../components/ui/ProfileCardDemo"));
const UserRoutes = () => { const UserRoutes = () => {

View File

@ -1,3 +1,3 @@
export const isAuthenticated = () => { export const isAuthenticated = () => {
return !!localStorage.getItem("token"); return !!localStorage.getItem("access_token");
}; };