Compare commits

..
16 Commits
80 changed files with 10311 additions and 7437 deletions

No files matched your search

+9
View File
@@ -22,3 +22,12 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Environment variables
.env
.env.local
.env.*.local
# Build artifacts
dist.zip
+53 -9
View File
@@ -1,16 +1,60 @@
# React + Vite
# Thirukalyanam Web Application
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
A modern matrimonial web application built with React, Vite, and Tailwind CSS.
Currently, two official plugins are available:
## Live Application
- [Live Site](https://www.thirukalyanam.amrithaa.net/)
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Features
- Multi-step Registration Form (Personal, Educational, Family, Lifestyle, Partner Preferences)
- User Dashboard
- Real-time Chat (WebSocket)
- Profile Filtering and Matching
- Notification System
## React Compiler
## Tech Stack
- **Frontend**: React 19, Vite
- **Styling**: Tailwind CSS v4, Material UI
- **State Management**: Redux Toolkit, Redux Persist
- **Data Fetching**: React Query (TanStack Query)
- **Icons**: Lucide React, Material Icons
- **Notifications**: Firebase Cloud Messaging
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Getting Started
## Expanding the ESLint configuration
### Prerequisites
- Node.js (v18 or higher)
- npm or yarn
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
### Installation
1. Clone the repository:
```bash
git clone https://gitpro.amrithaa.com/mageshwaran/thirukalyanamweb.git
```
2. Install dependencies:
```bash
npm install
```
3. Create a `.env` file and add the API base URL:
```env
VITE_THIRUKALYANAM_API_BASE_URL=https://www.thirukalyanam.amrithaa.net/backend/api/
```
### Development
Run the development server:
```bash
npm run dev
```
### Production Build
Generate a production-ready build:
```bash
npm run build
```
Preview the build locally:
```bash
npm run preview
```
## License
Proprietary. All rights reserved.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> -->
@@ -14,6 +14,7 @@
/>
<title>thirukalyanam</title>
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
</head>
<body>
<div id="root"></div>
+88
View File
@@ -31,6 +31,9 @@
"react-lazy-load-image-component": "^1.6.3",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.6",
"react-select": "^5.10.2",
"react-window": "^2.2.7",
"redux-persist": "^6.0.0",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17"
},
@@ -1719,6 +1722,31 @@
"integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==",
"license": "Apache-2.0"
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@grpc/grpc-js": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
@@ -7139,6 +7167,12 @@
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -8053,6 +8087,27 @@
"react-dom": ">=18"
}
},
"node_modules/react-select": {
"version": "5.10.2",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
"integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@@ -8116,6 +8171,16 @@
}
}
},
"node_modules/react-window": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
"integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -8205,6 +8270,15 @@
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
"license": "MIT",
"peerDependencies": {
"redux": ">4.0.0"
}
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
@@ -9069,6 +9143,20 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+3
View File
@@ -33,6 +33,9 @@
"react-lazy-load-image-component": "^1.6.3",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.6",
"react-select": "^5.10.2",
"react-window": "^2.2.7",
"redux-persist": "^6.0.0",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17"
},
+70 -42
View File
@@ -1,57 +1,85 @@
export const API_ENDPOINTS = {
LOGOUT: "logout",
TERMS_AND_POLICIES_PRIVACY:"terms-and-policies",
TERMS_AND_POLICIES_PRIVACY: "terms-and-policies",
// registration api's
PERSONAL_DETAILS_MASTER :"personal_details_masters",
CASTE_MASTER : "get_caste_masters",
SUB_CASTE_MASTER : "get_sub_caste_masters",
CITY_MASTER : "get_district_masters",
STAR_MASTER : "get_star_masters",
MOBILE_SEND_OTP: "send_otp",
MOBILE_VERIFY_OTP: "verify_otp",
PERSONAL_DETAILS_MASTER: "personal_details_masters",
CASTE_MASTER: "get_caste_masters",
SUB_CASTE_MASTER: "get_sub_caste_masters",
CITY_MASTER: "get_district_masters",
STAR_MASTER: "get_star_masters",
PATHAM_MASTER: "get_patham_masters",
MOBILE_SEND_OTP: "send_otp",
MOBILE_VERIFY_OTP: "verify_otp",
EDUCATION_DETAILS_MASTER: "educational_details_masters",
EDUCATION_LIST_API:"get_education",
EDUCATION_DETAILS_MASTER: "educational_details_masters",
EDUCATION_LIST_API: "get_education",
FAMILY_DETAILS_MASTER: "family_details_masters", // family details master api
FAMILY_DETAILS_MASTER: "family_details_masters", // family details master api
LIFESTYLE_DETAILS_MASTER:"lifetstyle_details_masters",
LIFESTYLE_DETAILS_MASTER: "lifetstyle_details_masters",
PREFERED_PARTNER_DETAILS_MASTER:"prefered_details_masters",
PREFERED_PARTNER_DETAILS_MASTER: "prefered_details_masters",
REGISTER_STEP1: "register", // register api
REGISTER_STEP2:"update_educational_details", // educational details updated api
REGSITER_STEP3:"update_family_details", // family details updated api
REGISTER_STEP4:"update_lifestyle_details", // lifestyle details updated api
REGISTER_STEP5:"update_preferred_details", // partner preference details updated api
PREVIEW_DETAILS: "get_preview_details",
REVIEWS: "reviews",
REGISTER_STEP1: "register", // register api
REGISTER_STEP2: "update_educational_details", // educational details updated api
REGSITER_STEP3: "update_family_details", // family details updated api
REGISTER_STEP4: "update_lifestyle_details", // lifestyle details updated api
REGISTER_STEP5: "update_preferred_details", // partner preference details updated api
PREVIEW_DETAILS: "get_preview_details",
REVIEWS: "reviews",
// edit api's autopapulated
// edit api's autopapulated
EDIT_PERSONAL_DETAILS: "get_personal_details",
EDIT_EDUCATION_DETAILS: "get_educational_details",
EDIT_FAMILY_DETAILS: "get_family_details",
EDIT_LIFESTYLE_DETAILS: "get_lifestyle_details",
EDIT_PREFERED_PARTNER_DETAILS: "get_preferred_details",
EDIT_PERSONAL_DETAILS: "get_personal_details",
EDIT_EDUCATION_DETAILS: "get_educational_details",
EDIT_FAMILY_DETAILS: "get_family_details",
EDIT_LIFESTYLE_DETAILS: "get_lifestyle_details",
EDIT_PREFERED_PARTNER_DETAILS: "get_preferred_details",
// delete api
// delete api
DELETE_ACCOUNT: "delete_account",
PHONE_NUMBER_VISIBILITY: "get_phone_number_visibility",
UPDATE_PHONE_NUMBER_VISIBILITY: "update_phone_number_visibility",
CHAT_ALERT_NOTIFICATION:"get_chat_alert_notification",
UPDATE_CHAT_ALERT_NOTIFICATION:"update_chat_alert_notification",
PROFILE_PROTECT_API:"get_profile_protection",
UPDATE_PROFILE_PROTECT_API:"update_profile_protection",
MATCH_ALERT:"get_match_alert",
UPDATE_MATCH_ALERT:"update_match_alert",
WHO_CAN_VIEW_MESSAGE:"get_who_can_message_me",
UPDATE_WHO_CAN_VIEW_MESSAGE:"update_who_can_message_me",
CONTACT_US:"get_contact_us",
BE_SAFE_ONLINE:"get_be_safe_online",
NOTIFICATION_LIST:"notification/lists",
NOTIFICATION_COUNT:"notification/un_read_count",
DELETE_ACCOUNT: "delete_account",
PHONE_NUMBER_VISIBILITY: "get_phone_number_visibility",
UPDATE_PHONE_NUMBER_VISIBILITY: "update_phone_number_visibility",
CHAT_ALERT_NOTIFICATION: "get_chat_alert_notification",
UPDATE_CHAT_ALERT_NOTIFICATION: "update_chat_alert_notification",
PROFILE_PROTECT_API: "get_profile_protection",
UPDATE_PROFILE_PROTECT_API: "update_profile_protection",
MATCH_ALERT: "get_match_alert",
UPDATE_MATCH_ALERT: "update_match_alert",
WHO_CAN_VIEW_MESSAGE: "get_who_can_message_me",
UPDATE_WHO_CAN_VIEW_MESSAGE: "update_who_can_message_me",
CONTACT_US: "get_contact_us",
BE_SAFE_ONLINE: "get_be_safe_online",
NOTIFICATION_LIST: "notification/lists",
NOTIFICATION_COUNT: "notification/un_read_count",
// filter with profiles list api's
PROFILES_FILTER_LIST: "profiles/lists",
PROFILES_FILTER_MASTER: "profiles/filter/masters",
DASHBOARD_API: "dashboard",
HEADER_API: "header_data",
SHORTLIST_API: "shortlist_profile",
BLOCK_PROFILE_LIST: "block_profile_list",
REPORT_PROFILE_LIST: "report_profile_list",
PROFILE_DETAIL: "profiles/detail",
INTEREST_LIST: "interest_lists",
UPDATE_INTEREST_STATUS: "interest_status_update",
CHAT_LIST: "chat/lists",
CHAT_MESSAGES: (id) => `chat/${id}/messages`,
UNREAD_CHAT_COUNT: "chat/un_read_chat_count",
INTEREST_SEND: "interest_send",
DAILY_RECOMMENDED_DONT_SHOW: "daily_recomended-dont_show",
VIEW_CONTACT: "profiles/view_contact",
CHAT_CREATE: "chat/create",
SUBSCRIPTION_PLANS: "subscription-plans",
SUBSCRIPTION_PURCHASE_RAZORPAY: "subscription-purchase-razorpay",
SUBSCRIPTION_HISTORY: "subscription-history",
//Report and Block profile
REPORT_PROFILE : "report_profile",
BLOCK_PROFILE: "block_profile",
};
// https://www.thirukalyanam.amrithaa.net/backend/api/report_profile?profile_id=151&reason=test
+12 -4
View File
@@ -7,8 +7,9 @@ import { API_ENDPOINTS } from "./apiEndpoints";
* and default headers for JSON communication.
*/
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL ||
"https://www.thirukalyanam.amrithaa.net/backend/api/" ,
baseURL: (import.meta.env.DEV)
? "/backend/api/"
: (import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL || "https://www.thirukalyanam.amrithaa.net/backend/api/"),
headers: {
"Content-Type": "application/json",
},
@@ -19,7 +20,9 @@ const axiosInstance = axios.create({
* while sharing the same base URL and authorization mechanism.
*/
const apiForFiles = axios.create({
baseURL: import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL,
baseURL: (import.meta.env.DEV)
? "/backend/api/"
: (import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL || "https://www.thirukalyanam.amrithaa.net/backend/api/"),
headers: {
"Content-Type": "multipart/form-data",
},
@@ -123,8 +126,13 @@ export const clearUserData = () => {
export const urlToFile = async (url, filename = "file") => {
try {
// Rewrite URL to use proxy in development to avoid CORS
const finalUrl = (import.meta.env.DEV && url && url.startsWith('https://www.thirukalyanam.amrithaa.net/backend'))
? url.replace('https://www.thirukalyanam.amrithaa.net/backend', '/backend')
: url;
// Use your axios instance to request the URL as a Blob
const response = await apiForFiles.get(url, {
const response = await apiForFiles.get(finalUrl, {
responseType: "blob",
});
+7
View File
@@ -0,0 +1,7 @@
import axiosInstance from "./axiosInstance";
import { API_ENDPOINTS } from "./apiEndpoints";
export const getDashboardDetails = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.DASHBOARD_API);
return res.data;
};
+21
View File
@@ -35,6 +35,13 @@ export const getStarMasters = async (raasi_id) => {
return res.data;
};
export const getPathamMasters = async (star_id) => {
const res = await axiosInstance.get(API_ENDPOINTS.PATHAM_MASTER, {
params: { star_id },
});
return res.data;
};
export const getEducationMasters = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.EDUCATION_DETAILS_MASTER);
return res.data;
@@ -63,3 +70,17 @@ export const getPartnerPreferenceMasters = async () => {
);
return res.data;
};
// profile filter masters
export const getProfilesFilterList = async (filters) => {
const res = await axiosInstance.get(API_ENDPOINTS.PROFILES_FILTER_LIST, {
params: filters,
});
return res.data;
};
export const getProfilesFilterMasters = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.PROFILES_FILTER_MASTER);
return res.data;
};
+6
View File
@@ -5,3 +5,9 @@ export const getPreviewDetails = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.PREVIEW_DETAILS);
return res.data;
};
export const getHeaderDetails = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.HEADER_API);
return res.data;
}
+27
View File
@@ -0,0 +1,27 @@
import axiosInstance from "./axiosInstance";
import { API_ENDPOINTS } from "./apiEndpoints";
export const getSubscriptionPlans = async () => {
const response = await axiosInstance.get(API_ENDPOINTS.SUBSCRIPTION_PLANS);
return response.data;
};
export const purchaseSubscription = async (planId, paymentData = null) => {
if (!paymentData) {
// Step 1: Initiate purchase to get order details
const response = await axiosInstance.post(`${API_ENDPOINTS.SUBSCRIPTION_PURCHASE_RAZORPAY}?supscription_plan_id=${planId}`);
return response.data;
} else {
// Step 2: Verify payment
const response = await axiosInstance.post(API_ENDPOINTS.SUBSCRIPTION_PURCHASE_RAZORPAY, {
...paymentData,
supscription_plan_id: planId
});
return response.data;
}
};
export const getSubscriptionHistory = async () => {
const response = await axiosInstance.get(API_ENDPOINTS.SUBSCRIPTION_HISTORY);
return response.data;
};
+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8716 6.28503V4.88837C11.8716 4.51795 11.7245 4.1627 11.4626 3.90077C11.2006 3.63885 10.8454 3.4917 10.475 3.4917H3.49164C3.12122 3.4917 2.76597 3.63885 2.50404 3.90077C2.24212 4.1627 2.09497 4.51795 2.09497 4.88837V9.07837C2.09497 9.44878 2.24212 9.80403 2.50404 10.066C2.76597 10.3279 3.12122 10.475 3.49164 10.475H4.8883M6.28497 13.2684H13.2683C13.6387 13.2684 13.994 13.1212 14.2559 12.8593C14.5178 12.5974 14.665 12.2421 14.665 11.8717V7.6817C14.665 7.31128 14.5178 6.95603 14.2559 6.69411C13.994 6.43218 13.6387 6.28503 13.2683 6.28503H6.28497C5.91455 6.28503 5.5593 6.43218 5.29738 6.69411C5.03545 6.95603 4.8883 7.31128 4.8883 7.6817V11.8717C4.8883 12.2421 5.03545 12.5974 5.29738 12.8593C5.5593 13.1212 5.91455 13.2684 6.28497 13.2684ZM11.1733 9.7767C11.1733 10.1471 11.0262 10.5024 10.7642 10.7643C10.5023 11.0262 10.1471 11.1734 9.77664 11.1734C9.40622 11.1734 9.05097 11.0262 8.78905 10.7643C8.52712 10.5024 8.37997 10.1471 8.37997 9.7767C8.37997 9.40628 8.52712 9.05103 8.78905 8.78911C9.05097 8.52718 9.40622 8.38003 9.77664 8.38003C10.1471 8.38003 10.5023 8.52718 10.7642 8.78911C11.0262 9.05103 11.1733 9.40628 11.1733 9.7767Z" stroke="#AA5806" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86372 15.4545C7.86372 15.4545 2.79321 11.1842 2.79321 6.98228C2.79321 5.50084 3.38171 4.08007 4.42926 3.03253C5.4768 1.98499 6.89756 1.39648 8.37901 1.39648C9.86046 1.39648 11.2812 1.98499 12.3288 3.03253C13.3763 4.08007 13.9648 5.50084 13.9648 6.98228C13.9648 11.1842 8.8943 15.4545 8.8943 15.4545C8.61222 15.7143 8.1479 15.7115 7.86372 15.4545ZM8.37901 9.42607C8.69993 9.42607 9.01771 9.36286 9.31421 9.24005C9.6107 9.11724 9.8801 8.93723 10.107 8.7103C10.334 8.48338 10.514 8.21397 10.6368 7.91748C10.7596 7.62099 10.8228 7.30321 10.8228 6.98228C10.8228 6.66136 10.7596 6.34358 10.6368 6.04709C10.514 5.75059 10.334 5.48119 10.107 5.25427C9.8801 5.02734 9.6107 4.84733 9.31421 4.72452C9.01771 4.60171 8.69993 4.5385 8.37901 4.5385C7.73088 4.5385 7.10929 4.79597 6.65099 5.25427C6.19269 5.71256 5.93522 6.33415 5.93522 6.98228C5.93522 7.63042 6.19269 8.252 6.65099 8.7103C7.10929 9.1686 7.73088 9.42607 8.37901 9.42607Z" fill="#DF1D46"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.79346 12.5679C2.79346 11.8272 3.08771 11.1168 3.61148 10.593C4.13525 10.0693 4.84563 9.77502 5.58636 9.77502H11.1722C11.9129 9.77502 12.6233 10.0693 13.147 10.593C13.6708 11.1168 13.9651 11.8272 13.9651 12.5679C13.9651 12.9383 13.8179 13.2935 13.556 13.5554C13.2942 13.8172 12.939 13.9644 12.5686 13.9644H4.18991C3.81954 13.9644 3.46435 13.8172 3.20247 13.5554C2.94058 13.2935 2.79346 12.9383 2.79346 12.5679Z" stroke="#0E69BE" stroke-width="1.57101" stroke-linejoin="round"/>
<path d="M8.3791 6.9822C9.53596 6.9822 10.4738 6.04438 10.4738 4.88752C10.4738 3.73066 9.53596 2.79285 8.3791 2.79285C7.22224 2.79285 6.28442 3.73066 6.28442 4.88752C6.28442 6.04438 7.22224 6.9822 8.3791 6.9822Z" stroke="#0E69BE" stroke-width="1.57101"/>
</svg>

After

Width:  |  Height:  |  Size: 847 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.18966 15.361C3.80564 15.361 3.47701 15.2244 3.20377 14.9511C2.93053 14.6779 2.79368 14.349 2.79321 13.9645V2.79293C2.79321 2.40891 2.93006 2.08028 3.20377 1.80704C3.47747 1.5338 3.8061 1.39695 4.18966 1.39648H12.5684C12.9524 1.39648 13.2812 1.53334 13.5549 1.80704C13.8287 2.08074 13.9653 2.40938 13.9648 2.79293V13.9645C13.9648 14.3486 13.8282 14.6774 13.5549 14.9511C13.2817 15.2248 12.9528 15.3614 12.5684 15.361H4.18966ZM4.18966 13.9645H12.5684V2.79293H11.1719V7.68051L9.42635 6.63317L7.68079 7.68051V2.79293H4.18966V13.9645Z" fill="#D00768"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

+254
View File
@@ -0,0 +1,254 @@
import React, { useState, useEffect } from 'react';
import { ArrowLeft, MoreVertical, Loader2 } from 'lucide-react';
import { getCallHistoryMyList, getCallHistoryOthersList, deleteCallHistory } from '../../services/chatApi';
import toast from 'react-hot-toast';
import { useQuery } from '@tanstack/react-query';
import { getHeaderDetails } from '../../api/preview.api';
const CallHistory = ({ onBack }) => {
const [activeTab, setActiveTab] = useState('me');
const [historyList, setHistoryList] = useState([]);
const [loading, setLoading] = useState(true);
const { data: headerData } = useQuery({
queryKey: ["headerDetails"],
queryFn: getHeaderDetails,
});
// console.log("headerData", headerData);
const myDetails = headerData?.myDetails || {};
const [isEditMode, setIsEditMode] = useState(false);
const [selectedIds, setSelectedIds] = useState([]);
const [showMenu, setShowMenu] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const fetchData = async () => {
setLoading(true);
setHistoryList([]);
try {
let res;
if (activeTab === 'me') {
res = await getCallHistoryMyList();
} else {
res = await getCallHistoryOthersList();
}
const data = res?.call_list || res?.data || res || [];
setHistoryList(Array.isArray(data) ? data : []);
} catch (error) {
toast.error("Failed to load call history.");
setHistoryList([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
setIsEditMode(false);
setSelectedIds([]);
}, [activeTab]);
const toggleSelection = (id) => {
if (selectedIds.includes(id)) {
setSelectedIds(selectedIds.filter(item => item !== id));
} else {
setSelectedIds([...selectedIds, id]);
}
};
const handleDelete = async () => {
if (selectedIds.length === 0) return;
setIsDeleting(true);
try {
const res = await deleteCallHistory(selectedIds);
toast.success(res?.message || "History deleted successfully.");
setIsEditMode(false);
setSelectedIds([]);
fetchData();
} catch (error) {
toast.error("Failed to delete history.");
} finally {
setIsDeleting(false);
}
};
const getSkeletons = () => {
return Array(5).fill(0).map((_, idx) => (
<div key={`skel-${idx}`} className="flex items-center gap-3 p-4 border-b border-gray-100 animate-pulse">
<div className="w-12 h-12 bg-gray-200 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-1/3"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
</div>
));
};
return (
<div className="flex flex-col h-full bg-white relative">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-white min-h-[64px]">
<div className="flex items-center gap-3">
<button
onClick={onBack}
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6 text-gray-800" />
</button>
<h2 className="text-xl font-bold text-[#1a237e]">Call History</h2>
</div>
<div className="relative">
<button
onClick={() => setShowMenu(!showMenu)}
className="p-2 rounded-full border border-gray-200 hover:bg-gray-50 flex items-center justify-center transition-colors shadow-sm"
>
<MoreVertical className="w-5 h-5 text-gray-700" />
</button>
{showMenu && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-xl border border-gray-100 z-50 overflow-hidden">
<button
onClick={() => {
setIsEditMode(!isEditMode);
setShowMenu(false);
if (isEditMode) setSelectedIds([]);
}}
className="w-full px-4 py-3 text-left hover:bg-gray-50 text-gray-700 font-medium text-sm transition-colors"
>
{isEditMode ? "Cancel Deletion" : "Delete History"}
</button>
</div>
)}
</div>
</div>
{/* User Info Banner */}
<div className="p-4 flex items-center gap-4 bg-white border-b border-gray-100">
<div className="w-14 h-14 rounded-full overflow-hidden border border-gray-200 shadow-sm relative">
<img
src={myDetails.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"}
alt="Profile"
className="w-full h-full object-cover"
/>
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<h3 className="font-bold text-[#df1d46] text-[17px]">{myDetails.name || "User"}</h3>
{myDetails.is_paid && (
<span className="bg-[#0c8626] text-white text-[10px] font-bold px-2 py-0.5 rounded uppercase tracking-wider">Free</span> // Or Paid dynamically
)}
</div>
<p className="text-sm font-semibold text-gray-800">
ID : <span className="font-bold">{myDetails.member_id || myDetails.profile_id || "N/A"}</span>
</p>
</div>
</div>
{/* Tabs */}
<div className="flex p-4 gap-4 bg-white sticky top-0 z-10 border-b border-gray-100 shadow-sm">
<button
onClick={() => setActiveTab('me')}
className={`flex-1 py-3 text-sm font-bold rounded-lg border transition-all ${activeTab === 'me'
? 'bg-[#df1d46] text-white border-[#df1d46]'
: 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'
}`}
>
Viewed By Me
</button>
<button
onClick={() => setActiveTab('others')}
className={`flex-1 py-3 text-sm font-bold rounded-lg border transition-all ${activeTab === 'others'
? 'bg-[#df1d46] text-white border-[#df1d46]'
: 'bg-white text-gray-600 border-gray-200 hover:bg-gray-50'
}`}
>
Viewed By Others
</button>
</div>
{/* Content List */}
<div className="flex-1 overflow-y-auto pb-20 relative bg-[#fcfcfc]">
{loading ? (
getSkeletons()
) : historyList.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 mt-10">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<span className="text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" /><line x1="22" y1="2" x2="2" y2="22" /></svg>
</span>
</div>
<p className="text-gray-500 font-medium">No Call History found</p>
</div>
) : (
<div className="divide-y divide-gray-100 border-t border-gray-100">
{historyList.map((item, idx) => (
<div
key={item.id || idx}
onClick={() => isEditMode && toggleSelection(item.id)}
className={`flex items-center gap-4 p-4 transition-colors cursor-pointer hover:bg-gray-50 ${isEditMode && selectedIds.includes(item.id) ? 'bg-red-50/50' : 'bg-white'}`}
>
{/* Checkbox for Edit Mode */}
{isEditMode && (
<div className="flex-shrink-0">
<div className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${selectedIds.includes(item.id)
? 'bg-[#df1d46] border-[#df1d46]'
: 'border-gray-300 bg-white'
}`}>
{selectedIds.includes(item.id) && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7" /></svg>
)}
</div>
</div>
)}
{/* Avatar */}
<img
src={item.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"}
alt={item.name}
className="w-14 h-14 rounded-full object-cover border border-gray-100 shadow-sm flex-shrink-0"
/>
{/* Details */}
<div className="flex-1 min-w-0 flex flex-col justify-center">
<div className="flex justify-between items-start mb-1.5">
<h4 className="font-semibold text-gray-900 text-[16px] truncate pr-2">{item.name}</h4>
<span className="text-xs text-gray-500 whitespace-nowrap mt-1">{item.date || item.created_at || "N/A"}</span>
</div>
{item.mobile ? (
<p className="text-sm font-semibold text-gray-800 truncate">
Mobile No : <span className="font-bold">{item.mobile}</span>
</p>
) : (
<p className="text-sm font-semibold text-gray-800 truncate">
ID : <span className="font-bold uppercase">{item.member_id || "N/A"}</span>
</p>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Delete Button (Sticky Bottom) */}
{isEditMode && selectedIds.length > 0 && (
<div className="absolute bottom-4 left-4 right-4 z-20">
<button
onClick={handleDelete}
disabled={isDeleting}
className="w-full bg-[#A80000] hover:bg-red-900 text-white font-bold py-4 rounded-xl shadow-lg shadow-red-200 transition-all flex items-center justify-center gap-2 text-lg active:scale-[0.98] disabled:opacity-70 disabled:active:scale-100"
>
{isDeleting ? <Loader2 className="w-6 h-6 animate-spin" /> : "Delete"}
</button>
</div>
)}
</div>
);
};
export default CallHistory;
@@ -242,7 +242,7 @@ const navigate = useNavigate();
>
<div onClick={(e) => e.stopPropagation()} className="bg-white rounded-2xl shadow-xl overflow-hidden select-none">
<div className="relative">
<div classname=" relative bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]" style={{height:"300px"}}>
<div className=" relative bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]" style={{height:"300px"}}>
<img
src={profile.image}
@@ -0,0 +1,20 @@
import React from "react";
export default function ProfileCardSkeleton() {
return (
<div className="w-full max-w-sm rounded-[10px] border shadow-lg p-4 animate-pulse">
<div className="w-full h-[280px] bg-gray-200 rounded mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/2 mb-3"></div>
<div className="grid grid-cols-2 gap-2">
<div className="h-3 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded"></div>
</div>
</div>
);
}
+620
View File
@@ -0,0 +1,620 @@
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { Crown, Bookmark, X, Heart, Eye, Phone, MessageSquare, ChevronLeft } from "lucide-react";
import VisibilityIcon from "@mui/icons-material/Visibility";
import axiosInstance, { apiForFiles } from "../../api/axiosInstance";
import { API_ENDPOINTS } from "../../api/apiEndpoints";
import UpgradeModal from "./UpgradeModal";
import toast from "react-hot-toast";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
import { sendMessage } from "../../services/chatApi";
import { getHeaderDetails } from "../../api/preview.api";
export default function ProfileCardUI({ profile }) {
const [isLiked, setIsLiked] = useState(false);
const [isShortlisted, setIsShortlisted] = useState(profile.is_shortlisted === 1);
const [isLimitModalOpen, setIsLimitModalOpen] = useState(false);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const [isViewContactModalOpen, setIsViewContactModalOpen] = useState(false);
const [isInterestStatusModalOpen, setIsInterestStatusModalOpen] = useState(false);
const [isInterestRejectedModalOpen, setIsInterestRejectedModalOpen] = useState(false);
const [isContactSuccessModalOpen, setIsContactSuccessModalOpen] = useState(false);
const [isChatConfirmModalOpen, setIsChatConfirmModalOpen] = useState(false);
const [isCreatingChat, setIsCreatingChat] = useState(false);
const [modalTitle, setModalTitle] = useState("");
const [modalMessage, setModalMessage] = useState("");
const [unlockedMobile, setUnlockedMobile] = useState(null);
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: headerData } = useQuery({
queryKey: ["headerDetails"],
queryFn: getHeaderDetails,
staleTime: 60000, // Reuse data for 1 minute
});
const isUserPaid = headerData?.myDetails?.is_paid_member === true;
const handleInterest = async (e) => {
e.stopPropagation();
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
try {
await sendInterest(profile.id);
setIsLiked(true);
toast.success(`Interest sent to ${profile.name}!`, {
icon: '❤️',
style: { borderRadius: '10px', background: '#333', color: '#fff' }
});
} catch (error) {
const msg = error.message || "";
if (msg.toLowerCase().includes("limit") || msg.toLowerCase().includes("reached")) {
setIsLimitModalOpen(true);
} else {
toast.error(msg || "Failed to send interest");
}
}
};
const handleDecline = async (e) => {
e.stopPropagation();
try {
await axiosInstance.post(`${API_ENDPOINTS.UPDATE_INTEREST_STATUS}?profile_id=${profile.id}&status=reject`);
toast.success("Profile declined");
// Optionally hide card or trigger refresh
} catch (error) {
toast.error("Failed to decline profile.");
}
};
const handleCall = (e) => {
e.stopPropagation();
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
const interestStatus = (profile.is_send_interest_status || "").toLowerCase();
if (profile.call_protection === 1) {
if (profile.is_send_interest) {
if (interestStatus === 'pending') {
setModalTitle("Note");
setModalMessage("An interest request has already been sent for this profile. Please wait for their response");
setIsInterestStatusModalOpen(true);
return;
} else if (interestStatus === 'reject' || interestStatus === 'rejected') {
setIsInterestRejectedModalOpen(true);
return;
}
}
}
_handleCallTap();
};
const _handleCallTap = () => {
const currentMobile = unlockedMobile || profile.mobile_number || "";
const mobile = currentMobile.toLowerCase();
if (mobile.includes("upgrade to view")) {
setIsUpgradeModalOpen(true);
} else if (mobile.includes("view contact")) {
setIsViewContactModalOpen(true);
} else {
// It's a real number - Show the success modal with the number
setUnlockedMobile(currentMobile);
setIsContactSuccessModalOpen(true);
}
};
const handleMessage = (e) => {
e.stopPropagation();
// 1. Check if chat already exists
if (profile.chat_id) {
navigate(`/chat/${profile.chat_id}`);
return;
}
// 2. Check membership
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
const interestStatus = (profile.is_send_interest_status || "").toLowerCase();
// 3. Check protection & interest status
if (profile.chat_protection === 1 || profile.call_protection === 1) {
if (profile.is_send_interest) {
if (interestStatus === 'pending') {
setModalTitle("Note");
setModalMessage("An interest request has already been sent for this profile. Please wait for their response");
setIsInterestStatusModalOpen(true);
return;
} else if (interestStatus === 'reject' || interestStatus === 'rejected') {
setIsInterestRejectedModalOpen(true);
return;
}
}
}
// 4. Show confirmation dialog
setIsChatConfirmModalOpen(true);
};
const _handleCreateChat = async () => {
if (isCreatingChat) return;
setIsChatConfirmModalOpen(false);
setIsCreatingChat(true);
try {
const response = await axiosInstance.post(API_ENDPOINTS.CHAT_CREATE, { profile_id: profile.id });
if (response.data?.status === true || response.data?.status === 'success') {
const newChatId = response.data?.chat_id;
// Send default initiation message manually
try {
await sendMessage(newChatId, "This profile has initiated a chat with you");
} catch (msgErr) {
console.error("Error sending initial message:", msgErr);
}
toast.success("Chat initiated!");
navigate(`/chat/${newChatId}`);
queryClient.invalidateQueries();
}
else {
toast.error(response.data?.message || "Could not create chat.");
}
} catch (error) {
console.error("Error creating chat", error);
toast.error("Failed to start conversation");
} finally {
setIsCreatingChat(false);
}
};
const _handleViewContact = async () => {
setIsViewContactModalOpen(false);
try {
const formData = new FormData();
formData.append("profile_id", profile.id);
const response = await apiForFiles.post(API_ENDPOINTS.VIEW_CONTACT, formData);
if (response.data?.status === 'success') {
const newMobile = response.data?.mobile_number;
setUnlockedMobile(newMobile);
setIsContactSuccessModalOpen(true);
toast.success("Contact details unlocked!");
// Refresh all queries to update the UI globally
queryClient.invalidateQueries();
} else {
setIsUpgradeModalOpen(true);
}
} catch (error) {
console.error("Error viewing contact", error);
toast.error("Failed to view contact");
}
};
const handleShortlistClick = async (e) => {
e.stopPropagation();
try {
const res = await shortlistProfile(profile.id);
if (res.status === "success") {
setIsShortlisted(!isShortlisted);
toast.success(res.data.message || "Updated shortlist status");
}
} catch (error) {
toast.error("Failed to update shortlist");
}
};
// Map API fields to UI, handling missing values
const imageSrc = profile.photo || profile.image || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
return (
<>
<UpgradeModal
isOpen={isUpgradeModalOpen}
onClose={() => setIsUpgradeModalOpen(false)}
/>
{/* View Contact Confirmation Modal */}
<AnimatePresence>
{isViewContactModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center relative"
>
<button onClick={() => setIsViewContactModalOpen(false)} className="absolute top-6 left-6 text-gray-400 hover:text-gray-600">
<ChevronLeft size={24} />
</button>
<h3 className="text-2xl font-bold text-gray-900 mb-6">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
You need to view this profile's contact details. If you choose to <span className="text-pink-600 font-bold">"Proceed"</span> one count will be deducted from your subscription.
</p>
<div className="flex gap-4">
<button onClick={() => setIsViewContactModalOpen(false)} className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500 hover:bg-gray-50">Cancel</button>
<button onClick={_handleViewContact} className="flex-1 py-4 bg-[#DF1D46] text-white rounded-xl font-bold hover:bg-red-700 shadow-lg shadow-red-200">Proceed</button>
</div>
</motion.div>
</div>
)}
{/* Interest Status Modal (Pending) */}
{isInterestStatusModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<h3 className="text-2xl font-bold text-gray-900 mb-6">{modalTitle}</h3>
<p className="text-gray-600 mb-8 leading-relaxed">{modalMessage}</p>
<button onClick={() => setIsInterestStatusModalOpen(false)} className="w-full py-4 bg-[#DF1D46] text-white rounded-xl font-bold hover:bg-red-700">OK</button>
</motion.div>
</div>
)}
{/* Interest Rejected Modal */}
{isInterestRejectedModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<div className="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-6">
<X size={40} className="text-[#DF1D46]" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
Your previous interest request was declined. You may resend your interest to this profile
</p>
<button onClick={() => setIsInterestRejectedModalOpen(false)} className="w-full py-4 bg-[#DF1D46] text-white rounded-xl font-bold hover:bg-red-700">OK</button>
</motion.div>
</div>
)}
{/* Contact Success Modal */}
{isContactSuccessModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center mx-auto mb-6">
<Phone size={40} className="text-[#0C8626]" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">Success!</h3>
<p className="text-gray-500 mb-6">The contact number has been unlocked.</p>
<div className="bg-gray-50 rounded-2xl p-6 mb-8 border border-gray-100 min-h-[80px] flex items-center justify-center">
<span className="text-3xl font-black text-gray-900 tracking-wider">
{unlockedMobile || (!profile.mobile_number?.toLowerCase().includes("view") ? profile.mobile_number : "Fetching...")}
</span>
</div>
<div className="flex gap-4">
<button
onClick={() => setIsContactSuccessModalOpen(false)}
className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500 hover:bg-gray-50"
>
Close
</button>
<button
onClick={() => {
const finalMobile = unlockedMobile || profile.mobile_number;
if (finalMobile) {
const cleanMobile = finalMobile.replace(/[^0-9+]/g, '');
window.location.href = `tel:${cleanMobile}`;
}
}}
className="flex-1 py-4 bg-[#0C8626] text-white rounded-xl font-bold hover:bg-green-700 shadow-lg shadow-green-200 flex items-center justify-center gap-2"
>
<Phone size={18} /> Call Now
</button>
</div>
</motion.div>
</div>
)}
{/* Chat Confirmation Modal */}
{isChatConfirmModalOpen && (() => {
const currentMobile = profile.mobile_number || "";
const mobileLower = currentMobile.toLowerCase();
const isMobileVisible = currentMobile !== "" &&
!mobileLower.includes("upgrade") &&
!mobileLower.includes("view contact");
return (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<h3 className="text-2xl font-bold text-gray-900 mb-6">
{isMobileVisible ? "Ready to Chat?" : "Note"}
</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
{isMobileVisible
? `Are you ready to chat with ${currentMobile}?`
: "Starting a conversation will use 1 chat count. Are you ready to proceed?"}
</p>
<div className="flex gap-4">
<button
onClick={() => setIsChatConfirmModalOpen(false)}
className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500 hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={_handleCreateChat}
className={`flex-1 py-4 text-white rounded-xl font-bold transition-all shadow-lg ${isCreatingChat ? "bg-gray-400" : "bg-[#DF1D46] hover:bg-red-700 shadow-red-200"}`}
>
{isCreatingChat ? "Starting..." : "Proceed"}
</button>
</div>
</motion.div>
</div>
);
})()}
</AnimatePresence>
<LimitReachedModal
isOpen={isLimitModalOpen}
onClose={() => setIsLimitModalOpen(false)}
/>
<div
onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200 bg-white cursor-pointer hover:shadow-2xl transition-all duration-300"
>
<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-red-900 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
)}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleShortlistClick}
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 transition-colors ${isShortlisted ? "text-black fill-black" : "text-gray-600"}`} />
<span className="text-[12px] font-medium text-gray-700">
{isShortlisted ? "Shortlisted" : "Shortlist"}
</span>
</motion.button>
<div className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]">
<img
src={imageSrc}
alt={profile.name}
className="w-full h-full object-cover bg-gray-200"
style={{ objectPosition: "top" }}
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/>
</div>
{/* Gradient Overlay */}
<div className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none" style={{ background: "linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%)" }}></div>
<div className="absolute bottom-0 left-0 right-0 p-6 pb-2 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-1 truncate">{profile.name}</h1>
<p className="text-[14px] text-gray-700 leading-relaxed font-medium">ID: {profile.member_id || profile.id}</p>
</div>
</div>
<div className="px-5 py-5 -mt-8 bg-white rounded-t-[32px] relative z-[2] shadow-[0_-12px_40px_rgba(0,0,0,0.1)] min-h-[280px] flex flex-col">
<div className="text-left mb-4">
<h2 className="text-[20px] font-bold text-gray-900 leading-tight">
{profile.name}
</h2>
<div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
<span>ID: {profile.member_id || profile.id}</span>
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<span className="flex items-center gap-1">
<VisibilityIcon sx={{ fontSize: 14 }} />
{profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"}
</span>
</div>
</div>
<div className="space-y-3 mt-2 flex-1">
{(profile.age || profile.height) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
<img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700">
{profile.age ? `${profile.age} yrs` : ""}
{profile.age && profile.height ? ", " : ""}
{profile.height ? `${profile.height} cm` : ""}
</span>
</div>
)}
{(profile.religion_name || profile.caste_name) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-pink-50 flex items-center justify-center flex-shrink-0">
<img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{profile.religion_name}
{profile.religion_name && profile.caste_name ? " / " : ""}
{profile.caste_name}
</span>
</div>
)}
{(profile.annual_income_name || profile.income) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-green-50 flex items-center justify-center flex-shrink-0">
<img src={cashIcon} alt="Cash" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{profile.annual_income_name || profile.income}
</span>
</div>
)}
{(profile.district_name || profile.location) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0">
<img src={locationIcon} alt="Location" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{profile.district_name || profile.location}
</span>
</div>
)}
</div>
<div className="flex gap-3 mt-6">
{profile.is_send_interest_received && profile.statusReceived?.toLowerCase() === 'pending' ? (
<>
<button
onClick={handleInterest}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-[#0C8626] text-white rounded-2xl font-bold text-[13px] hover:bg-green-700 transition-all active:scale-[0.98]"
>
<Heart size={16} fill="white" /> Accept
</button>
<button
onClick={handleDecline}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-white border border-red-200 text-red-600 rounded-2xl font-bold text-[13px] hover:bg-red-50 transition-all active:scale-[0.98]"
>
<X size={16} /> Reject
</button>
</>
) : (!(profile.is_send_interest || isLiked) && !profile.is_send_interest_received) ? (
<>
<button
onClick={handleInterest}
className={`flex-[1.2] flex items-center justify-center gap-2 py-3 rounded-2xl font-bold text-[13px] transition-all active:scale-[0.98] shadow-lg shadow-green-900/10 ${isLiked ? "bg-green-100 text-green-700 border border-green-200" : "bg-[#034E08] text-white hover:bg-green-800"}`}
>
<Heart size={16} fill={isLiked ? "#034E08" : "white"} />
{isLiked ? "Sent" : "Interest"}
</button>
<button
onClick={(e) => { e.stopPropagation(); navigate(`/profile-details/${profile.id}`); }}
className="flex-1 py-3 bg-gray-50 border border-gray-100 text-gray-600 rounded-2xl font-bold text-[13px] flex items-center justify-center gap-2 hover:bg-gray-100 transition-all active:scale-[0.98]"
>
<Eye size={16} /> Detail
</button>
</>
) : (
<>
<button
onClick={handleCall}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-white border border-green-200 text-green-700 rounded-2xl font-bold text-[13px] hover:bg-green-50 transition-all active:scale-[0.98]"
>
<Phone size={16} /> Call
</button>
<button
onClick={handleMessage}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-white border border-red-200 text-red-600 rounded-2xl font-bold text-[13px] hover:bg-red-50 transition-all active:scale-[0.98]"
>
<MessageSquare size={16} /> Message
</button>
</>
)}
</div>
</div>
</div>
</>
);
}
const LimitReachedModal = ({ isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[10000] p-4">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="bg-white rounded-3xl p-8 max-w-sm w-full text-center shadow-2xl relative"
>
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
>
<X className="w-6 h-6" />
</button>
<div className="w-20 h-20 bg-amber-50 rounded-full flex items-center justify-center mx-auto mb-6">
<Clock className="w-10 h-10 text-amber-600" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-3">Limit Reached!</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
You've reached your daily limit for sending interests. Please try again tomorrow or upgrade your plan for more daily connections!
</p>
<button
onClick={onClose}
className="w-full py-4 bg-[#034E08] text-white rounded-2xl font-bold hover:bg-green-800 transition-colors"
>
Got it
</button>
</motion.div>
</div>
);
};
+130 -16
View File
@@ -4,6 +4,7 @@ import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import SwipeableDrawer from "@mui/material/SwipeableDrawer";
import { useWebSocket } from "../../hooks/useWebSocket";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
@@ -19,7 +20,7 @@ import Button from "@mui/material/Button";
import LazyImage from "./LazyImage";
import Logo from "../../assets/images/logo.png";
import { useLocation, useNavigate } from "react-router-dom";
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect, useMemo } from "react";
import { useTheme, useMediaQuery, ListItemIcon } from "@mui/material";
import { Home, Users, Heart, MessageCircle, Search, Bell } from "lucide-react";
import { isAuthenticated } from "../../utills/auth";
@@ -27,13 +28,15 @@ import userimg from "../../assets/images/bride1.jpg"
import axiosInstance, { logoutAPI } from "../../api/axiosInstance";
import toast from "react-hot-toast";
import { API_ENDPOINTS } from "../../api/apiEndpoints";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSelector } from "react-redux";
import { getHeaderDetails } from "../../api/preview.api";
const NAV_LINKS = [
// { label: "Home", path: "/" },
{ label: "Matches", path: "/matches" },
// { label: "ProfileCard", path: "/profile-card" },
{ label: "Interest", path: "/interest" },
{ label: "Horoscope", path: "/horoscoper-generate" },
// { label: "Horoscope", path: "/horoscoper-generate" },
{ label: "Messages", path: "/chat" },
{ label: "Search", path: "/matches" },
{ label: "Notifications", path: "/notifications" }
@@ -158,6 +161,15 @@ const ProfileHeader = () => {
const [profileDrawerOpen, setProfileDrawerOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [logoutModalOpen, setLogoutModalOpen] = useState(false);
const queryClient = useQueryClient();
const { personalDetails } = useSelector((state) => state.registerform);
const profileImage =
personalDetails?.profiles?.[0]?.preview ||
personalDetails?.profiles?.[0]?.url ||
personalDetails?.profiles?.[0] ||
userimg;
const theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up("md"));
@@ -179,11 +191,105 @@ const ProfileHeader = () => {
return res.data;
},
enabled: !!auth,
refetchInterval: 60000,
});
// WebSocket for real-time updates - Match the robust strategy from ChatPage
const profileId = localStorage.getItem("profile_id");
const userId = localStorage.getItem("user_id");
const wsChannels = useMemo(() => {
const channels = [];
if (profileId && profileId !== "null") {
channels.push(`user-chat-notification${profileId}`);
channels.push(`user-chat-notification.${profileId}`);
channels.push(`partner-chat${profileId}`);
channels.push(`partner-chat.${profileId}`);
}
if (userId && userId !== "null") {
channels.push(`user-notification${userId}`);
channels.push(`user-notification.${userId}`);
}
return [...new Set(channels.filter(Boolean))];
}, [profileId, userId]);
const { messages: wsMessages, isConnected } = useWebSocket(wsChannels);
const processedMsgCount = useRef(wsMessages.length);
useEffect(() => {
if (isConnected) {
console.log("[HEADER-WS] Connected, refreshing badges...");
queryClient.invalidateQueries({ queryKey: ["notificationCount"] });
queryClient.invalidateQueries({ queryKey: ["unreadChatCount"] });
}
}, [isConnected, queryClient]);
useEffect(() => {
if (wsMessages.length > processedMsgCount.current) {
const newWsMsgs = wsMessages.slice(processedMsgCount.current);
console.log(`[HEADER-WS] Detected ${newWsMsgs.length} new signals.`);
let shouldRefresh = false;
newWsMsgs.forEach(lastMsg => {
if (lastMsg.event?.startsWith('pusher:')) return;
// Lenient detection matching ChatPage
const isMessageEvent = lastMsg.event?.toLowerCase().includes('message') ||
lastMsg.event?.toLowerCase().includes('chat') ||
lastMsg.event?.toLowerCase().includes('notification');
if (isMessageEvent) {
console.log(`[HEADER-WS] Relevant event detected: ${lastMsg.event}, refreshing counts...`);
shouldRefresh = true;
}
});
if (shouldRefresh) {
queryClient.invalidateQueries({ queryKey: ["unreadChatCount"] });
queryClient.invalidateQueries({ queryKey: ["notificationCount"] });
}
processedMsgCount.current = wsMessages.length;
}
}, [wsMessages, queryClient]);
const { data: chatCountData } = useQuery({
queryKey: ["unreadChatCount"],
queryFn: async () => {
const res = await axiosInstance.get(API_ENDPOINTS.UNREAD_CHAT_COUNT);
return res.data;
},
enabled: !!auth,
refetchInterval: 30000,
});
const notificationCount = notificationData?.count || 0;
const chatCount = chatCountData?.count || 0;
const { data: headerData } = useQuery({
queryKey: ["headerDetails"],
queryFn: getHeaderDetails,
enabled: !!auth,
});
// AUTO-SYNC: Recover missing IDs from Header API data
useEffect(() => {
if (headerData?.myDetails) {
const myId = headerData.myDetails.id || headerData.myDetails.profile_id;
const uId = headerData.myDetails.user_id;
if (myId && (localStorage.getItem("profile_id") === "null" || !localStorage.getItem("profile_id"))) {
localStorage.setItem("profile_id", myId);
console.log("Header API auto-synced profileId:", myId);
}
if (uId && (localStorage.getItem("user_id") === "null" || !localStorage.getItem("user_id"))) {
localStorage.setItem("user_id", uId);
console.log("Header API auto-synced userId:", uId);
}
}
}, [headerData]);
const apiProfileImage = headerData?.myDetails?.profile;
const handleMenuClick = (item) => {
if (item.action === "delete") {
@@ -200,7 +306,7 @@ const ProfileHeader = () => {
const deleteAccountMutation = useMutation({
mutationFn: async () => {
return await axiosInstance.delete(API_ENDPOINTS.DELETE_ACCOUNT);
return await axiosInstance.post(API_ENDPOINTS.DELETE_ACCOUNT);
},
onSuccess: (response) => {
toast.success(response?.data?.message || "Account deleted successfully");
@@ -310,10 +416,17 @@ const ProfileHeader = () => {
{getNavIcon(index)}
</ListItemIcon>
<ListItemText primary={
<div className="flex items-center justify-between">
{label}
<div className="flex items-center justify-between w-full pr-4">
<span>{label}</span>
{label === "Notifications" && notificationCount > 0 && (
<span className="bg-red-600 text-white text-xs px-2 py-0.5 rounded-full">{notificationCount}</span>
<span className="bg-red-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] flex items-center justify-center ml-2">
{notificationCount}
</span>
)}
{label === "Messages" && chatCount > 0 && (
<span className="bg-red-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] flex items-center justify-center ml-2">
{chatCount}
</span>
)}
</div>
} />
@@ -326,14 +439,13 @@ const ProfileHeader = () => {
return (
<>
<AppBar position="sticky" sx={{ backgroundColor: "#fff" }}>
<AppBar position="sticky" sx={{ backgroundColor: "#fff", zIndex: 1000 }}>
<Container maxWidth="xl">
<Toolbar disableGutters>
<Box onClick={() => navigate("/")} sx={{ display: { xs: "none", md: "flex" }, mr: 1 }}>
<LazyImage
src={Logo}
className="w-full h-[70px] my-2 rounded-lg object-cover cursor-pointer"
className="h-[50px] w-auto my-1 rounded-lg object-contain cursor-pointer"
/>
</Box>
@@ -349,8 +461,7 @@ const ProfileHeader = () => {
>
<LazyImage
src={Logo}
className="w-full h-[50px] rounded-lg object-cover"
className="h-[40px] w-auto rounded-lg object-contain"
/>
</Box>
@@ -359,7 +470,10 @@ const ProfileHeader = () => {
items={NAV_LINKS.map(link => link.label)}
color="#034E08"
activeItem={currentLabel}
badges={{ "Notifications": notificationCount }}
badges={{
"Notifications": notificationCount,
"Messages": chatCount
}}
onItemClick={(item) => {
setSelectedItem(item);
const link = NAV_LINKS.find(l => l.label === item);
@@ -369,16 +483,16 @@ const ProfileHeader = () => {
</Box>
{(auth ? (
<Box sx={{ flexGrow: 0 }}>
<Box key="user-menu-box" sx={{ flexGrow: 0 }}>
<Tooltip title="Account Menu">
<IconButton onClick={toggleProfileDrawer(true)}>
<Avatar sx={{width:"50px", height:"50px"}} src={userimg || "/static/images/avatar/2.jpg" }/>
<Avatar sx={{width:"50px", height:"50px"}} src={apiProfileImage || profileImage || userimg || "/static/images/avatar/2.jpg" }/>
</IconButton>
</Tooltip>
</Box>
):( <button className="ml-1 bg-red-900 text-white px-4 py-2 rounded-md hover:bg-red-800 transition-colors"
):( <button key="sign-in-btn" className="ml-1 bg-red-900 text-white px-4 py-2 rounded-md hover:bg-red-800 transition-colors"
onClick={() => navigate("/login")}>Sign In / Sign Up</button>))}
+40 -4
View File
@@ -52,10 +52,6 @@ export const SkeletonText = ({
height = 12,
className = "",
}) => {
useEffect(() => {
injectSkeletonStyles();
}, []);
return (
<div className={className} style={{ display: "grid", gap }}>
{Array.from({ length: lines }).map((_, idx) => (
@@ -69,4 +65,44 @@ export const SkeletonText = ({
);
};
export const SkeletonPage = ({ className = "" }) => {
return (
<div className={className} style={{ width: "100%", padding: "20px" }}>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "24px" }}>
<Skeleton width="150px" height={40} />
<div style={{ display: "flex", gap: "16px" }}>
<Skeleton width="80px" height={36} />
<Skeleton width="80px" height={36} />
<Skeleton width="80px" height={36} />
</div>
</div>
{/* Banner */}
<div style={{ marginBottom: "32px" }}>
<Skeleton width="100%" height={250} rounded={12} />
</div>
{/* 4 Cards */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "24px", marginBottom: "40px" }}>
{Array.from({ length: 4 }).map((_, idx) => (
<div key={idx} style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<Skeleton width="100%" height={180} rounded={8} />
<Skeleton width="80%" height={20} />
<Skeleton width="60%" height={20} />
</div>
))}
</div>
{/* Footer */}
<div style={{ borderTop: "1px solid #e5e7eb", paddingTop: "32px", display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "24px" }}>
<SkeletonText lines={3} />
<SkeletonText lines={3} />
<SkeletonText lines={3} />
<SkeletonText lines={3} />
</div>
</div>
);
};
export default Skeleton;
+75
View File
@@ -0,0 +1,75 @@
import { useState, useEffect } from 'react';
const TinderCard = ({ children, onSwipe, onCardLeftScreen, preventSwipe, className }) => {
const [pos, setPos] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [gone, setGone] = useState(false);
useEffect(() => {
setPos({ x: 0, y: 0 });
setDragging(false);
setGone(false);
}, []);
const handleStart = (clientX, clientY) => {
if (gone) return;
setDragging(true);
setStartPos({ x: clientX - pos.x, y: clientY - pos.y });
};
const handleMove = (clientX, clientY) => {
if (!dragging || gone) return;
setPos({ x: clientX - startPos.x, y: clientY - startPos.y });
};
const handleEnd = () => {
if (gone) return;
setDragging(false);
if (Math.abs(pos.x) > 120) {
const dir = pos.x > 0 ? 'right' : 'left';
if (onSwipe) onSwipe(dir);
setGone(true);
setTimeout(() => onCardLeftScreen && onCardLeftScreen(dir), 300);
} else {
setPos({ x: 0, y: 0 });
}
};
if (gone) return null;
const rotation = pos.x / 20;
const opacity = Math.min(Math.abs(pos.x) / 100, 1);
return (
<div
className={className}
style={{
transform: `translate(${pos.x}px, ${pos.y}px) rotate(${rotation}deg)`,
transition: dragging ? 'none' : 'transform 0.3s ease-out',
touchAction: 'none'
}}
onMouseDown={(e) => handleStart(e.clientX, e.clientY)}
onMouseMove={(e) => handleMove(e.clientX, e.clientY)}
onMouseUp={handleEnd}
onMouseLeave={() => dragging && handleEnd()}
onTouchStart={(e) => handleStart(e.touches[0].clientX, e.touches[0].clientY)}
onTouchMove={(e) => handleMove(e.touches[0].clientX, e.touches[0].clientY)}
onTouchEnd={handleEnd}
>
{pos.x > 50 && (
<div className="absolute top-8 left-8 z-10 border-4 border-green-500 text-green-500 px-4 py-2 rounded-lg text-2xl font-bold rotate-[-20deg]" style={{ opacity }}>
LIKE
</div>
)}
{pos.x < -50 && (
<div className="absolute top-8 right-8 z-10 border-4 border-red-500 text-red-500 px-4 py-2 rounded-lg text-2xl font-bold rotate-[20deg]" style={{ opacity }}>
NOPE
</div>
)}
{children}
</div>
);
};
export default TinderCard;
+62
View File
@@ -0,0 +1,62 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
const UpgradeModal = ({ isOpen, onClose }) => {
const navigate = useNavigate();
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
/>
{/* Modal Content */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative bg-white rounded-[32px] shadow-2xl w-full max-w-sm overflow-hidden p-10 flex flex-col items-center text-center"
>
<h4 className="text-[22px] font-bold text-gray-900 mb-6">Note</h4>
<h3 className="text-[20px] font-bold text-gray-900 mb-4 leading-tight">
Membership Upgrade Required
</h3>
<p className="text-[#888] text-[15px] leading-relaxed mb-10 px-2 font-medium">
You are currently a Free Member. Upgrade to a Premium Plan to start sending interests and connecting with your perfect match.
</p>
<div className="flex w-full gap-4">
<button
onClick={onClose}
className="flex-1 py-4 border border-gray-200 text-gray-500 rounded-2xl font-bold text-[16px] hover:bg-gray-50 transition-all active:scale-[0.98]"
>
Later
</button>
<button
onClick={() => {
navigate('/subscription-plan');
onClose();
}}
className="flex-1 py-4 bg-[#e91e4a] text-white rounded-2xl font-bold text-[16px] shadow-lg shadow-red-200 hover:bg-[#d81b45] transition-all active:scale-[0.98]"
>
Upgrade Now
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
};
export default UpgradeModal;
+1 -1
View File
@@ -125,7 +125,7 @@ const AppPromoteSection = () => {
className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16 max-w-6xl mx-auto"
>
{features.map((feature, index) => (
<div className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
<div key={index} className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
<BorderBeam
colorFrom="#ff0000ff"
colorTo="#338105ff"
+173 -332
View File
@@ -1,15 +1,10 @@
import React, { useState } from "react";
import { Crown, Bookmark, CurrencyIcon, Currency, Wallet, Receipt, Sparkles, MoonStar, IdCard, RockingChair, LocateFixed, School, WorkflowIcon } from "lucide-react";
import CakeIcon from "@mui/icons-material/Cake";
import GroupsIcon from "@mui/icons-material/Groups";
import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import React, { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react";
import PersonIcon from "@mui/icons-material/Person";
import StarIcon from "@mui/icons-material/Star";
import VisibilityIcon from "@mui/icons-material/Visibility";
import PersonAddIcon from "@mui/icons-material/PersonAdd";
import { motion } from "framer-motion";
import FilterModal from "../../feature/FilterModal";
import bride1 from "../../assets/images/bride1.jpg";
import bride2 from "../../assets/images/bride2.jpg";
@@ -23,349 +18,142 @@ import groom4 from "../../assets/images/groom4.jpg";
import horoscope from "../../assets/images/horoscopeicon.png";
import { useNavigate } from "react-router-dom";
import { Button, Fab } from "@mui/material";
import MessageIcon from "@mui/icons-material/Message";
import PhoneIcon from "@mui/icons-material/Phone";
// Profile Card Component
function ProfileCard({ profile }) {
const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
return (
<div
onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200"
>
<div className="relative">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute top-4 left-4 z-10 bg-red-900 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"
>
<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"
className="w-full h-full object-cover bg-gray-200"
style={{
// objectFit:"inherit",
objectPosition: "top",
}}
/>
</div>
<div
className="absolute bottom-0 left-0 right-0 h-25 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: {profile.id}
</p>
</div>
</div>
<div className="px-4 pt-2 pb-4 flex flex-col gap-2 bg-white">
<div className="flex items-center gap-2">
<VisibilityIcon />
<span className="text-[14px] text-gray-900">{profile.lastseen}</span>
</div>
<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-semibold text-gray-900">
{profile.age} yr
</span>
</div>
<div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
{profile.height} cm
</span>
</div>
<div className="flex items-center gap-2">
<Receipt className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
5 - 10 LPA
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<MoonStar className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
Aries
</span>
</div>
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
Scorpio
</span>
</div>
<div className="flex items-center gap-2">
<IdCard className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
Bramin
</span>
</div>
</div>
<div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
{profile.location}
</span>
</div>
<div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<button
onClick={(e) => {
e.stopPropagation();
// your decline logic
}} className="gap-2 px-3 w-[fit-content] bg-red-50 border-1 border-red-200
font-400 text-base py-1.5 rounded-[20px] shadow-md text-[14px]
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-green-50 border-1 border-green-200 font-400 text-base text-[14px]
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 className="flex gap-3 my-2 justify-between w-full px-[0px]">
<div className="flex gap-2">
<Fab size="medium" color="primary" aria-label="add">
<MessageIcon />
</Fab>
<Fab size="medium" color="secondary" aria-label="add">
<PhoneIcon />
</Fab>
</div>
<Button
variant="contained"
color="#f5fbff"
onClick={(e) => {
e.stopPropagation();
navigate(`/profile-details/${profile.id}`);
}}
sx={{
color: "#000000",
background: "#f5fbff",
fontWeight: "600",
borderRadius: "30px",
}}
>
View Details
</Button>
</div> */}
</div>
</div>
);
}
import toast from "react-hot-toast";
import { useSelector, useDispatch } from "react-redux";
import { updateFilter } from "../../redux/filterSlice";
import { useProfiles } from "../../hooks/useProfiles";
import ProfileCardUI from "../common/ProfileCardUI";
import ProfileCardSkeleton from "../common/ProfileCardSkeleton";
// Main Component
export default function MatchesInterface() {
const [showSkeleton, setShowSkeleton] = React.useState(false);
const navigate = useNavigate();
const [selectedTab, setSelectedTab] = useState("your-matches");
const dispatch = useDispatch();
const filters = useSelector((state) => state.filters);
const filterType = filters.filter_type;
const selectedTab = filterType || "all_matches";
const isPaidMember = filters.isPaidMember;
const { ref, inView } = useInView({
threshold: 0,
rootMargin: "300px"
});
// Fetch real profiles data
const {
data: profilesData,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useProfiles(filters);
const profiles =
profilesData?.pages.flatMap((page) => page?.data || []) || [];
// const { ref, inView } = useInView();
// useEffect(() => {
// if (inView && hasNextPage && !isFetchingNextPage) {
// fetchNextPage();
// }
// }, [inView, hasNextPage, isFetchingNextPage]);
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
setShowSkeleton(true); // show skeleton
const timer = setTimeout(() => {
fetchNextPage();
setShowSkeleton(false); // hide skeleton after API call
}, 120); // 0.5 seconds
return () => clearTimeout(timer);
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
console.log("Fetched profiles:", profiles);
console.log({
inView,
hasNextPage,
isFetchingNextPage,
});
const tabs = [
{
id: "your-matches",
id: "all_matches",
icon: <PersonIcon className="w-6 h-6" />,
title: "Your Matches",
description: "View all the profiles that match your preferences",
category: "All Matches",
title: "All Matches",
description: "Profiles matching your preferences",
category: "Primary Matches",
},
{
id: "shortlisted-by-you",
id: "newly_joined",
icon: <RockingChair className="w-6 h-6" />,
title: "Newly Joined",
description: "Joined within the last 30 days",
category: "Primary Matches",
},
{
id: "shorlisted_by_you",
icon: <StarIcon className="w-6 h-6" />,
title: "Shortlisted by you",
description: "Matches you have shortlisted",
category: "Based on activity",
category: "Your Activity",
},
{
id: "viewed-you",
id: "viewed_you",
icon: <VisibilityIcon className="w-6 h-6" />,
title: "Viewed you",
description: "Matches who have viewed your profile",
category: "Based on activity",
description: "Profiles who viewed you",
category: "Your Activity",
},
{
id: "shortlisted-you",
id: "shorlisted_you",
icon: <PersonAddIcon className="w-6 h-6" />,
title: "Shortlisted you",
description: "Matches who have shortlisted your profile",
category: "Based on activity",
description: "Profiles who shortlisted you",
category: "Your Activity",
},
{
id: "viewed-by-you",
id: "viewed_by_you",
icon: <VisibilityIcon className="w-6 h-6" />,
title: "Viewed by you",
description: "Matches you have viewed",
category: "Based on activity",
description: "Profiles you have viewed",
category: "Your Activity",
},
{
id: "newly-joined",
icon: <RockingChair className="w-6 h-6" />,
title: "Newly Joined",
description: "Matches who Joined within the last 30 days",
category: "Based on activity",
},
{
id: "location",
id: "location_matches",
icon: <LocateFixed className="w-6 h-6" />,
title: "Location matches",
description: "Matches near your location",
category: "Based on activity",
category: "Smart Matches",
},
{
id: "education",
id: "education_matches",
icon: <School className="w-6 h-6" />,
title: "Education matches",
description: "Matches near your education match",
category: "Based on activity",
description: "Profiles with similar education",
category: "Smart Matches",
},
{
id: "job",
id: "job_matches",
icon: <WorkflowIcon className="w-6 h-6" />,
title: "Job matches",
description: "Matches near your job",
category: "Based on activity",
description: "Profiles with similar profession",
category: "Smart Matches",
},
];
const profiles = [
{
id: "JB2847593",
name: "Jerome Bell",
age: 22,
height: "5.2",
lastseen: "Last seen 14 Nov 2025",
education: "BCA / Data analyst",
location: "Chennai",
image: bride1,
},
{
id: "SA8392847",
name: "Sarah Anderson",
age: 24,
height: "5.4",
lastseen: "Last seen 14 Nov 2025",
education: "MBA / Marketing Manager",
location: "Bangalore",
image: bride4,
},
{
id: "PR9384756",
name: "Priya Reddy",
age: 23,
height: "5.3",
lastseen: "Last seen 14 Nov 2025",
education: "B.Tech / Software Engineer",
location: "Hyderabad",
image: bride2,
},
{
id: "AN4758392",
name: "Ananya Krishnan",
age: 25,
height: "5.5",
lastseen: "Last seen 14 Nov 2025",
education: "MD / Doctor",
location: "Kochi",
image: bride3,
},
{
id: "AN4758392",
name: "Ananya Krishnan",
age: 25,
height: "5.5",
lastseen: "Last seen 14 Nov 2025",
education: "MD / Doctor",
location: "Kochi",
image: groom1,
},
{
id: "AN4758392",
name: "Ananya Krishnan",
age: 25,
height: "5.5",
lastseen: "Last seen 14 Nov 2025",
education: "MD / Doctor",
location: "Kochi",
image: groom2,
},
{
id: "AN4758392",
name: "Ananya Krishnan",
age: 25,
height: "5.5",
lastseen: "Last seen 14 Nov 2025",
education: "MD / Doctor",
location: "Kochi",
image: groom4,
},
{
id: "AN4758392",
name: "Ananya Krishnan",
age: 25,
height: "5.5",
lastseen: "Last seen 14 Nov 2025",
education: "MD / Doctor",
location: "Kochi",
image: groom3,
},
];
let currentCategory = "";
@@ -381,10 +169,11 @@ export default function MatchesInterface() {
<div className="w-full md:w-80">
<div
className="rounded-[10px] border border-gray-200 bg-white my-6
shadow-lg h-[400px] md:h-[600px] overflow-y-auto md:sticky md:top-[150px]"
className="relative rounded-[15px] border border-gray-200 bg-white my-6
shadow-xl h-[450px] md:h-[calc(100vh-200px)] overflow-y-auto md:sticky md:top-[120px] custom-sidebar-scrollbar"
>
<div className="py-2 px-4 sticky top-0 bg-[#fff] ">
<h2 className="text-xl font-bold text-green-900 mb-4 mt-6 first:mt-0">
Filter Matches </h2>
@@ -401,22 +190,25 @@ export default function MatchesInterface() {
return (
<div key={tab.id}>
{showCategory && (
<h2 className="text-xl font-bold text-gray-900 mb-4 mt-6 first:mt-0">
<h2 className="text-xs font-bold text-green-700 uppercase tracking-widest mb-3 mt-6 first:mt-0 px-2 opacity-80">
{tab.category}
</h2>
)}
<div
onClick={() => setSelectedTab(tab.id)}
className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${
selectedTab === tab.id
onClick={() => {
const finalFilterType = tab.id === "all_matches" ? "" : tab.id;
dispatch(updateFilter({ filter_type: finalFilterType }));
}}
className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${selectedTab === tab.id
? "bg-green-50 border-l-4 border-green-600"
: "hover:bg-gray-50"
}`}
>
<div className="flex items-start gap-3">
<div
className={`mt-1 ${
selectedTab === tab.id
className={`mt-1 ${selectedTab === tab.id
? "text-green-600"
: "text-gray-600"
}`}
@@ -426,8 +218,7 @@ export default function MatchesInterface() {
<div className="flex-1">
<div className="flex items-center justify-between">
<h3
className={`font-semibold text-base ${
selectedTab === tab.id
className={`font-semibold text-base ${selectedTab === tab.id
? "text-green-900"
: "text-gray-900"
}`}
@@ -435,8 +226,7 @@ export default function MatchesInterface() {
{tab.title}
</h3>
<svg
className={`w-5 h-5 ${
selectedTab === tab.id
className={`w-5 h-5 ${selectedTab === tab.id
? "text-green-600"
: "text-gray-400"
}`}
@@ -463,7 +253,7 @@ export default function MatchesInterface() {
})}
</div>
</div>
</div>
</div>
{/* Right Content Area - Scrollable */}
<div className=" px-2 py-8 w-full md:w-9/12 ">
<div className="w-[100%] max-w-[1200px] mx-auto">
@@ -472,19 +262,58 @@ export default function MatchesInterface() {
{tabs.find((t) => t.id === selectedTab)?.title}
</h1>
<div className="flex gap-2 items-center">
<div className="relative cursor-pointer" onClick={() => {
if (isPaidMember) {
navigate("/horoscoper-generate");
} else {
toast.error("Star Matching is locked for free members");
}
}}>
<img
src={horoscope}
onClick={() => {
navigate("/horoscoper-generate");
}}
className={!isPaidMember ? "opacity-50 blur-[1px]" : ""}
/>
{!isPaidMember && (
<div className="absolute inset-0 flex items-center justify-center">
<Lock className="w-4 h-4 text-black" />
</div>
)}
</div>
<FilterModal />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-2">
{profiles.map((profile) => (
<ProfileCard key={profile.id} profile={profile} />
{isLoading && !isFetchingNextPage ? (
[...Array(6)].map((_, i) => <ProfileCardSkeleton key={i} />)
) : profiles.length > 0 ? (
profiles.map((profile) => (
<ProfileCardUI key={profile.id} profile={profile} />
))
) : !isLoading && !isFetchingNextPage ? (
<div className="col-span-full text-center py-10 text-gray-500">
No profiles found
</div>
) : null}
{/* {isFetchingNextPage &&
[...Array(5)].map((_, i) => (
<ProfileCardSkeleton key={`skel-${i}`} />
))} */}
{(isFetchingNextPage || showSkeleton) &&
[...Array(6)].map((_, i) => (
<ProfileCardSkeleton key={`skel-${i}`} />
))}
</div>
<div ref={ref} className="h-[20px]">
{!isLoading && !hasNextPage && profiles.length > 0 && (
<p className="text-center text-gray-500 py-8">
You've reached the end.
</p>
)}
</div>
</div>
</div>
@@ -503,10 +332,22 @@ export default function MatchesInterface() {
<style>{`
.custom-sidebar-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-sidebar-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-sidebar-scrollbar::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 10px;
}
.custom-sidebar-scrollbar::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}
`}</style>
</>
);
}
+15 -1
View File
@@ -1,9 +1,23 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Search } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux';
import { updateFilter } from '../../redux/filterSlice';
import useDebounce from '../../hooks/useDebounce.jsx';
export default function SearchUI() {
const dispatch = useDispatch();
const searchFromStore = useSelector((state) => state.filters.search);
const [searchValue, setSearchValue] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false);
const debouncedSearchValue = useDebounce(searchValue, 500);
useEffect(() => {
setSearchValue(searchFromStore || '');
}, [searchFromStore]);
useEffect(() => {
dispatch(updateFilter({ search: debouncedSearchValue }));
}, [debouncedSearchValue, dispatch]);
// Sample suggestions data - you can replace with dynamic data
const allSuggestions = [
@@ -1,405 +1,409 @@
import { useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { useRef, useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules';
import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react';
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 { Crown, Bookmark, X, Ban, ChevronLeft, ChevronRight, RotateCcw, Heart, Timer } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-hot-toast';
import axiosInstance from "../../api/axiosInstance";
import { API_ENDPOINTS } from "../../api/apiEndpoints";
import { sendInterest, shortlistProfile } from '../../services/shortlistapi';
import { unblockProfile } from '../../services/profileActionApi';
import UpgradeModal from '../common/UpgradeModal';
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import { useQuery } from '@tanstack/react-query';
import { getHeaderDetails } from "../../api/preview.api";
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/effect-coverflow';
import { useNavigate } from 'react-router-dom';
const DailyRecommendedCard = () => {
/* ─────────────────────────────────────────────
Countdown Timer Component (Popup Style)
───────────────────────────────────────────── */
const CountdownTimer = ({ onContinue }) => {
const [timeLeft, setTimeLeft] = useState({ hours: 0, minutes: 0, seconds: 0 });
useEffect(() => {
const calculateTimeLeft = () => {
const now = new Date();
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const diff = endOfDay - now;
if (diff <= 0) return { hours: 0, minutes: 0, seconds: 0 };
return {
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / 1000 / 60) % 60),
seconds: Math.floor((diff / 1000) % 60)
};
};
setTimeLeft(calculateTimeLeft());
const timer = setInterval(() => {
setTimeLeft(calculateTimeLeft());
}, 1000);
return () => clearInterval(timer);
}, []);
const fmt = n => String(n).padStart(2, '0');
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-md p-4 overflow-y-auto">
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 40 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="bg-white rounded-[40px] shadow-2xl w-full max-w-lg overflow-hidden p-10 flex flex-col items-center text-center my-auto"
>
<div className="w-24 h-24 bg-blue-50 rounded-full flex items-center justify-center mb-8">
<div className="w-16 h-16 rounded-full border-2 border-blue-100 flex items-center justify-center">
<Timer className="w-10 h-10 text-blue-900" strokeWidth={1.5} />
</div>
</div>
<h3 className="text-[28px] font-bold text-[#1e3a8a] mb-4">Daily Limit Exceeded</h3>
<p className="text-gray-500 text-[16px] leading-relaxed mb-10 px-4">
You've viewed all recommended profiles for today. Your daily limit will reset in:
</p>
<div className="flex items-start justify-center gap-4 mb-12">
{[
{ label: 'HOURS', value: timeLeft.hours },
{ label: 'MINS', value: timeLeft.minutes },
{ label: 'SECS', value: timeLeft.seconds }
].map((u, i) => (
<div key={i} className="flex flex-col items-center">
<div className="w-20 h-20 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.06)] border border-gray-50 flex items-center justify-center mb-3">
<span className="text-[32px] font-bold text-[#1e3a8a]">{fmt(u.value)}</span>
</div>
<span className="text-[10px] font-bold text-gray-400 tracking-widest">{u.label}</span>
</div>
))}
</div>
<button
onClick={onContinue}
className="w-full py-5 bg-[#009944] text-white rounded-2xl font-bold text-[18px] shadow-lg shadow-green-900/20 hover:bg-[#00883d] transition-all active:scale-[0.98]"
>
Continue to Dashboard
</button>
</motion.div>
</div>
);
};
const DailyRecommendedCard = ({ profiles: initialProfiles = [] }) => {
const swiperRef = useRef(null);
const navigate = useNavigate();
const [activeProfiles, setActiveProfiles] = useState(initialProfiles);
const [isHidden, setIsHidden] = useState(() => {
const hiddenDate = localStorage.getItem('daily_limit_hidden_date');
return hiddenDate === new Date().toDateString();
});
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
// Sample profile data
const profiles = [
{
id: 1,
name: 'Selva Kumar . R',
userId: 'TK52586A',
lastSeen: '14 Nov 25',
age: 23,
height: '5\'2"',
religion: 'Hindu / Agamudayar / Thuluva Vellal',
education: 'BCA, Data Analyst',
location: 'Vellore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 2,
name: 'Priya Sharma',
userId: 'TK52587B',
lastSeen: '15 Nov 25',
age: 25,
height: '5\'4"',
religion: 'Hindu / Brahmin / Iyer',
education: 'MBA, Marketing Manager',
location: 'Chennai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 3,
name: 'Rahul Venkat',
userId: 'TK52588C',
lastSeen: '16 Nov 25',
age: 28,
height: '5\'10"',
religion: 'Hindu / Mudaliar / Arcot',
education: 'B.Tech, Software Engineer',
location: 'Bangalore, Karnataka',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 4,
name: 'Aishwarya Reddy',
userId: 'TK52589D',
lastSeen: '17 Nov 25',
age: 26,
height: '5\'5"',
religion: 'Hindu / Reddy / Telangana',
education: 'CA, Chartered Accountant',
location: 'Hyderabad, Telangana',
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 5,
name: 'Karthik Mohan',
userId: 'TK52590E',
lastSeen: '18 Nov 25',
age: 27,
height: '5\'8"',
religion: 'Hindu / Nadar / Tamil',
education: 'M.Tech, Civil Engineer',
location: 'Madurai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 6,
name: 'Divya Lakshmi',
userId: 'TK52591F',
lastSeen: '19 Nov 25',
age: 24,
height: '5\'3"',
religion: 'Hindu / Pillai / Tamil',
education: 'B.Com, HR Executive',
location: 'Coimbatore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&h=500&fit=crop',
isPremium: true
const { data: headerData } = useQuery({
queryKey: ["headerDetails"],
queryFn: getHeaderDetails,
staleTime: 60000,
});
console.log("Header Data:", headerData);
const isUserPaid = headerData?.myDetails?.is_paid_member === true;
useEffect(() => {
setActiveProfiles(initialProfiles);
}, [initialProfiles]);
const handleContinueToDashboard = () => {
localStorage.setItem('daily_limit_hidden_date', new Date().toDateString());
setIsHidden(true);
};
const handleInterest = async (e, profileId, name) => {
e.stopPropagation();
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
];
try {
await sendInterest(profileId);
toast.success(`Interest sent to ${name}!`, {
icon: '❤️',
style: { borderRadius: '10px', background: '#333', color: '#fff' }
});
setActiveProfiles(prev => prev.filter(p => p.id !== profileId));
} catch (error) {
const msg = error.message || "";
if (msg.toLowerCase().includes("limit") || msg.toLowerCase().includes("reached")) {
setIsUpgradeModalOpen(true);
} else {
toast.error(msg || "Failed to send interest. Please try again.");
}
}
};
const handleDecline = async (e, profileId) => {
e.stopPropagation();
try {
await axiosInstance.post(`${API_ENDPOINTS.DAILY_RECOMMENDED_DONT_SHOW}?profile_id=${profileId}`);
setActiveProfiles(prev => prev.filter(p => p.id !== profileId));
toast.success("Profile hidden");
} catch (error) {
toast.error("Failed to hide profile.");
}
};
const handleUnblock = async (e, profileId) => {
e.stopPropagation();
try {
const res = await unblockProfile(profileId);
toast.success(res?.message || "Profile unblocked successfully");
setActiveProfiles(prev => prev.map(p => p.id === profileId ? { ...p, is_blocked: 0 } : p));
} catch (error) {
toast.error(error?.message || "Failed to unblock profile.");
}
};
const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false);
const image = profile.photo || profile.image;
const memberId = profile.member_id || profile.userId;
const religion = profile.religion || (profile.religion_name ? `${profile.religion_name}${profile.caste_name ? ' / ' + profile.caste_name : ''}` : null);
const education = profile.education || profile.education_name;
const occupation = profile.occupation || profile.occupation_name;
const location = profile.location || profile.district_name || profile.city_name;
const income = profile.income || profile.annual_income || profile.salary;
const [isShortlisted, setIsShortlisted] = useState(profile.is_shortlisted === 1);
const handleShortlistClick = async (e) => {
e.stopPropagation();
try {
const res = await shortlistProfile(profile.id);
console.log(res);
if (res.status === "success" || res.message) {
setIsShortlisted(!isShortlisted);
toast.success(res.message || "Updated shortlist status");
}
} catch (error) {
console.log(error);
toast.error("Failed to update shortlist");
}
};
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
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-[24px] shadow-xl overflow-hidden border border-green-100 bg-white group hover:shadow-2xl transition-all"
>
<div className="relative">
<div className="relative overflow-hidden">
{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>
)}
{!profile.is_blocked && (
<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();
}}
className="absolute top-4 right-4 z-10 bg-white/90 backdrop-blur-sm rounded-full px-4 py-2 shadow-md flex items-center space-x-2 hover:bg-white transition-colors"
onClick={handleShortlistClick}
>
<Bookmark className="w-4 h-4" />
<span className="text-[12px] font-medium">Shortlist</span>
<Bookmark
className={`w-4 h-4 transition-colors ${isShortlisted ? "text-black fill-black" : "text-green-700"}`}
/>
<span className="text-[12px] font-bold text-gray-700">
{isShortlisted ? "Shortlisted" : "Shortlist"}
</span>
</motion.button>
)}
<div
className=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
style={{ height: "300px" }}
>
<div className="bg-gray-200 overflow-hidden w-full h-[300px]">
<img
src={profile.image}
src={image}
alt={profile.name}
className="w-full h-full object-cover"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
<div
className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none"
className="absolute bottom-0 left-0 right-0 h-32 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%)",
background: "linear-gradient(to top, white 0%, rgba(255,255,255,0.8) 40%, transparent 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">
<div className="absolute bottom-0 left-0 right-0 p-6 pb-2">
<h1 className="text-[20px] text-black-900 font-bold mb-1 truncate">
{profile.name}
</h1>
<p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: {profile.userId}
<p className="text-[13px] text-gray-500 font-medium">
ID: {memberId}
</p>
</div>
</div>
<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="px-6 pb-6 pt-2 space-y-3 bg-white min-h-[160px]">
{(profile.age || profile.height) && (
<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 : {profile.age}
<img src={personIcon} alt="Person" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700">
{profile.age ? `${profile.age} Yrs` : ''}
{profile.age && profile.height ? ', ' : ''}
{profile.height || ''}
</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: {profile.height}
</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">
{profile.religion}
</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">
{profile.education}
</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">
{profile.location}
</span>
</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
{religion && (
<div className="flex items-center gap-2">
<img src={religionIcon} alt="Religion" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{religion}
</span>
</div>
)}
{(education || occupation) && (
<div className="flex items-center gap-2">
<SchoolIcon sx={{ fontSize: 18 }} className="text-gray-400" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{education || occupation}
</span>
</div>
)}
{location && (
<div className="flex items-center gap-2">
<img src={locationIcon} alt="Location" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{location}
</span>
</div>
)}
{income && (
<div className="flex items-center gap-2">
<img src={cashIcon} alt="Income" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{income}
</span>
</div>
)}
<div className="flex gap-3 pt-4 justify-between">
{profile.is_blocked ? (
<button
onClick={(e) => handleUnblock(e, profile.id)}
className="flex-1 px-4 py-2 bg-[#DF1D46] hover:bg-red-600 text-white font-bold text-sm rounded-full transition-all flex items-center justify-center gap-2 shadow-lg shadow-red-900/20 active:scale-95"
>
<Ban size={16} /> Unblock Profile
</button>
) : (
<>
<button
onClick={(e) => handleDecline(e, profile.id)}
className="flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-600 font-bold text-sm rounded-full transition-all flex items-center justify-center gap-2 active:scale-95"
>
<X size={16} /> Decline
</button>
<button
className="flex-1 px-4 py-2 bg-[#034E08] hover:bg-green-800 text-white font-bold text-sm rounded-full transition-all flex items-center justify-center gap-2 shadow-lg shadow-green-900/20 active:scale-95"
onClick={(e) => handleInterest(e, profile.id, profile.name)}
>
<Heart size={16} fill="white" /> Interest
</button>
</>
)}
</div>
</div>
</motion.div>
);
};
if (isHidden) return null;
return (
<div className=" py-10">
<div className="w-[100%] max-w-[1400px] mx-auto">
{/* Header */}
<div className="py-12">
<UpgradeModal
isOpen={isUpgradeModalOpen}
onClose={() => setIsUpgradeModalOpen(false)}
/>
{activeProfiles.length === 0 ? (
<CountdownTimer onContinue={handleContinueToDashboard} />
) : (
<div className="w-full max-w-[1400px] mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<h1 className="text-[20px] text-[#00000] sm:text-[22px] lg:text-[24px] font-semibold mb-3">
Daily Recommended
</h1>
<p className="text-gray-900 text-[12px]">Find your perfect match today</p>
<h2 className="text-3xl font-extrabold text-gray-900 mb-2">
Daily <span className="text-green-700">Recommended</span>
</h2>
<p className="text-gray-500 font-medium">Handpicked matches just for you</p>
</motion.div>
{/* Swiper Container */}
<div className="relative px-0 sm:px-0">
<div className="relative group">
<Swiper
ref={swiperRef}
modules={[Navigation, Pagination, Autoplay, EffectCoverflow]}
spaceBetween={10}
spaceBetween={24}
slidesPerView={1}
autoplay={{
delay: 5000,
disableOnInteraction: false,
pauseOnMouseEnter: true
}}
// pagination={{
// clickable: true,
// dynamicBullets: true
// }}
loop={true}
speed={800}
autoplay={{ delay: 4000, disableOnInteraction: false }}
breakpoints={{
640: {
slidesPerView: 2,
spaceBetween: 10
},
1024: {
slidesPerView: 3,
spaceBetween: 10
},
1280: {
slidesPerView: 4,
spaceBetween: 5
}
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 },
1280: { slidesPerView: 4 }
}}
className="pb-16"
className="!pb-12"
>
{profiles.map((profile) => (
{activeProfiles.map((profile) => (
<SwiperSlide key={profile.id}>
<ProfileCard profile={profile} />
</SwiperSlide>
))}
</Swiper>
{/* Custom Navigation Buttons */}
<motion.button
whileHover={{ scale: 1.1, x: -5 }}
whileTap={{ scale: 0.9 }}
<button
onClick={() => swiperRef.current?.swiper.slidePrev()}
className="hidden sm:flex absolute left-0 top-1/2 -translate-y-1/2 z-10
bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all
w-12 h-12 rounded-full shadow-xl hover:shadow-2xl items-center justify-center "
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 -translate-x-4 bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all hidden lg:flex border border-gray-100"
>
<ChevronLeft className="w-6 h-6 text-gray-700" />
</motion.button>
<ChevronLeft className="w-6 h-6 text-green-800" />
</button>
<motion.button
whileHover={{ scale: 1.1, x: 5 }}
whileTap={{ scale: 0.9 }}
<button
onClick={() => swiperRef.current?.swiper.slideNext()}
className="hidden sm:flex absolute right-0 top-1/2 -translate-y-1/2 z-10
bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all
w-12 h-12 rounded-full shadow-xl hover:shadow-2xl items-center justify-center transition-all"
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 translate-x-4 bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all hidden lg:flex border border-gray-100"
>
<ChevronRight className="w-6 h-6 text-gray-700" />
</motion.button>
<ChevronRight className="w-6 h-6 text-green-800" />
</button>
</div>
{/* View All Button */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-center mt-12"
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-[#034E08] text-white rounded-full font-semibold text-lg shadow-lg hover:shadow-xl transition-shadow"
onClick={() => navigate("/matches")}
>
View All Recommendations
</motion.button>
</motion.div>
</div>
{/* Custom Swiper Styles */}
<style jsx global>{`
.swiper-pagination-bullet {
width: 10px;
height: 10px;
background: #A70710;
opacity: 0.5;
}
.swiper-pagination-bullet-active {
opacity: 1;
width: 30px;
border-radius: 5px;
}
// .swiper-slide {
// // height: auto;
// // display: flex;
// }
`}</style>
)}
</div>
);
};
+158 -236
View File
@@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { useRef, useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Swiper, SwiperSlide } from "swiper/react";
import {
@@ -10,141 +10,91 @@ import {
import {
Crown,
Bookmark,
User,
Briefcase,
MapPin,
X,
Send,
ChevronLeft,
ChevronRight,
Heart,
Eye,
} from "lucide-react";
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 AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
import profilebg from "../../assets/images/profilebg.jpg";
import Image from "../../assets/images/astrology-horoscope-svgrepo-com.svg";
import Image1 from "../../assets/images/scorpio-svgrepo-com.svg";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
// Import Swiper styles
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/effect-coverflow";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { updateFilter } from "../../redux/filterSlice";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
const MatchingList = () => {
const swiperRef = useRef(null);
const ProfileCard = ({ profile }) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [isShortlisted, setIsShortlisted] = useState(profile?.is_shortlisted === 1);
// Sample profile data
const profiles = [
{
id: 1,
name: "Selva Kumar . R",
userId: "TK52586A",
lastSeen: "14 Nov 25",
age: 23,
height: "5'2\"",
salary: "5-10 LPA",
location: "chennai",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop",
isPremium: true,
},
{
id: 2,
name: "Priya Sharma",
userId: "TK52587B",
lastSeen: "15 Nov 25",
age: 25,
height: "5'4\"",
salary: "8-12 LPA",
location: "hyderabad",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=500&fit=crop",
isPremium: true,
},
{
id: 3,
name: "Rahul Venkat",
userId: "TK52588C",
lastSeen: "16 Nov 25",
age: 28,
height: "5'10\"",
salary: "6-11 LPA",
location: "Mumbai",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=500&fit=crop",
isPremium: false,
},
{
id: 4,
name: "Aishwarya Reddy",
userId: "TK52589D",
lastSeen: "17 Nov 25",
age: 26,
height: "5'5\"",
salary: "7-11 LPA",
location: "Bangalore",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image:
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=500&fit=crop",
isPremium: true,
},
{
id: 5,
name: "Karthik Mohan",
userId: "TK52590E",
lastSeen: "18 Nov 25",
age: 27,
height: "5'8\"",
salary: "9-14 LPA",
location: "kerala",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image:
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&h=500&fit=crop",
isPremium: false,
},
{
id: 6,
name: "Divya Lakshmi",
userId: "TK52591F",
lastSeen: "19 Nov 25",
age: 24,
height: "5'3\"",
salary: "5-10 LPA",
location: "madya pradesh",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image:
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&h=500&fit=crop",
isPremium: true,
},
];
useEffect(() => {
setIsShortlisted(profile?.is_shortlisted === 1);
}, [profile?.is_shortlisted]);
// Profile Card Component
const shortlistMutation = useMutation({
mutationFn: shortlistProfile,
onMutate: () => {
setIsShortlisted((prev) => !prev);
},
onSuccess: (data) => {
toast.success(data.message || "Profile shortlisted successfully.");
// Invalidating queries will refetch data and update all profile cards simultaneously
queryClient.invalidateQueries();
},
onError: (error) => {
setIsShortlisted(profile?.is_shortlisted === 1);
toast.error(error.message || "Failed to update shortlist status.");
}
});
const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
const interestMutation = useMutation({
mutationFn: sendInterest,
onSuccess: (data) => {
toast.success(data.message || "Interest sent successfully.");
queryClient.invalidateQueries();
},
onError: (error) => {
toast.error(error.message || "Failed to send interest.");
}
});
const declineMutation = useMutation({
mutationFn: declineProfile,
onSuccess: (data) => {
toast.success(data.message || "Profile declined.");
queryClient.invalidateQueries();
},
onError: (error) => {
toast.error(error.message || "Failed to decline profile.");
}
});
const id = profile.id;
const image = profile.photo || profile.image;
const name = profile.name || "Unknown";
const idNumber = profile.member_id || profile.userId || "N/A";
const lastSeen = profile.last_seen_at || profile.lastSeen || "Recently";
const age = profile.age ? `${profile.age} yrs` : null;
const height = profile.height || null;
const salary = profile.annual_income_name || profile.salary || null;
const location = profile.district_name || profile.location || null;
const caste = profile.caste_name || profile.caste || null;
const zodiac1 = profile.raasi_name || profile.zodiac1 || null;
const zodiac2 = profile.star_name || profile.zodiac2 || null;
const isPremium = profile.is_paid_member !== undefined ? profile.is_paid_member === 1 : profile.isPremium;
return (
<motion.div
@@ -152,140 +102,106 @@ const MatchingList = () => {
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
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"
onClick={() => navigate(`/profile-details/${id}`)}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer"
>
{/* Profile Image Section */}
{/* 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-red-900 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
<img
src={image}
alt="profile"
className="w-full h-[320px] object-cover"
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/>
{isPremium && (
<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>
)}
{/* 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"
<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 hover:bg-gray-50 transition-colors ${shortlistMutation.isPending ? "opacity-50 cursor-wait" : "cursor-pointer"}`}
onClick={(e) => {
e.stopPropagation();
// shortlist logic
if (!shortlistMutation.isPending) {
shortlistMutation.mutate(id);
}
}}
>
<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"
/>
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} className={isShortlisted ? "text-black" : ""} />
{shortlistMutation.isPending ? "..." : (isShortlisted ? "Shortlisted" : "Shortlist")}
</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
className="absolute bottom-0 left-0 right-0 h-25 pointer-events-none"
style={{
background:
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.75) 50%, rgb(255, 255, 255) 100%)",
}}
></div>
{/* Profile Info Overlay - positioned at bottom */}
<div className="absolute bottom-1 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: {profile.userId}
</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 : {profile.age}
</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: {profile.height}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<AccountBalanceWalletIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.salary}
</span>
</div>
<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">
{profile.location}
{/* CONTENT */}
<div className="px-5 py-5 -mt-8 bg-white rounded-t-[32px] relative z-[2] shadow-[0_-12px_40px_rgba(0,0,0,0.1)] h-[240px] flex flex-col">
<div className="text-left mb-4">
<h2 className="text-[20px] font-bold text-gray-900 leading-tight">
{name}
</h2>
<div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
<span>ID: {idNumber}</span>
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<span className="flex items-center gap-1">
<Eye size={12} /> {lastSeen}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<TempleHinduIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.caste}
<div className="space-y-3 mt-2">
{(age || height) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
<img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700">
{age || ""}
{age && height ? ", " : ""}
{height || ""}
</span>
</div>
<div className="flex items-center gap-2">
<img src={Image} alt="" className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.zodiac1}
)}
{(caste || zodiac1) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-pink-50 flex items-center justify-center flex-shrink-0">
<img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{caste}
{caste && zodiac1 ? ` (${zodiac1})` : zodiac1 || ""}
</span>
</div>
)}
{location && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0">
<img src={locationIcon} alt="Location" className="w-4 h-4 opacity-80" />
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<img src={Image1} alt="" className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.zodiac2}
<span className="text-[13px] font-bold text-gray-700 truncate">
{location}
</span>
</div>
)}
</div>
</div>
</motion.div>
);
};
const MatchingList = ({ matches }) => {
const swiperRef = useRef(null);
const navigate = useNavigate();
const dispatch = useDispatch();
const displayProfiles = matches || [];
if (displayProfiles.length === 0) return null;
return (
<>
<div
@@ -297,16 +213,19 @@ const MatchingList = () => {
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
className="text-center mb-10"
>
<h1 className="text-[20px] text-[#00000] sm:text-[22px] lg:text-[24px] font-semibold mb-3">
Your Matching List
</h1>
<p className="text-gray-900 text-[12px]">
Find your perfect match today
<h2 className="text-[28px] font-extrabold text-gray-900 mb-1">
All Matches <span className="text-green-700">List</span>
</h2>
<p className="text-gray-500 font-medium">
We've found these profiles that match your preferences
</p>
</motion.div>
{/* Swiper Container */}
<div className="relative">
<Swiper
@@ -341,8 +260,8 @@ const MatchingList = () => {
}}
className="pb-16"
>
{profiles.map((profile) => (
<SwiperSlide key={profile.id}>
{displayProfiles.map((profile, index) => (
<SwiperSlide key={profile.id || index}>
<ProfileCard profile={profile} />
</SwiperSlide>
))}
@@ -383,7 +302,10 @@ const MatchingList = () => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-[#034E08] text-white rounded-full font-semibold text-lg shadow-lg hover:shadow-xl transition-shadow"
onClick={() => navigate("/matches")}
onClick={() => {
dispatch(updateFilter({ filter_type: "all_matches" }));
navigate("/matches", { state: { activeTab: "allmatches", filter_type: "all_matches" } });
}}
>
View All Matches
</motion.button>
@@ -391,7 +313,7 @@ const MatchingList = () => {
</div>
{/* Custom Swiper Styles */}
<style jsx global>{`
<style>{`
.swiper-pagination-bullet {
width: 10px;
height: 10px;
@@ -5,48 +5,13 @@ import "swiper/css/navigation";
import { motion } from 'framer-motion';
import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react';
import { useRef } from "react";
import LazyImage from "../common/LazyImage";
import weddingImg1 from "../../assets/images/wedding6.jpeg";
import weddingImg2 from "../../assets/images/wedding8.jpg";
import weddingImg3 from "../../assets/images/wedding7.jpg";
const articleData = [
{
title: "Marriage is not just finding the right partner, it's creating a lifetime of moments together Find someone who understands your heart and walks with you in every season.",
img: weddingImg1,
},
{
title: "Top 10 Qualities for a Happy Marriage, A perfect match begins with trust, respect, and shared dreams",
img: weddingImg2
},
{
title: "Expert Tips for a Strong Relationship, A perfect match begins with trust, respect, and shared dreams",
img: weddingImg3
},
{
title: "How to Build Trust in Marriage, A perfect match begins with trust, respect, and shared dreams",
img: weddingImg1
},
{
title: "Communication Secrets for Couples, A perfect match begins with trust, respect, and shared dreams,A perfect match begins with trust, respect, and shared dreams.A perfect match begins with trust, respect, and shared dreams.A perfect match begins with trust, respect, and shared dreams,Real relationships are built on honesty, compassion, and understanding.,Real relationships are built on honesty, compassion, and understanding.",
img: weddingImg1
}
];
const twoLineStyle = {
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'normal'
};
const MatrimonyArticles = () => {
const MatrimonyArticles = ({ articles }) => {
const swiperRef = useRef(null);
const displayArticles = articles || [];
if (displayArticles.length === 0) return null;
return (
<>
<div className="custom-article-swiper py-10 px-2 max-w-[1400px] mx-auto my-10">
@@ -70,51 +35,19 @@ const MatrimonyArticles = () => {
loop={true}
className="mySwiper"
>
{articleData.map((item, index) => (
<SwiperSlide key={index}>
{displayArticles.map((item) => (
<SwiperSlide key={item.id}>
<div className="w-full max-w-[600px] rounded-[10px] overflow-hidden border border-2 border-[#f2f2f2] bg-white">
<div className="relative w-full">
<div classname=" bg-gray-200 overflow-hidden w-full max-w-sm " style={{height:"240px"}}>
<div className="bg-gray-200 overflow-hidden w-full max-w-sm" style={{height:"240px"}}>
<img
src={item.img}
className="w-full h-88 object-cover"
alt={item.title}
src={item.image}
className="w-full h-full object-cover"
alt={`Article ${item.id}`}
/>
</div>
{/* <LazyImage
src={item.img}
className="w-full h-48 object-cover"
alt={item.title}
/> */}
{/* White Gradient Overlay at bottom of image */}
{/* Profile Info Overlay - positioned at bottom */}
<div
className="px-4 pb-4 flex flex-col gap-2 relative "
style={{
background: "rgb(255, 255, 255)",
}}
>
<div
className="z-9 absolute top-[-40px] left-0 right-0 h-10 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="z-10 relative pb-4" style={{
background: "rgb(255, 255, 255)",
}}>
<h3 className="text-[16px] font-semibold two-line-ellipsis" style={twoLineStyle} >{item.title}</h3>
</div>
</div>
</div>
</div>
@@ -1,7 +1,19 @@
import { Phone, MessageCircle, ThumbsUp, Eye } from 'lucide-react';
import promogirl from "../../assets/images/mobile.png"
const MembershipCard = ()=>{
import { useNavigate } from 'react-router-dom';
const MembershipCard = ({ becomePaidMember })=>{
const navigate = useNavigate();
const offerPercentage = becomePaidMember?.offer_percentage
? parseInt(becomePaidMember.offer_percentage)
: 58;
const points = becomePaidMember?.points || [];
const icons = [Phone, MessageCircle, Eye, ThumbsUp];
return (
<>
@@ -19,50 +31,31 @@ return (
{/* Subheading with offer */}
<p className="text-[16px] lg:text-[16px] text-gray-900 mb-4">
Get up to <span className="text-red-600 font-bold">58% OFF</span> on paid membership!
Get up to <span className="text-red-600 font-bold">{offerPercentage}% OFF</span> on paid membership!
</p>
{/* Features List */}
<div className="space-y-2 mb-2">
<div className="flex items-start gap-4">
{points.map((point, index) => {
const Icon = icons[index % icons.length];
return (
<div key={index} className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<Phone className="w-5 h-5 text-gray-700" />
<Icon className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
Call/WhatsApp matches
</p>
</div>
<div className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<MessageCircle className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
Unlimited messages
</p>
</div>
<div className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<ThumbsUp className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
Higher chances of response
</p>
</div>
<div className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<Eye className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
View and match horoscopes
{point}
</p>
</div>
);
})}
</div>
{/* CTA Button */}
<button className="mt-2 bg-[#034E08] hover:bg-[#A70710] text-white font-bold text-[14px] lg:text-[14px] py-4 px-6 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105">
<button
onClick={() => navigate('/subscription-plan')}
className="mt-2 bg-[#034E08] hover:bg-[#A70710] text-white font-bold text-[14px] lg:text-[14px] py-4 px-6 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
>
See membership plans
</button>
</div>
@@ -86,4 +79,3 @@ return (
</>);
};
export default MembershipCard
@@ -1,116 +1,95 @@
import { useRef, useState } from 'react';
import { useRef, useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules';
import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react';
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 profilebg from "../../assets/images/profilebg.jpg";
import {
Crown,
Bookmark,
X,
ChevronLeft,
ChevronRight,
Heart,
Eye,
} from 'lucide-react';
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/effect-coverflow';
import { useNavigate } from 'react-router-dom';
const NewJoinedProfile = () => {
const swiperRef = useRef(null);
const navigate = useNavigate();
// Sample profile data
const profiles = [
{
id: 1,
name: 'Selva Kumar . R',
userId: 'TK52586A',
lastSeen: '14 Nov 25',
age: 23,
height: '5\'2"',
religion: 'Hindu / Agamudayar / Thuluva Vellal',
education: 'BCA, Data Analyst',
location: 'Vellore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 2,
name: 'Priya Sharma',
userId: 'TK52587B',
lastSeen: '15 Nov 25',
age: 25,
height: '5\'4"',
religion: 'Hindu / Brahmin / Iyer',
education: 'MBA, Marketing Manager',
location: 'Chennai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 3,
name: 'Rahul Venkat',
userId: 'TK52588C',
lastSeen: '16 Nov 25',
age: 28,
height: '5\'10"',
religion: 'Hindu / Mudaliar / Arcot',
education: 'B.Tech, Software Engineer',
location: 'Bangalore, Karnataka',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 4,
name: 'Aishwarya Reddy',
userId: 'TK52589D',
lastSeen: '17 Nov 25',
age: 26,
height: '5\'5"',
religion: 'Hindu / Reddy / Telangana',
education: 'CA, Chartered Accountant',
location: 'Hyderabad, Telangana',
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 5,
name: 'Karthik Mohan',
userId: 'TK52590E',
lastSeen: '18 Nov 25',
age: 27,
height: '5\'8"',
religion: 'Hindu / Nadar / Tamil',
education: 'M.Tech, Civil Engineer',
location: 'Madurai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 6,
name: 'Divya Lakshmi',
userId: 'TK52591F',
lastSeen: '19 Nov 25',
age: 24,
height: '5\'3"',
religion: 'Hindu / Pillai / Tamil',
education: 'B.Com, HR Executive',
location: 'Coimbatore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&h=500&fit=crop',
isPremium: true
}
];
// Profile Card Component
import { useDispatch } from 'react-redux';
import { updateFilter } from "../../redux/filterSlice";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
const queryClient = useQueryClient();
const [isShortlisted, setIsShortlisted] = useState(profile?.is_shortlisted === 1);
useEffect(() => {
setIsShortlisted(profile?.is_shortlisted === 1);
}, [profile?.is_shortlisted]);
const shortlistMutation = useMutation({
mutationFn: shortlistProfile,
onMutate: () => {
setIsShortlisted((prev) => !prev);
},
onSuccess: (data) => {
toast.success(data.message || "Profile shortlisted successfully.");
// Invalidating queries will refetch data and update all profile cards simultaneously
queryClient.invalidateQueries();
},
onError: (error) => {
setIsShortlisted(profile?.is_shortlisted === 1);
toast.error(error.message || "Failed to update shortlist status.");
}
});
const interestMutation = useMutation({
mutationFn: sendInterest,
onSuccess: (data) => {
toast.success(data.message || "Interest sent successfully.");
queryClient.invalidateQueries();
},
onError: (error) => {
toast.error(error.message || "Failed to send interest.");
}
});
const declineMutation = useMutation({
mutationFn: declineProfile,
onSuccess: (data) => {
toast.success(data.message || "Profile declined.");
queryClient.invalidateQueries();
},
onError: (error) => {
toast.error(error.message || "Failed to decline profile.");
}
});
const id = profile.id;
const image = profile.photo || profile.image;
const name = profile.name || "Unknown";
const idNumber = profile.member_id || profile.userId || "N/A";
const lastSeen = profile.last_seen_at || profile.lastSeen || "Recently";
const age = profile.age ? `${profile.age} yrs` : null;
const height = profile.height || null;
const salary = profile.annual_income_name || profile.salary || null;
const location = profile.district_name || profile.location || null;
const caste = profile.caste_name || profile.caste || null;
const zodiac1 = profile.raasi_name || profile.zodiac1 || null;
const zodiac2 = profile.star_name || profile.zodiac2 || null;
const isPremium = profile.is_paid_member !== undefined ? profile.is_paid_member === 1 : profile.isPremium;
return (
<motion.div
@@ -118,173 +97,106 @@ const ProfileCard = ({ profile }) => {
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200"
onClick={() => navigate(`/profile-details/${id}`)}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer"
>
{/* IMAGE SECTION */}
<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>
)}
<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>
<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>
<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%)",
src={image}
alt="profile"
className="w-full h-[320px] object-cover"
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
></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: {profile.userId}
</p>
</div>
</div>
<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 : {profile.age}
</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: {profile.height}
</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">
{profile.religion}
</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">
{profile.education}
</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">
{profile.location}
</span>
</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>
{isPremium && (
<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 hover:bg-gray-50 transition-colors ${shortlistMutation.isPending ? "opacity-50 cursor-wait" : "cursor-pointer"}`}
onClick={(e) => {
e.stopPropagation();
if (!shortlistMutation.isPending) {
shortlistMutation.mutate(id);
}
}}
>
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} className={isShortlisted ? "text-black" : ""} />
{shortlistMutation.isPending ? "..." : (isShortlisted ? "Shortlisted" : "Shortlist")}
</div>
</div>
{/* CONTENT */}
<div className="px-5 py-5 -mt-8 bg-white rounded-t-[32px] relative z-[2] shadow-[0_-12px_40px_rgba(0,0,0,0.1)] h-[240px] flex flex-col">
<div className="text-left mb-4">
<h2 className="text-[20px] font-bold text-gray-900 leading-tight">
{name}
</h2>
<div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
<span>ID: {idNumber}</span>
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<span className="flex items-center gap-1">
<Eye size={12} /> {lastSeen}
</span>
</div>
</div>
<div className="space-y-3 mt-2">
{(age || height) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
<img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700">
{age || ""}
{age && height ? ", " : ""}
{height || ""}
</span>
</div>
)}
{(caste || zodiac1) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-pink-50 flex items-center justify-center flex-shrink-0">
<img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{caste}
{caste && zodiac1 ? ` (${zodiac1})` : zodiac1 || ""}
</span>
</div>
)}
{location && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0">
<img src={locationIcon} alt="Location" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{location}
</span>
</div>
)}
Interest
</button>
</div>
</div>
</motion.div>
);
};
};
const NewJoinedProfile = ({ profiles }) => {
const swiperRef = useRef(null);
const navigate = useNavigate();
const dispatch = useDispatch();
const displayProfiles = profiles || [];
if (displayProfiles.length === 0) return null;
return (
<>
@@ -299,14 +211,16 @@ const ProfileCard = ({ profile }) => {
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
className="text-center mb-10"
>
<h1 className="text-[20px] text-[#034E08] sm:text-[22px] lg:text-[24px] font-bold mb-3">
New Joined
</h1>
<p className="text-gray-600 text-[12px]">Find your perfect match today</p>
<h2 className="text-[28px] font-extrabold text-gray-900 mb-1">
Newly <span className="text-green-700">Joined</span>
</h2>
<p className="text-gray-500 font-medium">Be the first to connect with our newest members</p>
</motion.div>
{/* Swiper Container */}
<div className="relative">
<Swiper
@@ -319,10 +233,6 @@ const ProfileCard = ({ profile }) => {
disableOnInteraction: false,
pauseOnMouseEnter: true
}}
pagination={{
clickable: true,
dynamicBullets: true
}}
loop={true}
speed={800}
breakpoints={{
@@ -341,8 +251,8 @@ const ProfileCard = ({ profile }) => {
}}
className="pb-16"
>
{profiles.map((profile) => (
<SwiperSlide key={profile.id}>
{displayProfiles.map((profile, index) => (
<SwiperSlide key={profile.id || index}>
<ProfileCard profile={profile} />
</SwiperSlide>
))}
@@ -383,14 +293,18 @@ const ProfileCard = ({ profile }) => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-[#034E08] text-white rounded-full font-semibold text-lg shadow-lg hover:shadow-xl transition-shadow"
onClick={() => {
dispatch(updateFilter({ filter_type: "newly_joined" }));
navigate("/matches", { state: { activeTab: "newly_joined", filter_type: "newly_joined" } });
}}
>
View All
View All Matches
</motion.button>
</motion.div>
</div>
{/* Custom Swiper Styles */}
<style jsx global>{`
<style>{`
.swiper-pagination-bullet {
width: 10px;
height: 10px;
@@ -1,47 +1,75 @@
import { useState } from "react";
import React, { 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";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import VisibilityIcon from "@mui/icons-material/Visibility";
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 buildDefaultRows = (profile) => {
const rows = [];
// Row 1: Age & Height
if (profile?.age || profile?.height) {
const row = [];
if (profile?.age && profile?.age !== "-") {
row.push({
icon: <img src={personIcon} alt="Person" className="w-4 h-4 opacity-70" />,
text: `${profile.age} Yrs`,
});
}
if (profile?.height && profile?.height !== "-") {
row.push({
icon: row.length === 0 ? <img src={personIcon} alt="Person" className="w-4 h-4 opacity-70" /> : null,
text: profile.height,
});
}
if (row.length > 0) rows.push(row);
}
// Row 2: Religion / Caste
const religionText = profile?.religion || (profile?.religion_name ? `${profile.religion_name}${profile.caste_name ? ' / ' + profile.caste_name : ''}` : null);
if (religionText && religionText !== "-") {
rows.push([{
icon: <img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-70" />,
text: religionText,
}]);
}
// Row 3: Education
const eduText = profile?.education || profile?.qualification || profile?.education_name;
if (eduText && eduText !== "-") {
rows.push([{
icon: <SchoolIcon sx={{ fontSize: 16 }} className="text-gray-400" />,
text: eduText,
}]);
}
// Row 4: Income
const incomeText = profile?.income || profile?.annual_income || profile?.annual_income_name;
if (incomeText && incomeText !== "-") {
rows.push([{
icon: <img src={cashIcon} alt="Cash" className="w-4 h-4 opacity-70" />,
text: incomeText,
}]);
}
// Row 5: Location
const locText = profile?.location || profile?.district_name;
if (locText && locText !== "-") {
rows.push([{
icon: <img src={locationIcon} alt="Location" className="w-4 h-4 opacity-70" />,
text: locText,
}]);
}
return rows;
};
const getProfileIdText = (profile) =>
profile?.userId ||
@@ -1,16 +1,16 @@
import React, { useState } from "react";
import { motion } from "framer-motion";
import AstroChatUI from "./AstroChatUI";
import LazyImage from "../common/LazyImage";
import ProfileIcon from "../../assets/images/profileicon.png";
import HoroscodeIcon from "../../assets/images/horoscopericon.png";
import FamilyIcon from "../../assets/images/homeicon.png";
import Box from "@mui/material/Box";
import { useNavigate } from "react-router-dom";
import AstroChatUI from "./AstroChatUI";
import MembershipCard from "./MembershipCard";
const ProfileCompletion = () => {
const [completeness, setCompleteness] = useState(30);
const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember }) => {
const navigate = useNavigate();
const cards = [
@@ -60,7 +60,9 @@ const ProfileCompletion = () => {
return (
<>
<div className="max-w-[1400px] w-[100%] mx-auto my-10 px-2 sm:p-2 lg:p-2">
<div className="max-w-[1400px] w-full mx-auto my-10 px-4">
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
@@ -95,7 +97,7 @@ const ProfileCompletion = () => {
transition={{ delay: 0.6, type: "spring", stiffness: 200 }}
className="text-lg sm:text-xl font-bold text-gray-900"
>
{completeness}%
{percentage}%
</motion.span>
</div>
@@ -103,7 +105,7 @@ const ProfileCompletion = () => {
<div className="relative h-3 bg-gray-200 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${completeness}%` }}
animate={{ width: `${percentage}%` }}
transition={{ delay: 0.8, duration: 1, ease: "easeOut" }}
className="absolute top-0 left-0 h-full bg-gradient-to-r from-red-500 to-red-600 rounded-full"
/>
@@ -112,48 +114,39 @@ const ProfileCompletion = () => {
</div>
{/* Cards Section */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 ">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* Left Side: Astro Chat */}
<div className="w-full">
<AstroChatUI />
</div>
{/* Desktop Logo */}
{/* <Box sx={{ display: { xs: "none", md: "flex" }, mr: 1 }}>
<LazyImage
src={Logo}
className="w-full h-[50px] rounded-lg object-cover"
/>
</Box> */}
<AstroChatUI/>
<div className="my-4 rounded-2xl bg-green-50 border border-1 border-green-200 p-4 ">
{/* Right Side: Cards & Membership */}
<div className="w-full">
<div className="rounded-2xl bg-green-50 border border-green-200 p-6">
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
>
{cards.map((card, index) => (
<div
key={card.id}
onClick={() => navigate(card.url)}
className=" border border-1 border-red-50 bg-white rounded-3xl hover:bg-red-50 hover:border-2
flex flex-col items-center space-x-2 h-32 justify-center transition-colors duration-500
cursor-pointer ">
{/* Icon Container */}
className="border border-red-50 bg-white rounded-3xl hover:bg-red-50 hover:border-2 flex flex-col items-center space-x-2 h-32 justify-center transition-colors duration-500 cursor-pointer"
>
<motion.div
initial={{ rotate: -180, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
transition={{ delay: 0.9 + index * 0.1, duration: 0.5 }}
// className={`${card.bgColor} p-0 rounded-xl border ${card.borderColor}`}
>
<LazyImage
src={card.icon}
alt={card.title}
className="w-ful h-full "
className="w-full h-full"
/>
</motion.div>
{/* Text */}
<motion.h3
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
@@ -163,16 +156,17 @@ const ProfileCompletion = () => {
{card.title}
</motion.h3>
</div>
))}
</motion.div>
<MembershipCard/>
<div className="mt-6">
<MembershipCard becomePaidMember={becomePaidMember} />
</div>
</div>
</div>
</div>
</div>
{/* Additional Info Section */}
{/* <motion.div
initial={{ opacity: 0, y: 20 }}
@@ -2,113 +2,31 @@ import React, { useState, useRef } from 'react';
import { motion } from 'framer-motion';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay } from 'swiper/modules';
import { Play, X, Heart, Share2, Eye, ChevronLeft, ChevronRight } from 'lucide-react';
import weddingImg1 from "../../assets/images/wedding6.jpeg";
import weddingImg2 from "../../assets/images/wedding8.jpg";
import weddingImg3 from "../../assets/images/wedding6.jpeg";
import { Play, X, ChevronLeft, ChevronRight } from 'lucide-react';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
const VideoSwiperGallery = () => {
const VideoSwiperGallery = ({ videos }) => {
const [selectedVideo, setSelectedVideo] = useState(null);
const swiperRef = useRef(null);
// Video data with online sources
const videos = [
{
id: 1,
title: 'Priya & Rahul - Wedding Story',
thumbnail: weddingImg1,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
views: '2.4K',
likes: '142',
duration: '3:45'
},
{
id: 2,
title: 'Aisha - Profile Introduction',
thumbnail: weddingImg2,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
views: '1.8K',
likes: '98',
duration: '2:30'
},
{
id: 3,
title: 'Rohan - Life Journey',
thumbnail: weddingImg3,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
views: '3.2K',
likes: '256',
duration: '4:15'
},
{
id: 4,
title: 'Divya - Family Values',
thumbnail: weddingImg1,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
views: '1.5K',
likes: '87',
duration: '3:00'
},
{
id: 5,
title: 'Karthik & Meera - First Meet',
thumbnail: weddingImg2,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4',
views: '4.1K',
likes: '312',
duration: '5:20'
},
{
id: 6,
title: 'Sneha - Hobbies & Interests',
thumbnail: weddingImg3,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4',
views: '2.7K',
likes: '178',
duration: '2:45'
},
{
id: 7,
title: 'Arjun - Career & Dreams',
thumbnail: weddingImg1,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4',
views: '1.9K',
likes: '134',
duration: '3:30'
},
{
id: 8,
title: 'Lakshmi - Traditional Values',
thumbnail: weddingImg2,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4',
views: '3.5K',
likes: '267',
duration: '4:00'
},
{
id: 9,
title: 'Vikram - Adventure Life',
thumbnail: weddingImg3,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
views: '5.2K',
likes: '423',
duration: '4:30'
},
{
id: 10,
title: 'Anjali - Creative Journey',
thumbnail: weddingImg1,
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
views: '3.8K',
likes: '289',
duration: '3:15'
const displayVideos = videos || [];
if (displayVideos.length === 0) return null;
const getEmbedUrl = (url) => {
if (!url) return '';
let embedUrl = url;
if (url.includes('youtu.be/')) {
embedUrl = url.replace('youtu.be/', 'www.youtube.com/embed/');
} else if (url.includes('youtube.com/watch?v=')) {
embedUrl = url.replace('youtube.com/watch?v=', 'youtube.com/embed/');
}
];
return embedUrl.split('?')[0] + '?autoplay=1';
};
const VideoCard = ({ video }) => {
const [isHovered, setIsHovered] = useState(false);
@@ -129,12 +47,12 @@ const VideoSwiperGallery = () => {
<div className="relative aspect-video">
<img
src={video.thumbnail}
alt={video.title}
alt="Video Thumbnail"
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent opacity-60 group-hover:opacity-80 transition-opacity" />
<div className="absolute inset-0 bg-black/30 group-hover:bg-black/50 transition-colors" />
{/* Play Button */}
<motion.div
@@ -146,28 +64,6 @@ const VideoSwiperGallery = () => {
<Play className="w-7 h-7 text-[#034E08] group-hover:text-white ml-1" fill="currentColor" />
</div>
</motion.div>
{/* Duration Badge */}
<div className="absolute top-2 right-2 bg-black/70 text-white text-xs font-semibold px-2 py-1 rounded">
{video.duration}
</div>
{/* Stats at bottom */}
<div className="absolute bottom-0 left-0 right-0 p-3">
<h3 className="text-white font-semibold text-sm mb-2 line-clamp-2">
{video.title}
</h3>
<div className="flex items-center gap-3 text-white/90 text-xs">
<div className="flex items-center gap-1">
<Eye className="w-3 h-3" />
<span>{video.views}</span>
</div>
<div className="flex items-center gap-1">
<Heart className="w-3 h-3" />
<span>{video.likes}</span>
</div>
</div>
</div>
</div>
</div>
</motion.div>
@@ -202,36 +98,13 @@ const VideoSwiperGallery = () => {
</button>
{/* Video Player */}
<div className="bg-[#034E08] rounded-2xl overflow-y-scroll h-[100%] max-h-[480px] shadow-2xl">
<video
src={selectedVideo.videoUrl}
controls
autoPlay
<div className="bg-black rounded-2xl overflow-hidden shadow-2xl">
<iframe
src={getEmbedUrl(selectedVideo.youtube_url)}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="w-full aspect-video"
>
Your browser does not support the video tag.
</video>
{/* Video Info */}
<div className="bg-gray-900 p-6">
<h2 className="text-white text-2xl font-bold mb-3">
{selectedVideo.title}
</h2>
<div className="flex items-center gap-6 text-gray-300">
<div className="flex items-center gap-2">
<Eye className="w-5 h-5" />
<span>{selectedVideo.views} views</span>
</div>
<div className="flex items-center gap-2">
<Heart className="w-5 h-5" />
<span>{selectedVideo.likes} likes</span>
</div>
<button className="flex items-center gap-2 ml-auto bg-[#034E08] hover:bg-green-700 text-white px-6 py-2 rounded-full transition-colors">
<Share2 className="w-4 h-4" />
<span>Share</span>
</button>
</div>
</div>
></iframe>
</div>
</motion.div>
</motion.div>
@@ -290,7 +163,7 @@ const VideoSwiperGallery = () => {
}}
className="pb-16"
>
{videos.map((video) => (
{displayVideos.map((video) => (
<SwiperSlide key={video.id}>
<VideoCard video={video} />
</SwiperSlide>
@@ -343,7 +216,7 @@ const VideoSwiperGallery = () => {
{selectedVideo && <VideoModal />}
{/* Custom Swiper Styles */}
<style jsx global>{`
<style>{`
.swiper-pagination-bullet {
width: 10px;
height: 10px;
File diff suppressed because it is too large. Load diff
@@ -1,24 +1,29 @@
import React from 'react';
import { Check, X } from 'lucide-react';
const PartnerPreferences = () => {
const PartnerPreferences = ({ data }) => {
if (!data) return null;
const pref = data.preferedDetails;
const matchDetails = data.mutual_match.my_preferences_match;
const overallMatch = data.mutual_match.overall_match_percentage;
const basicPreferences = [
{ label: "Preferred Bride's Age", value: "22-29 yrs", match: true },
{ label: "Preferred Height", value: "5'0\" - 5'5\"", match: false },
{ label: "Preferred Marital Status", value: "Never Married", match: true },
{ label: "Preferred Mother Tongue", value: "Tamil", match: true },
{ label: "Preferred Physical Status", value: "Normal", match: true },
{ label: "Preferred Eating Habits", value: "Vegetarian", match: false },
{ label: "Preferred Smoking Habits", value: "Doesn't Matter", match: true },
{ label: "Preferred Drinking Habits", value: "Doesn't Matter", match: true },
{ label: "Preferred Groom's Age", value: pref.preferred_age_range || "Any", match: matchDetails.age },
{ label: "Preferred Height", value: (pref.preferred_height_from && pref.preferred_height_to) ? `${pref.preferred_height_from} - ${pref.preferred_height_to} ft` : "Any", match: matchDetails.height },
{ label: "Preferred Marital Status", value: pref.preferred_marital_statuses?.join(", ") || "Any", match: matchDetails.marital_status },
{ label: "Preferred Mother Tongue", value: pref.preferred_mother_tongues?.join(", ") || "Any", match: matchDetails.mother_tongue },
{ label: "Preferred Education", value: pref.preferred_educations?.join(", ") || "Any", match: matchDetails.education },
{ label: "Preferred Employee Type", value: pref.preferred_employee_types?.join(", ") || "Any", match: true }, // Not in matchDetails?
];
const religiousPreferences = [
{ label: "Preferred Religion", value: "Hindu", match: true },
{ label: "Preferred Caste", value: "Brahmin - Iyer", match: false },
{ label: "Preferred Subcaste", value: "Any", match: false },
{ label: "Preferred Star", value: "Any", match: true },
{ label: "Preferred Dosham", value: "No Dosham", match: true },
{ label: "Preferred Caste", value: pref.preferred_castes?.join(", ") || "Any", match: matchDetails.caste },
{ label: "Preferred Sub-caste", value: pref.preferred_sub_castes?.join(", ") || "Any", match: matchDetails.sub_caste },
{ label: "Preferred State", value: pref.preferred_states?.join(", ") || "Any", match: true },
{ label: "Preferred City", value: pref.preferred_districts?.join(", ") || "Any", match: true },
{ label: "Preferred Occupation", value: pref.preferred_occupations?.join(", ") || "Any", match: matchDetails.occupation },
{ label: "Preferred Annual Income", value: pref.preferred_annual_income || "Any", match: matchDetails.annual_income },
];
const PreferenceItem = ({ label, value, match }) => (
@@ -46,7 +51,7 @@ const PartnerPreferences = () => {
<div className="text-center mb-6 ">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-800 mb-2 flex items-center justify-center gap-2">
<span className="text-pink-400">✨</span>
His Partner Preferences
Partner Preferences
<span className="text-pink-400">✨</span>
</h1>
</div>
@@ -56,65 +61,40 @@ const PartnerPreferences = () => {
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4">
<img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=male1"
alt="Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100"
src={data.my_profile || "https://api.dicebear.com/7.x/avataaars/svg?seed=male1"}
alt="Your Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100 object-cover"
/>
<div>
<p className="text-gray-600 text-sm mb-1">You match</p>
<p className="text-gray-600 text-sm mb-1">Overall Match Score</p>
<p className="text-2xl sm:text-3xl font-bold text-red-600">
14<span className="text-[#034E08]">/20</span>
{overallMatch}<span className="text-[#034E08]">%</span>
</p>
<p className="text-xs text-gray-500">of his preferences</p>
<p className="text-xs text-gray-500">of preferences match</p>
</div>
</div>
<img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=female1"
alt="Your Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100"
src={data.profile.profile_picture || "https://api.dicebear.com/7.x/avataaars/svg?seed=female1"}
alt="Partner Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100 object-cover"
/>
</div>
</div>
<div className='grid grid-cols-1 gap-2 md:grid-cols-2 mb-8 pt-4'>
{/* Basic Preferences Section */}
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="flex items-center justify-between mb-4 bg-[#f5fbff] pt-4 pb-4 px-6">
<h2 className="text-lg font-bold text-gray-800">Basic Preferences</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">You match</span>
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
</div>
<div className='max-w-3xl mx-auto mb-8 pt-4'>
{/* Partner Preferences Section */}
<div className="bg-white rounded-2xl shadow-lg overflow-hidden border border-gray-100">
<div className="flex items-center justify-between bg-[#f5fbff] py-4 px-6 border-b border-gray-100">
<h2 className="text-lg font-bold text-gray-800">Partner Preference Details</h2>
</div>
<div className="space-y-1 p-6">
{basicPreferences.map((pref, index) => (
{[...basicPreferences, ...religiousPreferences].map((pref, index) => (
<PreferenceItem key={index} {...pref} />
))}
</div>
</div>
</div>
{/* Religious Preferences Section */}
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="flex items-center justify-between mb-4 bg-[#f5fbff] pt-4 pb-4 px-6">
<h2 className="text-lg font-bold text-gray-800">Religious Preferences</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">You match</span>
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
</div>
</div>
<div className="space-y-1 p-6">
{religiousPreferences.map((pref, index) => (
<PreferenceItem key={index} {...pref} />
))}
</div>
</div>
</div>
{/* Footer Note */}
<div className="text-center mt-6 text-sm text-gray-500">
<p>Preferences are used to find compatible matches</p>
+305 -126
View File
@@ -1,73 +1,251 @@
import React from "react";
import { Heart, X, Crown, Bookmark, Eye } from "lucide-react";
// Import your images
import Profile1 from "../../assets/images/bride1.jpg";
import Profile2 from "../../assets/images/bride2.jpg";
import Profile3 from "../../assets/images/bride3.jpg";
import Profile4 from "../../assets/images/bride4.jpg";
import React, { useState, useEffect } from "react";
import { Heart, X, Crown, Bookmark, Eye, Clock, ChevronLeft, ChevronRight } from "lucide-react";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import SchoolIcon from "@mui/icons-material/School";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import { motion } from "framer-motion";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
import { useNavigate } from "react-router-dom";
export default function ProfileCard() {
// Sample data for multiple cards with image paths
const profiles = [
{
id: 1,
name: "Jerome Bell",
idNumber: "KI2847596",
lastSeen: "4 Nov 2025",
salary: "5-10",
age: "22 yrs",
height: "5'2\"",
location: "Chennai",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile1,
const ProfileCardItem = ({ profile }) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [isShortlisted, setIsShortlisted] = useState(profile?.is_shortlisted === 1);
useEffect(() => {
setIsShortlisted(profile?.is_shortlisted === 1);
}, [profile?.is_shortlisted]);
const shortlistMutation = useMutation({
mutationFn: shortlistProfile,
onMutate: () => {
setIsShortlisted((prev) => !prev);
},
{
id: 2,
name: "Neha Singh",
idNumber: "KI2847597",
lastSeen: "5 Nov 2025",
salary: "8-12",
age: "26 yrs",
height: "5'6\"",
location: "hyderabad",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile2,
onSuccess: (data) => {
toast.success(data.message || "Profile shortlisted successfully.");
queryClient.invalidateQueries();
},
{
id: 3,
name: "Priya Sharma",
idNumber: "KI2847598",
lastSeen: "3 Nov 2025",
salary: "6-11",
age: "24 yrs",
height: "5'4\"",
location: "Mumbai",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile3,
onError: (error) => {
setIsShortlisted(profile?.is_shortlisted === 1);
toast.error(error.message || "Failed to update shortlist status.");
}
});
const interestMutation = useMutation({
mutationFn: sendInterest,
onSuccess: (data) => {
toast.success(data.message || "Interest sent successfully.");
queryClient.invalidateQueries();
},
{
id: 4,
name: "Kavya Iyer",
idNumber: "KI2847599",
lastSeen: "2 Nov 2025",
salary: "7-10",
age: "23 yrs",
height: "5'3\"",
location: "Bangalore",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile4,
onError: (error) => {
toast.error(error.message || "Failed to send interest.");
}
});
const declineMutation = useMutation({
mutationFn: declineProfile,
onSuccess: (data) => {
toast.success(data.message || "Profile declined.");
queryClient.invalidateQueries();
},
];
onError: (error) => {
toast.error(error.message || "Failed to decline profile.");
}
});
const id = profile.id;
const image = profile.photo || profile.image;
const name = profile.name || "Unknown";
const idNumber = profile.member_id || profile.idNumber || "N/A";
const lastSeen = profile.last_seen_at || profile.lastSeen || "Recently";
const age = profile.age ? `${profile.age} yrs` : null;
const height = profile.height || null;
const salary = profile.annual_income_name || (profile.salary ? `${profile.salary} LPA` : null);
const location = profile.district_name || profile.location || null;
const caste = profile.caste_name || profile.caste || null;
const zodiac1 = profile.raasi_name || profile.zodiac1 || null;
const zodiac2 = profile.star_name || profile.zodiac2 || null;
const isPremium = profile.is_paid_member !== undefined ? profile.is_paid_member === 1 : true;
return (
<div
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer"
onClick={() => navigate(`/profile-details/${id}`)}
>
{/* IMAGE SECTION */}
<div className="relative">
<img
src={image}
alt="profile"
className="w-full h-[320px] object-cover"
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/>
{isPremium && (
<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 hover:bg-gray-50 transition-colors ${shortlistMutation.isPending ? "opacity-50 cursor-wait" : "cursor-pointer"}`}
onClick={(e) => {
e.stopPropagation();
if (!shortlistMutation.isPending) shortlistMutation.mutate(id);
}}
>
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} />
{shortlistMutation.isPending ? "..." : "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">
{name}
</h2>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8">
<p>ID: {idNumber}</p>
<p className="flex items-center gap-0.5">
<Eye size={12} /> {lastSeen}
</p>
</div>
<div className="flex flex-col gap-2 mt-4 min-h-[140px]">
{(age || height) && (
<div className="flex items-center gap-2">
<img src={personIcon} alt="Person" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700">
{age || ""}
{age && height ? ", " : ""}
{height || ""}
</span>
</div>
)}
{(caste || zodiac1) && (
<div className="flex items-center gap-2">
<img src={religionIcon} alt="Religion" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{caste}
{caste && zodiac1 ? ` (${zodiac1})` : zodiac1 || ""}
</span>
</div>
)}
{salary && (
<div className="flex items-center gap-2">
<img src={cashIcon} alt="Cash" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{salary}
</span>
</div>
)}
{location && (
<div className="flex items-center gap-2">
<img src={locationIcon} alt="Location" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{location}
</span>
</div>
)}
</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 hover:bg-red-100 transition-colors ${declineMutation.isPending ? "opacity-50 cursor-wait" : ""}`}
onClick={(e) => {
e.stopPropagation();
if (!declineMutation.isPending) declineMutation.mutate(id);
}}
disabled={declineMutation.isPending}
>
<X size={18} /> {declineMutation.isPending ? "..." : "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 hover:bg-green-100 transition-colors ${interestMutation.isPending ? "opacity-50 cursor-wait" : ""}`}
onClick={(e) => {
e.stopPropagation();
if (!interestMutation.isPending) interestMutation.mutate(id);
}}
disabled={interestMutation.isPending}
>
<Heart size={18} /> {interestMutation.isPending ? "..." : "Interest"}
</button>
</div>
</div>
</div>
);
};
export default function ProfileCard({ profiles }) {
const displayProfiles = profiles || [];
const [showTimer, setShowTimer] = useState(false);
const [timeLeft, setTimeLeft] = useState(24 * 60 * 60); // 24 hours in seconds
useEffect(() => {
const storedEndTime = localStorage.getItem("profileTimerEnd");
if (storedEndTime) {
const endTime = parseInt(storedEndTime, 10);
const now = Date.now();
if (endTime > now) {
setShowTimer(true);
setTimeLeft(Math.floor((endTime - now) / 1000));
} else {
localStorage.removeItem("profileTimerEnd");
}
}
}, []);
useEffect(() => {
let timer;
if (showTimer && timeLeft > 0) {
timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
setShowTimer(false);
localStorage.removeItem("profileTimerEnd");
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(timer);
}, [showTimer, timeLeft]);
const handleShowTimer = () => {
const endTime = Date.now() + 24 * 60 * 60 * 1000; // 24 hours from now
localStorage.setItem("profileTimerEnd", endTime.toString());
setTimeLeft(24 * 60 * 60);
setShowTimer(true);
};
const formatTime = (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
};
return (
<div className="h-auto py-8 px-4">
@@ -84,78 +262,79 @@ export default function ProfileCard() {
</h1>
<p className="text-gray-900 text-[12px]">
Find your perfect match today
</p>
</motion.div>
{/* CARDS GRID */}
<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">
{profiles.map((profile) => (
<div
key={profile.id}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md"
<div className="w-full max-w-[1400px] relative px-2 md:px-12">
{!showTimer ? (
<>
{/* Custom Navigation Arrows */}
<button className="profile-swiper-prev absolute left-0 md:left-2 top-[40%] -translate-y-1/2 z-20 w-10 h-10 bg-white shadow-xl border border-gray-100 rounded-full flex items-center justify-center text-[#8b0000] hover:bg-[#8b0000] hover:text-white transition-all disabled:opacity-30 disabled:cursor-not-allowed">
<ChevronLeft size={24} />
</button>
<button className="profile-swiper-next absolute right-0 md:right-2 top-[40%] -translate-y-1/2 z-20 w-10 h-10 bg-white shadow-xl border border-gray-100 rounded-full flex items-center justify-center text-[#8b0000] hover:bg-[#8b0000] hover:text-white transition-all disabled:opacity-30 disabled:cursor-not-allowed">
<ChevronRight size={24} />
</button>
<Swiper
modules={[Navigation, Pagination]}
spaceBetween={24}
slidesPerView={1}
navigation={{
prevEl: '.profile-swiper-prev',
nextEl: '.profile-swiper-next',
}}
pagination={{ clickable: true }}
breakpoints={{
768: { slidesPerView: 2 },
1024: { slidesPerView: 3 },
1280: { slidesPerView: 4 },
}}
onReachEnd={() => {
if (displayProfiles.length > 0) {
setTimeout(handleShowTimer, 2500); // Wait 2.5s on the last slide before hiding
}
}}
className="pb-12"
>
{/* IMAGE SECTION */}
<div className="relative">
<img
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" />
{displayProfiles.map((profile, index) => (
<SwiperSlide key={profile.id || index}>
<ProfileCardItem profile={profile} />
</SwiperSlide>
))}
{/* END SLIDE (BUFFER) */}
<SwiperSlide>
<div className="w-full h-full min-h-[400px] flex flex-col items-center justify-center bg-white rounded-[28px] shadow-md border border-gray-100 p-6">
<h3 className="text-xl font-bold mb-4 text-gray-800 text-center">You've seen all matches!</h3>
<button
onClick={handleShowTimer}
className="px-6 py-2 bg-[#8b0000] text-white rounded-full font-semibold shadow-lg hover:bg-red-800 transition"
>
View Next Batch Timer
</button>
</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
</SwiperSlide>
</Swiper>
</>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center justify-center bg-white rounded-[28px] shadow-xl max-w-lg mx-auto py-12 px-6 border border-gray-100"
>
<Clock size={48} className="text-[#8b0000] mb-4" />
<h2 className="text-2xl font-bold mb-2 text-gray-800 text-center">Next Recommendations In</h2>
<div className="text-5xl font-extrabold text-[#8b0000] tracking-widest my-4 tabular-nums">
{formatTime(timeLeft)}
</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 className="text-gray-500 text-center mb-6">
We are curating the best matches for you. Please check back when the timer ends!
</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>
))}
</motion.div>
)}
</div>
</div>
</div>
+6 -6
View File
@@ -75,12 +75,12 @@ const AdvancedDropzone = ({ value, onChange }) => {
<>
<Dropzone
onChange={updateFiles}
minHeight="195px"
minHeight="200px"
value={extFiles}
accept="image/*"
maxFiles={3}
maxFileSize={10 * 1024 * 1024}
label="Drag'n drop up to 3 images (max 10 MB each)"
label={<span style={{ fontSize: "16px" }}>Drag'n drop up to 3 images (max 10 MB each)</span>}
uploadConfig={{
url: BASE_URL + "/file",
cleanOnUpload: true,
@@ -155,9 +155,9 @@ const AdvancedDropzone = ({ value, onChange }) => {
}}
aria-label="Move left"
>
<ArrowLeftIcon fontSize="small" />
<ArrowLeftIcon fontSize="small" sx={{fontSize:"30px"}} />
</button>
<span
{/* <span
style={{
color: "#fff",
fontSize: 12,
@@ -170,7 +170,7 @@ const AdvancedDropzone = ({ value, onChange }) => {
title={file.name}
>
{file.name}
</span>
</span> */}
<button
type="button"
onClick={(e) => {
@@ -197,7 +197,7 @@ const AdvancedDropzone = ({ value, onChange }) => {
}}
aria-label="Move right"
>
<ArrowRightIcon fontSize="small" />
<ArrowRightIcon fontSize="small" sx={{fontSize:"30px"}} />
</button>
</div>
)}
+327 -373
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateEducationalDetails } from "../redux/registrationFormSlice";
import { updateEducationalDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import {
TextField,
Button,
@@ -9,454 +9,408 @@ import {
InputLabel,
Select,
MenuItem,
FormHelperText,
InputAdornment,
Box,
Typography,
} from "@mui/material";
import { useEducationMasters, useEducationList } from "../hooks/useMasters";
import { useCityMasters } from "../hooks/useDependentMasters";
import { toast } from "react-hot-toast";
const EducationalDetailsForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
errors: externalErrors,
isEditMode,
}) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.educationalDetails);
const inputRef = useRef(null);
const [localErrors, setLocalErrors] = useState({});
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: educationMasters, isLoading: isEducationMastersLoading } =
useEducationMasters();
const educationListQuery = useEducationList(data.fieldOfStudy);
const educationListQuery = useEducationList(data.study_field);
const districtQuery = useCityMasters(data.work_state);
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 studyFieldOptions = educationMasters?.studyFields || [];
const qualificationOptions = educationListQuery.data?.education || educationListQuery.data?.data || [];
const occupationOptions = educationMasters?.occupation || [];
const employeeTypeOptions = educationMasters?.employeeType || [];
const countryOptions = educationMasters?.country || [];
const stateOptions = educationMasters?.state || [];
const districtOptions = districtQuery.data?.districts || districtQuery.data || [];
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(() => {
inputRef.current?.focus();
}, []);
const isUnemployed = data.employee_type === 11;
const isIndia = Number(data.work_country) === 1;
const handleChange = (field, value) => {
const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "fieldOfStudy") {
updates.qualification = "";
fieldsToClear.push("qualification");
if (field === "study_field") {
updates.education = "";
}
if (field === "work_country") {
updates.work_state = "";
updates.work_district = "";
updates.work_city = "";
}
if (field === "work_state") {
updates.work_district = "";
}
if (field === "employee_type" && value === 11) {
// Clear fields that will be hidden
updates.occupation = "";
updates.occupation_detail = "";
updates.company_name = "";
updates.annual_income = "";
updates.work_country = "";
updates.work_state = "";
updates.work_district = "";
updates.work_city = "";
}
dispatch(updateEducationalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear);
setLocalErrors((prev) => ({ ...prev, [field]: "" }));
};
const validateForm = () => {
const newErrors = {};
if (!data.study_field) newErrors.study_field = "Field of study is required";
if (!data.education) newErrors.education = "Highest qualification is required";
if (!data.education_detail) newErrors.education_detail = "Education detail is required";
if (!data.employee_type) newErrors.employee_type = "Employee type is required";
if (!isUnemployed) {
if (!data.occupation) newErrors.occupation = "Occupation is required";
if (!data.occupation_detail) newErrors.occupation_detail = "Occupation detail is required";
if (!data.income_currency) newErrors.income_currency = "Income currency is required";
if (!data.annual_income) newErrors.annual_income = "Annual income is required";
if (!data.work_country) newErrors.work_country = "Work country is required";
if (isIndia) {
if (!data.work_state) newErrors.work_state = "Work state is required";
if (!data.work_district) newErrors.work_district = "Work city is required";
} else {
if (!data.work_city) newErrors.work_city = "Work city is required";
}
}
if (!data.address) newErrors.address = "Work address is required";
setLocalErrors(newErrors);
return newErrors;
};
const scrollToError = (errorMap) => {
const errorFields = Object.keys(errorMap);
if (errorFields.length > 0) {
const fieldId = errorFields[0];
const element = document.getElementById(fieldId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
const focusable = element.querySelector('[role="combobox"]') ||
element.querySelector('[role="button"]') ||
element.querySelector("input") ||
element.querySelector("select") ||
element;
if (focusable && typeof focusable.focus === "function") {
focusable.focus();
}
}, 300); // Reduced delay slightly for snappier feel
}
}
};
const handleSubmit = () => {
console.log("Submitting educational details:", data);
const freshErrors = validateForm();
if (Object.keys(freshErrors).length > 0) {
toast.error("Please fill all mandatory fields");
scrollToError(freshErrors);
return;
}
onSubmitStep();
};
return (
<>
<div className="w-full max-w-[1200px] mx-auto 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">
{/* Field of Study */}
<div className="flex flex-col gap-4">
<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>
{/* 1. Field of Study */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Field of Study{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.study_field)} id="study_field">
<InputLabel>Select Field of Study</InputLabel>
<Select
labelId="fieldOfStudy-label"
value={data.study_field}
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",
},
}}
onChange={(e) => handleChange("study_field", e.target.value)}
>
{studyFieldOptions.map((field) => (
<MenuItem key={field.id ?? field} value={field.id ?? field}>
{getOptionLabel(field, "Field of Study")}
</MenuItem>
{studyFieldOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.study_field_name}</MenuItem>
))}
</Select>
{errors.fieldOfStudy && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.fieldOfStudy}
</p>
)}
{localErrors.study_field && <FormHelperText>{localErrors.study_field}</FormHelperText>}
</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>
{/* 2. Highest Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Highest Qualification{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.education)} id="education" disabled={!data.study_field || educationListQuery.isLoading}>
<InputLabel>Select 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",
},
}}
value={data.education}
label="Select Qualification"
onChange={(e) => handleChange("education", e.target.value)}
>
{qualificationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Qualification")}
</MenuItem>
{qualificationOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.education_name || opt.name}</MenuItem>
))}
</Select>
{errors.qualification && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.qualification}
</p>
)}
{localErrors.education && <FormHelperText>{localErrors.education}</FormHelperText>}
</FormControl>
</div>
{/* College Name */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
College Name
</label>
{/* 3. Education in Detail */}
<div className="flex flex-col gap-2 md:col-span-2">
<label className="text-gray-900 text-[15px]">Education in Detail{requiredMark}</label>
<TextField
id="education_detail"
fullWidth
multiline
rows={3}
placeholder="Enter your education details"
value={data.education_detail}
onChange={(e) => handleChange("education_detail", e.target.value)}
error={Boolean(localErrors.education_detail)}
helperText={localErrors.education_detail}
/>
</div>
{/* 4. College Name */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Enter College Name</label>
<TextField
id="college_name"
fullWidth
name="collegeName"
label="College Name"
value={data.collegeName}
onChange={(e) => handleChange("collegeName", e.target.value)}
error={Boolean(errors.collegeName)}
helperText={errors.collegeName}
placeholder="Enter College Name"
variant="outlined"
value={data.college_name}
onChange={(e) => handleChange("college_name", e.target.value)}
/>
</div>
{/* Occupation */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Occupation{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.occupation)}
>
<InputLabel id="occupation-label">Select Occupation</InputLabel>
{/* 5. Employee Type */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Employee type{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.employee_type)} id="employee_type">
<InputLabel>Select Employee Type</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>
{/* Company / Organization Name */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Company / Organization Name{requiredMark}
</label>
<TextField
fullWidth
name="organization"
label="Company / Organization Name"
value={data.organization}
onChange={(e) => handleChange("organization", e.target.value)}
error={Boolean(errors.organization)}
helperText={errors.organization}
placeholder="Enter Company / Organization Name"
variant="outlined"
/>
</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"
value={data.employee_type}
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",
},
}}
onChange={(e) => handleChange("employee_type", e.target.value)}
>
{employeeTypeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Employee Type")}
</MenuItem>
{employeeTypeOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.employee_type_name}</MenuItem>
))}
</Select>
{errors.employeeType && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.employeeType}
</p>
)}
{localErrors.employee_type && <FormHelperText>{localErrors.employee_type}</FormHelperText>}
</FormControl>
</div>
{/* Annual Income */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Annual Income{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.income)}
>
<InputLabel id="income-label">Select Annual Income</InputLabel>
{!isUnemployed && (
<>
{/* 6. Occupation */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Occupation{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.occupation)} id="occupation">
<InputLabel>Select Occupation</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",
},
}}
value={data.occupation}
label="Select Occupation"
onChange={(e) => handleChange("occupation", e.target.value)}
>
{annualIncomeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Annual Income")}
</MenuItem>
{occupationOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.occupation_name}</MenuItem>
))}
</Select>
{errors.income && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.income}
</p>
)}
{localErrors.occupation && <FormHelperText>{localErrors.occupation}</FormHelperText>}
</FormControl>
</div>
{/* Work Location */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Work Location
</label>
{workLocationOptions.length > 0 ? (
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.workLocation)}
>
<InputLabel id="workLocation-label">
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>
) : (
{/* 7. Occupation in Detail */}
<div className="flex flex-col gap-2 md:col-span-2">
<label className="text-gray-900 text-[15px]">Occupation in Detail{requiredMark}</label>
<TextField
id="occupation_detail"
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"
multiline
rows={3}
placeholder="Enter your occupation details"
value={data.occupation_detail}
onChange={(e) => handleChange("occupation_detail", e.target.value)}
error={Boolean(localErrors.occupation_detail)}
helperText={localErrors.occupation_detail}
/>
</div>
{/* 8. Company / Organization Name */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Company / Organization Name</label>
<TextField
id="company_name"
fullWidth
placeholder="Enter Company Name"
value={data.company_name}
onChange={(e) => handleChange("company_name", e.target.value)}
/>
</div>
{/* 9. Income Currency Type */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Income Currency Type{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.income_currency)} id="income_currency">
<InputLabel>Select Currency</InputLabel>
<Select
value={data.income_currency}
label="Select Currency"
onChange={(e) => handleChange("income_currency", e.target.value)}
>
<MenuItem value="INR">INR</MenuItem>
<MenuItem value="USD">USD</MenuItem>
</Select>
{localErrors.income_currency && <FormHelperText>{localErrors.income_currency}</FormHelperText>}
</FormControl>
</div>
{/* 10. Annual Income */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Annual Income{requiredMark}</label>
<TextField
id="annual_income"
fullWidth
placeholder="Enter Annual Income"
value={data.annual_income}
onChange={(e) => handleChange("annual_income", e.target.value)}
error={Boolean(localErrors.annual_income)}
helperText={localErrors.annual_income}
InputProps={{
startAdornment: (
<InputAdornment position="start">
{data.income_currency === "USD" ? "$" : "₹"}
</InputAdornment>
),
}}
/>
</div>
{/* 11. Country */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Country{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.work_country)} id="work_country">
<InputLabel>Select Country</InputLabel>
<Select
value={data.work_country}
label="Select Country"
onChange={(e) => handleChange("work_country", e.target.value)}
>
{countryOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem>
))}
</Select>
{localErrors.work_country && <FormHelperText>{localErrors.work_country}</FormHelperText>}
</FormControl>
</div>
{/* 12. City/Town (Only if NOT India) */}
{!isIndia && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">City / Town{requiredMark}</label>
<TextField
id="work_city"
fullWidth
placeholder="Enter City / Town"
value={data.work_city}
onChange={(e) => handleChange("work_city", e.target.value)}
error={Boolean(localErrors.work_city)}
helperText={localErrors.work_city}
/>
</div>
)}
{/* 13. State (Only if India) */}
{isIndia && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">State{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.work_state)} id="work_state">
<InputLabel>Select State</InputLabel>
<Select
value={data.work_state}
label="Select State"
onChange={(e) => handleChange("work_state", e.target.value)}
>
{stateOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem>
))}
</Select>
{localErrors.work_state && <FormHelperText>{localErrors.work_state}</FormHelperText>}
</FormControl>
</div>
)}
{/* 14. City (District) (Only if India) */}
{isIndia && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">City{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.work_district)} id="work_district" disabled={!data.work_state || districtQuery.isLoading}>
<InputLabel>Select City</InputLabel>
<Select
value={data.work_district}
label="Select City"
onChange={(e) => handleChange("work_district", e.target.value)}
>
{districtOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem>
))}
</Select>
{localErrors.work_district && <FormHelperText>{localErrors.work_district}</FormHelperText>}
</FormControl>
</div>
)}
</>
)}
{/* 15. Address */}
<div className="flex flex-col gap-2 md:col-span-2">
<label className="text-gray-900 text-[15px]">Address{requiredMark}</label>
<TextField
id="address"
fullWidth
multiline
rows={2}
placeholder="Enter your address"
value={data.address}
onChange={(e) => handleChange("address", e.target.value)}
error={Boolean(localErrors.address)}
helperText={localErrors.address}
/>
</div>
</div>
<Grid
item
xs={12}
style={{
marginTop: "40px",
display: "flex",
gap: 16,
justifyContent: "center",
}}
>
<Button variant="outlined" onClick={onSkipStep}>
Next
<Box sx={{ mt: 5, display: "flex", gap: 2, justifyContent: "center" }}>
{onSkipStep && (
<Button variant="outlined" size="large" onClick={onSkipStep} sx={{ minWidth: 120 }}>
Skip
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit}>
Submit
)}
<Button variant="contained" size="large" onClick={handleSubmit} sx={{ minWidth: 120 }}>
{onSkipStep ? "Next" : "Update"}
</Button>
</Grid>
</Box>
</form>
</div>
</>
);
};
+278 -155
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateFamilyDetails } from "../redux/registrationFormSlice";
import { updateFamilyDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import {
Grid,
TextField,
@@ -12,46 +12,55 @@ import {
Box,
} from "@mui/material";
import { useFamilyMasters } from "../hooks/useMasters";
import { useCityMasters } from "../hooks/useDependentMasters";
import { toast } from "react-hot-toast";
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange }) => {
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, isEditMode }) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.familyDetails);
const inputRef = useRef(null);
const brotherSectionRef = useRef(null);
const sisterSectionRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: familyMasters, isLoading: isFamilyMastersLoading } =
useFamilyMasters();
const { data: familyMasters, isLoading: isFamilyMastersLoading } = useFamilyMasters();
// District query for India
const districtQuery = useCityMasters(data.familyState);
const occupationOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
return raw.occupation || [];
}, [familyMasters]);
const maritalStatusOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.maritalStatus || raw.marital_status || [];
return raw.maritalStatus || [];
}, [familyMasters]);
const familyStatusOptions = useMemo(() => {
const raw = familyMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.familyStatus || raw.family_status || [];
return raw.familyStatus || [];
}, [familyMasters]);
const countryOptions = useMemo(() => familyMasters?.country || [], [familyMasters]);
const stateOptions = useMemo(() => familyMasters?.state || [], [familyMasters]);
const districtOptions = useMemo(() => districtQuery.data?.districts || districtQuery.data || [], [districtQuery.data]);
useEffect(() => {
inputRef.current?.focus();
}, []);
const createSibling = () => ({
type: "",
name: "",
occupation: "",
maritalStatus: "",
haveChildrens: "",
hasChildren: "",
details: "",
});
const syncSiblingArray = (arr, count) => {
@@ -72,16 +81,40 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
if (field === "brotherCount") {
const count = Number(value) || 0;
updates.brotherCount = count;
updates.brotherCount = value;
updates.brothers = syncSiblingArray(data.brothers, count);
fieldsToClear.push("brothers");
if (count > 0) {
setTimeout(() => {
brotherSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
const firstInput = brotherSectionRef.current?.querySelector('.first-sibling-name');
if (firstInput) firstInput.focus();
}, 500);
}
}
if (field === "sisterCount") {
const count = Number(value) || 0;
updates.sisterCount = count;
updates.sisterCount = value;
updates.sisters = syncSiblingArray(data.sisters, count);
fieldsToClear.push("sisters");
if (count > 0) {
setTimeout(() => {
sisterSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
const firstInput = sisterSectionRef.current?.querySelector('.first-sibling-name');
if (firstInput) firstInput.focus();
}, 500);
}
}
if (field === "familyCountry") {
updates.familyState = "";
updates.familyDistrict = "";
updates.familyCity = "";
}
if (field === "familyState") {
updates.familyDistrict = "";
}
dispatch(updateFamilyDetails(updates));
@@ -96,12 +129,34 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
if (onFieldChange) onFieldChange(type);
};
const handleSubmit = () => {
console.log("Submitting family details:", data);
const scrollToError = (errorMap) => {
const errorFields = Object.keys(errorMap);
if (errorFields.length > 0) {
const fieldId = errorFields[0];
const element = document.getElementById(fieldId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
const focusable = element.querySelector('[role="combobox"]') ||
element.querySelector('[role="button"]') ||
element.querySelector("input") ||
element.querySelector("select") ||
element;
if (focusable && typeof focusable.focus === "function") {
focusable.focus();
}
}, 300);
}
}
};
const handleSubmit = (e) => {
if (e) e.preventDefault();
onSubmitStep();
};
const countOptions = Array.from({ length: 11 }, (_, i) => i);
const isIndia = Number(data.familyCountry) === 1;
const renderSiblingCard = (type, index) => {
const sibling = (data[type] || [])[index] || createSibling();
@@ -114,95 +169,88 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
borderRadius: 2,
padding: 2,
backgroundColor: "#fff",
mb: 4
}}
>
<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">
<FormControl fullWidth variant="outlined">
<InputLabel>Type</InputLabel>
<Select
label="Type"
value={sibling.type}
onChange={(e) => handleSiblingChange(type, index, "type", e.target.value)}
>
<MenuItem value=""><em>Select</em></MenuItem>
<MenuItem value="Elder">Elder</MenuItem>
<MenuItem value="Younger">Younger</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
label="Name"
placeholder="Enter Name"
value={sibling.name}
onChange={(e) =>
handleSiblingChange(type, index, "name", e.target.value)
}
onChange={(e) => handleSiblingChange(type, index, "name", e.target.value)}
variant="outlined"
inputProps={{ className: index === 0 ? "first-sibling-name" : "" }}
/>
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-occupation-label`}>
Occupation
</InputLabel>
<InputLabel>Occupation</InputLabel>
<Select
labelId={`${type}-${index}-occupation-label`}
label="Occupation"
value={sibling.occupation}
onChange={(e) =>
handleSiblingChange(type, index, "occupation", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
onChange={(e) => handleSiblingChange(type, index, "occupation", e.target.value)}
>
<MenuItem value=""><em>Select</em></MenuItem>
{occupationOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-marital-label`}>
Marital Status
</InputLabel>
<InputLabel>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",
},
}}
onChange={(e) => handleSiblingChange(type, index, "maritalStatus", e.target.value)}
>
<MenuItem value=""><em>Select</em></MenuItem>
{maritalStatusOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-children-label`}>
Have Children
</InputLabel>
<InputLabel>Have Children</InputLabel>
<Select
labelId={`${type}-${index}-children-label`}
label="Have Children"
value={sibling.haveChildrens}
onChange={(e) =>
handleSiblingChange(
type,
index,
"haveChildrens",
e.target.value
)
}
value={sibling.hasChildren}
onChange={(e) => handleSiblingChange(type, index, "hasChildren", e.target.value)}
>
<MenuItem value={1}>Yes</MenuItem>
<MenuItem value={0}>No</MenuItem>
<MenuItem value=""><em>Select</em></MenuItem>
<MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
</Select>
</FormControl>
<div className="md:col-span-2">
<TextField
fullWidth
multiline
rows={2}
label="Additional Details"
value={sibling.details}
onChange={(e) => handleSiblingChange(type, index, "details", e.target.value)}
variant="outlined"
/>
</div>
</div>
</Box>
);
@@ -212,20 +260,18 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
<div className="w-full max-w-[1200px] mx-auto 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">
<div className="flex flex-col gap-4" id="fatherName">
<label className="text-gray-900 text-[15px]">
Father Name{requiredMark}
</label>
<TextField
fullWidth
inputRef={inputRef}
name="fatherName"
label="Father Name"
placeholder="Enter 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>
@@ -236,30 +282,24 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</label>
<TextField
fullWidth
name="fatherOccupation"
label="Father Occupation"
placeholder="Enter 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">
<div className="flex flex-col gap-4" id="motherName">
<label className="text-gray-900 text-[15px]">
Mother Name{requiredMark}
</label>
<TextField
fullWidth
name="motherName"
label="Mother Name"
placeholder="Enter 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>
@@ -270,13 +310,9 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</label>
<TextField
fullWidth
name="motherOccupation"
label="Mother Occupation"
placeholder="Enter 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>
@@ -286,14 +322,13 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
Brother Count
</label>
<FormControl fullWidth variant="outlined">
<InputLabel id="brotherCount-label">Select Brother Count</InputLabel>
<InputLabel>Select Brother Count</InputLabel>
<Select
labelId="brotherCount-label"
label="Select Brother Count"
name="brotherCount"
value={data.brotherCount}
onChange={(e) => handleChange("brotherCount", e.target.value)}
>
<MenuItem value=""><em>Select</em></MenuItem>
{countOptions.map((count) => (
<MenuItem key={count} value={count}>
{count}
@@ -303,19 +338,27 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</FormControl>
</div>
{Number(data.brotherCount) > 0 && (
<div className="md:col-span-2 mt-2" ref={brotherSectionRef}>
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Brother Details
</div>
{data.brothers.map((_, i) => renderSiblingCard("brothers", i))}
</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>
<InputLabel>Select Sister Count</InputLabel>
<Select
labelId="sisterCount-label"
label="Select Sister Count"
name="sisterCount"
value={data.sisterCount}
onChange={(e) => handleChange("sisterCount", e.target.value)}
>
<MenuItem value=""><em>Select</em></MenuItem>
{countOptions.map((count) => (
<MenuItem key={count} value={count}>
{count}
@@ -325,103 +368,183 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</FormControl>
</div>
<div className="flex flex-col gap-4">
{Number(data.sisterCount) > 0 && (
<div className="md:col-span-2 mt-2" ref={sisterSectionRef}>
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Sister Details
</div>
{data.sisters.map((_, i) => renderSiblingCard("sisters", i))}
</div>
)}
<div className="flex flex-col gap-4" id="familyStatus">
<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>
<FormControl fullWidth variant="outlined" error={Boolean(errors.familyStatus)}>
<InputLabel>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 key={item.id} value={item.id}>
{item.family_type_name}
</MenuItem>
))}
</Select>
{errors.familyStatus && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.familyStatus}
</p>
<p className="text-[#d32f2f] text-[0.75rem] mt-1 ml-3">{errors.familyStatus}</p>
)}
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Native Place</label>
<div className="flex flex-col gap-4" id="nativePlace">
<label className="text-gray-900 text-[15px]">
Native Place{requiredMark}
</label>
<TextField
fullWidth
name="nativePlace"
label="Native Place"
placeholder="Enter 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 className="grid grid-cols-1 gap-4">
{Array.from({ length: Number(data.brotherCount) }).map(
(_, index) => renderSiblingCard("brothers", index)
)}
</div>
</div>
)}
{Number(data.sisterCount) > 0 && (
<div className="mt-6">
<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" }}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Country Living
</label>
<FormControl fullWidth variant="outlined">
<InputLabel>Select Country</InputLabel>
<Select
label="Select Country"
value={data.familyCountry}
onChange={(e) => handleChange("familyCountry", e.target.value)}
>
<Button variant="outlined" onClick={onSkipStep}>
{countryOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem>
))}
</Select>
</FormControl>
</div>
{isIndia ? (
<>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Residing State
</label>
<FormControl fullWidth variant="outlined" disabled={!data.familyCountry}>
<InputLabel>Select State</InputLabel>
<Select
label="Select State"
value={data.familyState}
onChange={(e) => handleChange("familyState", e.target.value)}
>
{stateOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Residing City
</label>
<FormControl fullWidth variant="outlined" disabled={!data.familyState || districtQuery.isLoading}>
<InputLabel>Select City</InputLabel>
<Select
label="Select City"
value={data.familyDistrict}
onChange={(e) => handleChange("familyDistrict", e.target.value)}
>
{districtOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem>
))}
</Select>
</FormControl>
</div>
</>
) : data.familyCountry ? (
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
City / Town
</label>
<TextField
fullWidth
placeholder="Enter City / Town"
value={data.familyCity}
onChange={(e) => handleChange("familyCity", e.target.value)}
variant="outlined"
/>
</div>
) : null}
<div className="flex flex-col gap-4 md:col-span-2">
<label className="text-gray-900 text-[15px]">
Address
</label>
<TextField
fullWidth
multiline
rows={3}
placeholder="Enter complete address"
value={data.address}
onChange={(e) => handleChange("address", e.target.value)}
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4 md:col-span-2">
<label className="text-gray-900 text-[15px]">
Expectations / Requirements Details
</label>
<TextField
fullWidth
multiline
rows={4}
placeholder="Describe your expectations"
value={data.expectationDetails}
onChange={(e) => handleChange("expectationDetails", e.target.value)}
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Willing to go abroad
</label>
<FormControl fullWidth variant="outlined">
<InputLabel>Select Option</InputLabel>
<Select
label="Select Option"
value={data.willingToGoAbroad}
onChange={(e) => handleChange("willingToGoAbroad", e.target.value)}
>
<MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
<MenuItem value="Any">Any</MenuItem>
</Select>
</FormControl>
</div>
</div>
<div className="mt-10 flex gap-4 justify-center">
{onSkipStep && (
<Button variant="outlined" onClick={onSkipStep} sx={{ minWidth: 120 }}>
Skip
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit}>
Submit
)}
<Button variant="contained" color="primary" onClick={handleSubmit} sx={{ minWidth: 120 }}>
{onSkipStep ? "Next" : "Update"}
</Button>
</Grid>
</div>
</form>
</div>
);
+306 -239
View File
@@ -1,15 +1,12 @@
import React, { useState } from "react";
import React, { useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
setAge,
setHeight,
setMaritalStatus,
setMotherTongue,
setReligion,
setMatchesWithHoroscope,
setCaste,
setSubCaste,
updateFilter,
resetFilters,
} from "../redux/filterSlice";
import {
Slider,
@@ -26,8 +23,12 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
CircularProgress,
} from "@mui/material";
import { ChevronDown } from "lucide-react";
import { ChevronDown, Lock, Crown, RotateCcw, Check } from "lucide-react";
import { useProfilesFilterMasters } from "../hooks/useProfiles";
import { useCityMasters } from "../hooks/useDependentMasters";
import toast from "react-hot-toast";
const FilterForm = () => {
const dispatch = useDispatch();
@@ -40,34 +41,61 @@ const FilterForm = () => {
location: false,
lifestyle: false,
family: false,
paidBenefit: false,
});
const { data: filterMasters, isLoading, isError } = useProfilesFilterMasters();
const { data: cityData } = useCityMasters(
filters.state && filters.state.length > 0 ? filters.state : null
);
const cityOptions = useMemo(() => {
const raw = cityData;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.subCaste || raw.district || raw.data || [];
}, [cityData]);
const handleAccordionChange = (section) => (event, isExpanded) => {
setExpandedSections((prev) => ({ ...prev, [section]: isExpanded }));
};
const casteOptions = ["Agamudayar", "Pillai", "Vellalar"];
const motherTongueOptions = [
"Tamil",
"Telugu",
"Malayalam",
"Kannada",
"Hindi",
];
const handleSubmit = () => {
console.log("Filter Values:", filters);
};
const handleClear = () => {
dispatch(resetFilters());
toast.success("Filters cleared");
};
const handleSelectionChange = (field, value) => {
console.log(`${field} selected:`, value);
};
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[300px]">
<CircularProgress />
</div>
);
}
if (isError) {
return (
<div className="flex justify-center items-center min-h-[300px]">
<Typography color="error">Failed to load filter options</Typography>
</div>
);
}
return (
<div className="max-w-6xl mx-auto p-4 px-0 md:p-6 ">
<div className="bg-white rounded-lg shadow-sm">
{/* Header */}
<div className="border-b border-pink-200 p-4 bg-[#fff5ed]">
<div className="border-b border-pink-200 p-4 bg-[#f2f2f2]">
<Typography variant="h5" className="font-semibold text-center">
Partner Filter preference
</Typography>
@@ -98,7 +126,7 @@ const FilterForm = () => {
<Typography gutterBottom sx={{marginBottom:"30px"}}>Age</Typography>
<div className="px-2">
<Slider
value={filters.age}
value={[filters.from_age, filters.to_age]}
onChange={(e, newValue) => {
dispatch(setAge(newValue));
handleSelectionChange("Age", newValue);
@@ -119,7 +147,7 @@ const FilterForm = () => {
<Typography gutterBottom sx={{marginBottom:"30px"}}>Height</Typography>
<div className="px-2">
<Slider
value={filters.height}
value={[filters.from_height, filters.to_height]}
onChange={(e, newValue) => {
dispatch(setHeight(newValue));
handleSelectionChange("Height", newValue);
@@ -141,46 +169,40 @@ const FilterForm = () => {
<FormControl fullWidth>
<InputLabel>Marital Status</InputLabel>
<Select
value={filters.maritalStatus}
multiple
value={filters.marital_status || []}
label="Marital Status"
onChange={(e) => {
dispatch(setMaritalStatus(e.target.value));
dispatch(updateFilter({ marital_status: e.target.value }));
handleSelectionChange("Marital Status", e.target.value);
}}
>
<MenuItem value="All Profile">All Profile</MenuItem>
<MenuItem value="Never Married">Never Married</MenuItem>
<MenuItem value="Divorced">Divorced</MenuItem>
<MenuItem value="Widowed">Widowed</MenuItem>
</Select>
</FormControl>
{/* Mother Tongue */}
<FormControl fullWidth>
<InputLabel>Mother Tongue</InputLabel>
<Select
multiple
value={filters.motherTongue}
label="Mother Tongue"
onChange={(e) => {
dispatch(setMotherTongue(e.target.value));
handleSelectionChange("Mother Tongue", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
{selected.map((value) => {
const label = filterMasters?.marital_statuses?.find((s) => s.id === value)?.marital_status_name || value;
return (
<Chip
key={value}
label={label}
size="small"
onDelete={() => dispatch(updateFilter({ marital_status: filters.marital_status.filter((v) => v !== value) }))}
onMouseDown={(e) => e.stopPropagation()}
/>
);
})}
</Box>
)}
>
{motherTongueOptions.map((lang) => (
<MenuItem key={lang} value={lang}>
{lang}
{filterMasters?.marital_statuses?.map((status) => (
<MenuItem key={status.id} value={status.id}>
{status.marital_status_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</AccordionDetails>
</Accordion>
@@ -207,61 +229,69 @@ const FilterForm = () => {
<FormControl fullWidth>
<InputLabel>Religion</InputLabel>
<Select
value={filters.religion}
multiple
value={filters.religion || []}
label="Religion"
onChange={(e) => {
dispatch(setReligion(e.target.value));
dispatch(updateFilter({ religion: e.target.value }));
handleSelectionChange("Religion", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.religions?.find((r) => r.id === value)?.religion_name || value;
return (
<Chip
key={value}
label={label}
size="small"
onDelete={() => dispatch(updateFilter({ religion: filters.religion.filter((v) => v !== value) }))}
onMouseDown={(e) => e.stopPropagation()}
/>
);
})}
</Box>
)}
>
<MenuItem value="Hindu">Hindu</MenuItem>
<MenuItem value="Muslim">Muslim</MenuItem>
<MenuItem value="Christian">Christian</MenuItem>
<MenuItem value="Sikh">Sikh</MenuItem>
{filterMasters?.religions?.map((rel) => (
<MenuItem key={rel.id} value={rel.id}>
{rel.religion_name}
</MenuItem>
))}
</Select>
</FormControl>
{/* Matches with Horoscope */}
<FormControlLabel
control={
<Checkbox
checked={filters.matchesWithHoroscope}
onChange={(e) => {
dispatch(setMatchesWithHoroscope(e.target.checked));
handleSelectionChange(
"Matches with Horoscope",
e.target.checked
);
}}
/>
}
label="Matches with horoscope"
/>
{/* Caste */}
<FormControl fullWidth>
<InputLabel>Caste (Multi Select)</InputLabel>
<InputLabel>Caste</InputLabel>
<Select
multiple
value={filters.caste}
label="Caste (Multi Select)"
label="Caste"
onChange={(e) => {
dispatch(setCaste(e.target.value));
handleSelectionChange("Caste", e.target.value);
}}
renderValue={(selected) => (
<Box
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}
>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.castes?.find((c) => c.id === value)?.caste_name || value;
return (
<Chip
key={value}
label={label}
size="small"
onDelete={() => dispatch(setCaste(filters.caste.filter((v) => v !== value)))}
onMouseDown={(e) => e.stopPropagation()}
/>
);
})}
</Box>
)}
>
{["Agamudayar", "Pillai", "Vellalar"].map((caste) => (
<MenuItem key={caste} value={caste}>
{caste}
{filterMasters?.castes?.map((caste) => (
<MenuItem key={caste.id} value={caste.id}>
{caste.caste_name}
</MenuItem>
))}
</Select>
@@ -269,21 +299,19 @@ const FilterForm = () => {
{/* Sub-Caste */}
<FormControl fullWidth>
<InputLabel>Sub-Caste (Multi Select)</InputLabel>
<InputLabel>Sub-Caste</InputLabel>
<Select
multiple
value={filters.subCaste}
label="Sub-Caste (Multi Select)"
value={filters.sub_caste}
label="Sub-Caste"
onChange={(e) => {
dispatch(setSubCaste(e.target.value));
handleSelectionChange("Sub-Caste", e.target.value);
}}
renderValue={(selected) => (
<Box
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}
>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
<Chip key={value} label={value} size="small" onDelete={() => dispatch(setSubCaste(filters.sub_caste.filter((v) => v !== value)))} onMouseDown={(e) => e.stopPropagation()} />
))}
</Box>
)}
@@ -296,39 +324,7 @@ const FilterForm = () => {
</Select>
</FormControl>
{/* Star */}
<FormControl fullWidth>
<InputLabel>Star</InputLabel>
<Select
value={filters.star}
label="Star"
onChange={(e) => {
dispatch(updateFilter({ star: e.target.value }));
handleSelectionChange("Star", e.target.value);
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Ashwini">Ashwini</MenuItem>
<MenuItem value="Bharani">Bharani</MenuItem>
</Select>
</FormControl>
{/* Dasham */}
<FormControl fullWidth>
<InputLabel>Dasham</InputLabel>
<Select
value={filters.dasham}
label="Dasham"
onChange={(e) => {
dispatch(updateFilter({ dasham: e.target.value }));
handleSelectionChange("Dasham", e.target.value);
}}
>
<MenuItem value="Doesn't matter">Doesn't matter</MenuItem>
<MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
</Select>
</FormControl>
</div>
</AccordionDetails>
</Accordion>
@@ -356,66 +352,108 @@ const FilterForm = () => {
<FormControl fullWidth>
<InputLabel>Occupation</InputLabel>
<Select
value={filters.occupation}
multiple
value={filters.occupation || []}
label="Occupation"
onChange={(e) => {
dispatch(updateFilter({ occupation: e.target.value }));
handleSelectionChange("Occupation", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.occupations?.find((o) => o.id === value)?.occupation_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ occupation: filters.occupation.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Software Engineer">
Software Engineer
{filterMasters?.occupations?.map((occ) => (
<MenuItem key={occ.id} value={occ.id}>
{occ.occupation_name}
</MenuItem>
<MenuItem value="Doctor">Doctor</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Annual Income</InputLabel>
<Select
value={filters.annualIncome}
multiple
value={filters.annual_income || []}
label="Annual Income"
onChange={(e) => {
dispatch(updateFilter({ annualIncome: e.target.value }));
dispatch(updateFilter({ annual_income: e.target.value }));
handleSelectionChange("Annual Income", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.annual_incomes?.find((i) => i.id === value)?.annual_income_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ annual_income: filters.annual_income.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="0-5 Lakhs">0-5 Lakhs</MenuItem>
<MenuItem value="5-10 Lakhs">5-10 Lakhs</MenuItem>
{filterMasters?.annual_incomes?.map((inc) => (
<MenuItem key={inc.id} value={inc.id}>
{inc.annual_income_name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Employee Type</InputLabel>
<Select
value={filters.employeeType}
multiple
value={filters.employee_type || []}
label="Employee Type"
onChange={(e) => {
dispatch(updateFilter({ employeeType: e.target.value }));
dispatch(updateFilter({ employee_type: e.target.value }));
handleSelectionChange("Employee Type", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.employee_types?.find((et) => et.id === value)?.employee_type_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ employee_type: filters.employee_type.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Government">Government</MenuItem>
<MenuItem value="Private">Private</MenuItem>
{filterMasters?.employee_types?.map((emp) => (
<MenuItem key={emp.id} value={emp.id}>
{emp.employee_type_name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Education</InputLabel>
<Select
value={filters.education}
multiple
value={filters.education || []}
label="Education"
onChange={(e) => {
dispatch(updateFilter({ education: e.target.value }));
handleSelectionChange("Education", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.educations?.find((ed) => ed.id === value)?.education_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ education: filters.education.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Bachelor">Bachelor</MenuItem>
<MenuItem value="Master">Master</MenuItem>
{filterMasters?.educations?.map((edu) => (
<MenuItem key={edu.id} value={edu.id}>
{edu.education_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
@@ -443,50 +481,59 @@ const FilterForm = () => {
<FormControl fullWidth>
<InputLabel>State</InputLabel>
<Select
value={filters.state}
multiple
value={filters.state || []}
label="State"
onChange={(e) => {
dispatch(updateFilter({ state: e.target.value }));
dispatch(updateFilter({ state: e.target.value, district: [] }));
handleSelectionChange("State", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.states?.find((s) => s.id === value)?.state_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ state: filters.state.filter((v) => v !== value), district: [] }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Tamil Nadu">Tamil Nadu</MenuItem>
<MenuItem value="Karnataka">Karnataka</MenuItem>
{filterMasters?.states?.map((state) => (
<MenuItem key={state.id} value={state.id}>
{state.state_name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Country</InputLabel>
<InputLabel>City</InputLabel>
<Select
value={filters.country}
label="Country"
multiple
value={filters.district || []}
label="City"
disabled={!filters.state || filters.state.length === 0}
onChange={(e) => {
dispatch(updateFilter({ country: e.target.value }));
handleSelectionChange("Country", e.target.value);
dispatch(updateFilter({ district: e.target.value }));
handleSelectionChange("City", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = cityOptions.find((c) => c.id === value)?.district_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ district: filters.district.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="India">India</MenuItem>
<MenuItem value="USA">USA</MenuItem>
{cityOptions.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.district_name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Citizenship</InputLabel>
<Select
value={filters.citizenship}
label="Citizenship"
onChange={(e) => {
dispatch(updateFilter({ citizenship: e.target.value }));
handleSelectionChange("Citizenship", e.target.value);
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Indian">Indian</MenuItem>
<MenuItem value="US Citizen">US Citizen</MenuItem>
</Select>
</FormControl>
</div>
</AccordionDetails>
</Accordion>
@@ -512,54 +559,22 @@ const FilterForm = () => {
<FormControl fullWidth>
<InputLabel>Eating Habits</InputLabel>
<Select
value={filters.eatingHabits}
value={filters.diet}
label="Eating Habits"
onChange={(e) => {
dispatch(updateFilter({ eatingHabits: e.target.value }));
dispatch(updateFilter({ diet: e.target.value }));
handleSelectionChange("Eating Habits", e.target.value);
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Vegetarian">Vegetarian</MenuItem>
<MenuItem value="Non-Vegetarian">Non-Vegetarian</MenuItem>
{filterMasters?.diets?.map((diet) => (
<MenuItem key={diet.id} value={diet.id}>
{diet.diet_name}
</MenuItem>
))}
</Select>
</FormControl>
{/* <FormControl fullWidth>
<InputLabel>Smoking Habits</InputLabel>
<Select
value={filters.smokingHabits}
label="Smoking Habits"
onChange={(e) => {
dispatch(
updateFilter({ smokingHabits: e.target.value })
);
handleSelectionChange("Smoking Habits", e.target.value);
}}
>
<MenuItem value="Doesn't matter">Doesn't matter</MenuItem>
<MenuItem value="Non-Smoker">Non-Smoker</MenuItem>
<MenuItem value="Smoker">Smoker</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Drinking Habits</InputLabel>
<Select
value={filters.drinkingHabits}
label="Drinking Habits"
onChange={(e) => {
dispatch(
updateFilter({ drinkingHabits: e.target.value })
);
handleSelectionChange("Drinking Habits", e.target.value);
}}
>
<MenuItem value="Doesn't matter">Doesn't matter</MenuItem>
<MenuItem value="Non-Drinker">Non-Drinker</MenuItem>
<MenuItem value="Social Drinker">Social Drinker</MenuItem>
</Select>
</FormControl> */}
</div>
</AccordionDetails>
</Accordion>
@@ -585,64 +600,116 @@ const FilterForm = () => {
<FormControl fullWidth>
<InputLabel>Family Type</InputLabel>
<Select
value={filters.familyType}
multiple
value={filters.family_type || []}
label="Family Type"
onChange={(e) => {
dispatch(updateFilter({ familyType: e.target.value }));
dispatch(updateFilter({ family_type: e.target.value }));
handleSelectionChange("Family Type", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.family_types?.find((t) => t.id === value)?.family_type_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ family_type: filters.family_type.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Nuclear">Nuclear</MenuItem>
<MenuItem value="Joint">Joint</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Family Status</InputLabel>
<Select
value={filters.familyStatus}
label="Family Status"
onChange={(e) => {
dispatch(updateFilter({ familyStatus: e.target.value }));
handleSelectionChange("Family Status", e.target.value);
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Middle Class">Middle Class</MenuItem>
<MenuItem value="Upper Middle Class">
Upper Middle Class
{filterMasters?.family_types?.map((type) => (
<MenuItem key={type.id} value={type.id}>
{type.family_type_name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Family Value</InputLabel>
<Select
value={filters.familyValue}
label="Family Value"
onChange={(e) => {
dispatch(updateFilter({ familyValue: e.target.value }));
handleSelectionChange("Family Value", e.target.value);
</div>
</AccordionDetails>
</Accordion>
{/* Paid Membership Benefit Section */}
<Accordion
expanded={expandedSections.paidBenefit && filters.isPaidMember}
onChange={(e, isExpanded) => {
if (!filters.isPaidMember) {
toast.error("This feature is locked for paid members only", {
icon: "🔒",
});
return;
}
handleAccordionChange("paidBenefit")(e, isExpanded);
}}
className="mb-4"
>
<AccordionSummary
expandIcon={filters.isPaidMember ? <ChevronDown /> : <Lock size={18} className="text-gray-400" />}
sx={{
backgroundColor: "#f5fbff",
"&:hover": {
backgroundColor: "#fff6f0",
},
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Traditional">Traditional</MenuItem>
<MenuItem value="Moderate">Moderate</MenuItem>
<MenuItem value="Liberal">Liberal</MenuItem>
<div className="flex items-center gap-2 -mx-4 -my-2 px-4 py-2 w-full">
<Crown size={20} className="text-yellow-600" />
<Typography className="font-semibold">
Paid Membership Benefit
</Typography>
</div>
</AccordionSummary>
<AccordionDetails>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
{/* Star Filter */}
<FormControl fullWidth>
<InputLabel>Star</InputLabel>
<Select
multiple
value={filters.star || []}
label="Star"
disabled={!filters.isPaidMember}
onChange={(e) => {
dispatch(updateFilter({ star: e.target.value }));
handleSelectionChange("Star", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.stars?.find((s) => s.id === value)?.star_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ star: filters.star.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
>
{filterMasters?.stars?.map((star) => (
<MenuItem key={star.id} value={star.id}>
{star.star_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</AccordionDetails>
</Accordion>
{/* Submit Button */}
<div className="mt-6 flex justify-center">
{/* Sticky Footer Actions */}
<div className="sticky bottom-0 bg-white p-4 border-t border-gray-200 flex justify-between gap-4 mt-4 z-10 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<Button
variant="outlined"
size="large"
onClick={handleClear}
className="px-8"
color="inherit"
startIcon={<RotateCcw size={20} />}
>
Clear Filters
</Button>
<Button
variant="contained"
size="large"
onClick={handleSubmit}
className="px-12"
startIcon={<Check size={20} />}
>
Apply Filters
</Button>
+1 -1
View File
@@ -27,7 +27,7 @@ const FilterModal = () => {
open={open}
onClose={handleClose}
fullWidth
maxWidth="lg" // adjust: "sm" | "md" | "lg"
maxWidth="md" // adjust: "sm" | "md" | "lg"
>
<DialogTitle sx={{background:"#f5fbff"}}>
<Box display="flex" alignItems="center" justifyContent="space-between">
+350 -173
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateLifestyleDetails } from "../redux/registrationFormSlice";
import { updateLifestyleDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import {
Grid,
FormControl,
@@ -11,52 +11,98 @@ import {
TextField,
Checkbox,
ListItemText,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Tooltip,
} 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 { LocalizationProvider, DatePicker, TimePicker } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { format } from "date-fns";
import { useLifestyleMasters } from "../hooks/useMasters";
import { useStarMasters, usePathamMasters } from "../hooks/useDependentMasters";
import horoscopeImg from "../assets/images/horoscopeimg.png";
const LifestyleDetailsForm = ({
onSubmitStep,
onSkipStep,
errors,
onFieldChange,
isEditMode,
}) => {
const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.lifestyleDetails);
if (!data) return null;
const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: lifestyleMasters, isLoading: isLifestyleMastersLoading } =
useLifestyleMasters();
const dietOptions = useMemo(() => {
const raasiOptions = useMemo(() => {
const raw = lifestyleMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.diet || raw.diets || [];
return raw.raasis || raw.raasi || raw.rasi || [];
}, [lifestyleMasters]);
const hobbyOptions = useMemo(() => {
const panjangamOptions = useMemo(() => {
const raw = lifestyleMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.hobbies || raw.hobby || [];
return raw.panjangam_types || raw.panjangamTypes || [];
}, [lifestyleMasters]);
const { data: starData } = useStarMasters(data.raasi);
const starOptions = useMemo(() => {
if (!starData) return [];
return starData.stars || starData.star || starData.data || (Array.isArray(starData) ? starData : []);
}, [starData]);
const { data: pathamData } = usePathamMasters(data.star);
const pathamOptions = useMemo(() => {
if (!pathamData) return [];
return (
pathamData.pathams ||
pathamData.patham ||
pathamData.data ||
(Array.isArray(pathamData) ? pathamData : [])
);
}, [pathamData]);
const grahaOptions = useMemo(() => {
const raw = lifestyleMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.grahas || raw.graha || [];
}, [lifestyleMasters]);
const [confirmDialog, setConfirmDialog] = useState({
open: false,
type: "",
house: null,
item: "",
sourceHouse: null,
pendingValue: [],
});
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
if (data.dob) {
const date = new Date(data.dob);
if (!isNaN(date.getTime())) {
const day = format(date, "EEEE");
if (data.dayOfBirth !== day) {
handleChange("dayOfBirth", day);
}
}
}
}, [data.dob]);
const handleChange = (field, value) => {
dispatch(updateLifestyleDetails({ [field]: value }));
if (onFieldChange) onFieldChange(field);
@@ -68,23 +114,74 @@ const LifestyleDetailsForm = ({
if (onFieldChange) onFieldChange(field);
};
const handleGrahaChange = (house, value) => {
const next = { ...(data.graha || {}) };
next[house] = value;
dispatch(updateLifestyleDetails({ graha: next }));
if (onFieldChange) onFieldChange("graha");
const handleChartChange = (type, house, newValue) => {
const currentChart = type === "graha" ? data.graha : data.amsam;
const currentHouseValue = currentChart?.[house] || [];
// Find added items
const addedItems = newValue.filter((item) => !currentHouseValue.includes(item));
if (addedItems.length > 0) {
for (const addedItem of addedItems) {
for (const [otherHouse, items] of Object.entries(currentChart || {})) {
if (String(otherHouse) !== String(house) && items && items.includes(addedItem)) {
setConfirmDialog({
open: true,
type,
house,
item: addedItem,
sourceHouse: otherHouse,
pendingValue: newValue,
});
return;
}
}
}
}
// No duplicates, proceed with update
const updates = {};
const nextChart = { ...(currentChart || {}) };
nextChart[house] = newValue;
if (type === "graha") {
updates.graha = nextChart;
} else {
updates.amsam = nextChart;
}
dispatch(updateLifestyleDetails(updates));
if (onFieldChange) onFieldChange(type);
};
const handleAmsamChange = (house, value) => {
const next = { ...(data.amsam || {}) };
next[house] = value;
dispatch(updateLifestyleDetails({ amsam: next }));
if (onFieldChange) onFieldChange("amsam");
const handleConfirmMove = () => {
const { type, house, item, sourceHouse, pendingValue } = confirmDialog;
const currentChart = type === "graha" ? data.graha : data.amsam;
const nextChart = { ...(currentChart || {}) };
// Remove from source house
if (nextChart[sourceHouse]) {
nextChart[sourceHouse] = nextChart[sourceHouse].filter((i) => i !== item);
}
// Add to target house
nextChart[house] = pendingValue;
const updates = type === "graha" ? { graha: nextChart } : { amsam: nextChart };
dispatch(updateLifestyleDetails(updates));
if (onFieldChange) onFieldChange(type);
setConfirmDialog({ ...confirmDialog, open: false });
};
const handleCancelMove = () => {
setConfirmDialog({ ...confirmDialog, open: false });
};
const handleSubmit = () => {
console.log("Submitting lifestyle details:", data);
onSubmitStep();
};
const parseDateValue = (value) => {
@@ -117,7 +214,8 @@ const LifestyleDetailsForm = ({
return `${h}:${m}`;
};
const renderChartCell = (label, value, onChange) => (
const renderChartCell = (label, value = [], onChange) => (
<Tooltip title={value && value.length > 0 ? value.join(", ") : ""} arrow placement="top">
<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}
@@ -130,7 +228,17 @@ const LifestyleDetailsForm = ({
onChange={(e) => onChange(e.target.value)}
renderValue={(selected) => {
if (!selected || selected.length === 0) return `+ Add ${label}`;
return selected.join(", ");
return (
<div style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "60px",
fontSize: "11px"
}}>
{selected.join(", ")}
</div>
);
}}
MenuProps={{
PaperProps: { style: { maxHeight: 280 } },
@@ -138,22 +246,21 @@ const LifestyleDetailsForm = ({
>
{grahaOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
<Checkbox checked={value.indexOf(opt) > -1} />
<Checkbox checked={Array.isArray(value) && value.indexOf(opt) > -1} />
<ListItemText primary={opt} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
</Tooltip>
);
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);
handleChartChange(type, house, value);
const label = type === "graha" ? "Rasi" : "Navamsam";
return (
@@ -165,7 +272,9 @@ const LifestyleDetailsForm = ({
{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 className="w-full aspect-square rounded-full overflow-hidden bg-white">
<img src={horoscopeImg} alt="Horoscope" className="w-full h-full object-contain" />
</div>
</div>
{renderChartCell(label, getValue(6), (value) => onChange(6, value))}
@@ -183,132 +292,13 @@ const LifestyleDetailsForm = ({
return (
<div className="w-full max-w-[1200px] mx-auto 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-2">
<label className="text-gray-900 text-[15px]">
Diet (Multi-select){requiredMark}
</label>
<FormControl fullWidth variant="outlined" error={Boolean(errors.diets)}>
<InputLabel id="diets-label">Select Diet</InputLabel>
<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",
},
}}
>
{dietOptions.map((opt) => {
const value = opt.id ?? opt;
const label = opt.diet_name || opt.name || String(opt);
return (
<MenuItem key={value} value={value}>
<Checkbox checked={data.diets.indexOf(value) > -1} />
<ListItemText primary={label} />
</MenuItem>
);
})}
</Select>
{errors.diets && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.diets}
</p>
)}
</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="grid grid-cols-1 md:grid-cols-4 gap-6 w-full max-w-[1200px] mx-auto">
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Date of Birth{requiredMark}
@@ -331,6 +321,19 @@ const LifestyleDetailsForm = ({
</LocalizationProvider>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Day of Birth</label>
<TextField
fullWidth
name="dayOfBirth"
value={data.dayOfBirth}
readOnly
variant="outlined"
placeholder="Day of Week"
InputProps={{ readOnly: true }}
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Time of Birth{requiredMark}
@@ -352,7 +355,7 @@ const LifestyleDetailsForm = ({
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Place of Birth</label>
<label className="text-gray-900 text-[15px]">Place of Birth{requiredMark}</label>
<TextField
fullWidth
name="placeOfBirth"
@@ -366,32 +369,186 @@ const LifestyleDetailsForm = ({
</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")}
{errors.graha_duplicate && (
<p style={{ color: "#d32f2f", fontSize: "0.75rem", marginTop: "4px" }}>
{errors.graha_duplicate}
</p>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[1200px] mx-auto mt-6">
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Select Raasi{requiredMark}</label>
<FormControl fullWidth variant="outlined" error={Boolean(errors.raasi)} id="raasi">
<InputLabel id="raasi-label">Select Raasi</InputLabel>
<Select
labelId="raasi-label"
label="Select Raasi"
name="raasi"
value={data.raasi}
onChange={(e) => {
handleChange("raasi", e.target.value);
handleChange("star", "");
handleChange("patham", "");
}}
>
{raasiOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.raasi_name || opt.rasi_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Select Birth Star{requiredMark}</label>
<FormControl fullWidth variant="outlined" error={Boolean(errors.star)} id="star">
<InputLabel id="star-label">Select Birth Star</InputLabel>
<Select
labelId="star-label"
label="Select Birth Star"
name="star"
value={data.star}
onChange={(e) => {
handleChange("star", e.target.value);
handleChange("patham", "");
}}
>
{starOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.star_name || opt.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Select Padham</label>
<FormControl fullWidth variant="outlined" id="patham">
<InputLabel id="patham-label">Select Padham</InputLabel>
<Select
labelId="patham-label"
label="Select Padham"
name="patham"
value={data.patham}
onChange={(e) => handleChange("patham", e.target.value)}
>
{pathamOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Select Lagnam</label>
<FormControl fullWidth variant="outlined" id="lagnam">
<InputLabel id="lagnam-label">Select Lagnam</InputLabel>
<Select
labelId="lagnam-label"
label="Select Lagnam"
name="lagnam"
value={data.lagnam}
onChange={(e) => handleChange("lagnam", e.target.value)}
>
{raasiOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.raasi_name || opt.rasi_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Panjangam Type</label>
<FormControl fullWidth variant="outlined" id="panjangam_type">
<InputLabel id="panjangam-label">Select Panjangam Type</InputLabel>
<Select
labelId="panjangam-label"
label="Select Panjangam Type"
name="panjangam_type"
value={data.panjangam_type}
onChange={(e) => handleChange("panjangam_type", e.target.value)}
>
{panjangamOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</div>
<div>
<div className="py-4">
<h3 className="text-base font-semibold text-gray-800 mb-3">
Add Navamsam
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-[1200px] mx-auto mt-6">
<div className="flex flex-col gap-2">
<h3 className="text-base font-semibold text-gray-800 mb-3">Add Rasi</h3>
{renderChartGrid("graha")}
</div>
<div className="flex flex-col gap-2">
<h3 className="text-base font-semibold text-gray-800 mb-3">Add Navamsam</h3>
{renderChartGrid("amsam")}
{errors.amsam_duplicate && (
<p style={{ color: "#d32f2f", fontSize: "0.75rem", marginTop: "4px" }}>
{errors.amsam_duplicate}
</p>
)}
</div>
</div>
<div className="mt-8 w-full max-w-[1200px] mx-auto p-6 border border-gray-200 rounded-lg bg-gray-50">
<h3 className="text-base font-semibold text-gray-800 mb-4">Dasa Details</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Type</label>
<FormControl fullWidth variant="outlined" id="dasa_balance">
<InputLabel id="dasa-label">Select Dasa Type</InputLabel>
<Select
labelId="dasa-label"
label="Select Dasa Type"
name="dasa_balance"
value={data.dasa_balance}
onChange={(e) => handleChange("dasa_balance", e.target.value)}
>
{grahaOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Year</label>
<TextField
id="dasa_years"
fullWidth
name="dasa_years"
value={data.dasa_years}
onChange={(e) => handleChange("dasa_years", e.target.value)}
placeholder="Enter Year"
variant="outlined"
type="number"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Month</label>
<TextField
id="dasa_months"
fullWidth
name="dasa_months"
value={data.dasa_months}
onChange={(e) => handleChange("dasa_months", e.target.value)}
placeholder="Enter Month"
variant="outlined"
type="number"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Day</label>
<TextField
id="dasa_days"
fullWidth
name="dasa_days"
value={data.dasa_days}
onChange={(e) => handleChange("dasa_days", e.target.value)}
placeholder="Enter Day"
variant="outlined"
type="number"
/>
</div>
</div>
</div>
@@ -406,13 +563,33 @@ const LifestyleDetailsForm = ({
justifyContent: "center",
}}
>
{onSkipStep && (
<Button variant="outlined" onClick={onSkipStep}>
Skip
</Button>
)}
<Button variant="contained" color="primary" onClick={handleSubmit}>
Next
{onSkipStep ? "Next" : "Update"}
</Button>
</Grid>
<Dialog
open={confirmDialog.open}
onClose={handleCancelMove}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Duplicate Selection"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{confirmDialog.item} is already selected in House {confirmDialog.sourceHouse}. Do you want to move it to House {confirmDialog.house}?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCancelMove}>Cancel</Button>
<Button onClick={handleConfirmMove} autoFocus>Move</Button>
</DialogActions>
</Dialog>
</form>
</div>
);
+265 -149
View File
@@ -10,6 +10,7 @@ import {
Button,
Checkbox,
ListItemText,
TextField,
} from "@mui/material";
import { usePartnerPreferenceMasters } from "../hooks/useMasters";
import { useCityMasters, useSubCasteMasters } from "../hooks/useDependentMasters";
@@ -30,11 +31,30 @@ const PartnerPreferencesForm = ({
const subCasteQuery = useSubCasteMasters(data.castes);
const cityQuery = useCityMasters(data.states);
const ageRangeOptions = useMemo(() => {
const ageOptions = useMemo(() => Array.from({ length: 53 }, (_, i) => i + 18), []);
const heightOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.ageRange || raw.age_range || [];
return raw.heights || raw.height || [];
}, [masters]);
const maritalStatusOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
return raw.maritalStatus || raw.marital_status || [];
}, [masters]);
const starOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
return raw.stars || raw.star || [];
}, [masters]);
const employeeTypeOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
return raw.employeeTypes || raw.employee_type || [];
}, [masters]);
const casteOptions = useMemo(() => {
@@ -72,6 +92,8 @@ const PartnerPreferencesForm = ({
return raw.annual_income || raw.annualIncome || [];
}, [masters]);
const currencyOptions = useMemo(() => ["INR", "USD"], []);
const stateOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
@@ -96,8 +118,15 @@ const PartnerPreferencesForm = ({
const cityOptions = useMemo(() => {
const raw = cityQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.subCaste || raw.district || raw.data || [];
if (Array.isArray(raw)) {
const merged = raw.flatMap((entry) => {
if (!entry) return [];
if (Array.isArray(entry)) return entry;
return entry.district || entry.districts || entry.data || [];
});
return merged;
}
return raw.district || raw.districts || raw.data || [];
}, [cityQuery.data]);
useEffect(() => {
@@ -107,12 +136,15 @@ const PartnerPreferencesForm = ({
const handleChange = (field, value) => {
const arrayFields = new Set([
"castes",
"subCastes",
"sub_castes",
"occupations",
"educations",
"hobbies",
"states",
"districts",
"marital_statuses",
"birth_stars",
"employee_types",
"currencies",
]);
const nextValue =
arrayFields.has(field) && typeof value === "string"
@@ -122,8 +154,8 @@ const PartnerPreferencesForm = ({
const fieldsToClear = [field];
if (field === "castes") {
updates.subCastes = [];
fieldsToClear.push("subCastes");
updates.sub_castes = [];
fieldsToClear.push("sub_castes");
}
if (field === "states") {
updates.districts = [];
@@ -201,215 +233,297 @@ const PartnerPreferencesForm = ({
<div className="w-full max-w-[1200px] mx-auto 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">
{/* Age Range */}
{/* 1. Age Range */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Age Range{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.ageRange)}
>
<InputLabel id="ageRange-label">Select Age Range</InputLabel>
<label className="text-gray-900 text-[15px]">Age Range (From - To)</label>
<div className="flex gap-2">
<FormControl fullWidth variant="outlined" error={Boolean(errors.age_from)}>
<InputLabel>From Age</InputLabel>
<Select
labelId="ageRange-label"
label="Select Age Range"
name="ageRange"
value={data.ageRange}
onChange={(e) => handleChange("ageRange", e.target.value)}
id="age_from"
name="age_from"
value={data.age_from}
label="From Age"
inputRef={inputRef}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
onChange={(e) => handleChange("age_from", e.target.value)}
>
{ageRangeOptions.map((opt) => (
<MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
{opt.name || opt}
</MenuItem>
{ageOptions.map((age) => (
<MenuItem key={age} value={age}>{age}</MenuItem>
))}
</Select>
{errors.ageRange && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
{errors.age_from && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.age_from}
</p>
)}
</FormControl>
<FormControl fullWidth variant="outlined" error={Boolean(errors.age_to)}>
<InputLabel>To Age</InputLabel>
<Select
id="age_to"
name="age_to"
value={data.age_to}
label="To Age"
onChange={(e) => handleChange("age_to", e.target.value)}
>
{errors.ageRange}
{ageOptions.map((age) => (
<MenuItem key={age} value={age}>{age}</MenuItem>
))}
</Select>
{errors.age_to && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.age_to}
</p>
)}
</FormControl>
</div>
</div>
{/* Caste */}
{/* 2. Height Range */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Caste{requiredMark}
</label>
<label className="text-gray-900 text-[15px]">Height Range (From - To)</label>
<div className="flex gap-2">
<FormControl fullWidth variant="outlined" error={Boolean(errors.height_from)}>
<InputLabel>From Height</InputLabel>
<Select
id="height_from"
name="height_from"
value={data.height_from}
label="From Height"
onChange={(e) => handleChange("height_from", e.target.value)}
>
{heightOptions.map((h) => (
<MenuItem key={h.id} value={h.id}>{h.height_text}</MenuItem>
))}
</Select>
{errors.height_from && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.height_from}
</p>
)}
</FormControl>
<FormControl fullWidth variant="outlined" error={Boolean(errors.height_to)}>
<InputLabel>To Height</InputLabel>
<Select
id="height_to"
name="height_to"
value={data.height_to}
label="To Height"
onChange={(e) => handleChange("height_to", e.target.value)}
>
{heightOptions.map((h) => (
<MenuItem key={h.id} value={h.id}>{h.height_text}</MenuItem>
))}
</Select>
{errors.height_to && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.height_to}
</p>
)}
</FormControl>
</div>
</div>
{/* 3. Marital Status */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Marital Status</label>
{renderMultiSelect({
name: "marital_statuses",
label: "Marital Status",
options: maritalStatusOptions,
value: data.marital_statuses,
getLabel: (opt) => opt.marital_status_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* 4. Birth Star */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Birth Star</label>
{renderMultiSelect({
name: "birth_stars",
label: "Birth Star",
options: starOptions,
value: data.birth_stars,
getLabel: (opt) => opt.star_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* 5. Caste */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Caste</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,
getLabel: (opt) => opt.caste_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Sub Caste */}
{/* 6. Sub-Sect */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Sub Caste{requiredMark}
</label>
<label className="text-gray-900 text-[15px]">Sub-Sect</label>
{renderMultiSelect({
name: "subCastes",
label: "Sub Caste",
name: "sub_castes",
label: "Sub-Sect",
options: subCasteOptions,
value: data.subCastes,
getLabel: (opt) =>
opt.sub_caste_name || opt.subCaste_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
value: data.sub_castes,
getLabel: (opt) => opt.sub_caste_name || opt.name,
getValue: (opt) => opt.id,
disabled: data.castes.length === 0 || subCasteQuery.isLoading,
})}
</div>
{/* Occupation */}
{/* 7. Qualification */}
<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>
<label className="text-gray-900 text-[15px]">Qualification</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,
getLabel: (opt) => opt.education_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Lifestyle and Hobbies */}
{/* 8. Occupation */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Lifestyle & Hobbies{requiredMark}
</label>
<label className="text-gray-900 text-[15px]">Occupation</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,
name: "occupations",
label: "Occupation",
options: occupationOptions,
value: data.occupations,
getLabel: (opt) => opt.occupation_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Annual Income */}
{/* 9. Employee Type */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Annual Income{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.annualIncome)}
>
<InputLabel id="annualIncome-label">
Select Annual Income
</InputLabel>
<Select
labelId="annualIncome-label"
label="Select Annual Income"
name="annualIncome"
value={data.annualIncome}
onChange={(e) => handleChange("annualIncome", e.target.value)}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{annualIncomeOptions.map((opt) => (
<MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
{opt.annual_income_name || opt.name || opt}
</MenuItem>
))}
</Select>
{errors.annualIncome && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.annualIncome}
</p>
)}
</FormControl>
<label className="text-gray-900 text-[15px]">Employee Type</label>
{renderMultiSelect({
name: "employee_types",
label: "Employee Type",
options: employeeTypeOptions,
value: data.employee_types,
getLabel: (opt) => opt.employee_type_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* State */}
{/* 10. Currency Type */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
State{requiredMark}
</label>
<label className="text-gray-900 text-[15px]">Income Currency Type</label>
{renderMultiSelect({
name: "currencies",
label: "Currency",
options: currencyOptions,
value: data.currencies,
getLabel: (opt) => opt,
getValue: (opt) => opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* 11. Income Range */}
{data.currencies.includes("INR") && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Annual Income (INR Range)</label>
<div className="flex gap-2">
<TextField
id="inr_from"
name="inr_from"
fullWidth
label="From (INR)"
value={data.inr_from}
error={Boolean(errors.inr_from)}
helperText={errors.inr_from}
onChange={(e) => handleChange("inr_from", e.target.value)}
/>
<TextField
id="inr_to"
name="inr_to"
fullWidth
label="To (INR)"
value={data.inr_to}
error={Boolean(errors.inr_to)}
helperText={errors.inr_to}
onChange={(e) => handleChange("inr_to", e.target.value)}
/>
</div>
</div>
)}
{data.currencies.includes("USD") && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Annual Income (USD Range)</label>
<div className="flex gap-2">
<TextField
id="usd_from"
name="usd_from"
fullWidth
label="From (USD)"
value={data.usd_from}
error={Boolean(errors.usd_from)}
helperText={errors.usd_from}
onChange={(e) => handleChange("usd_from", e.target.value)}
/>
<TextField
id="usd_to"
name="usd_to"
fullWidth
label="To (USD)"
value={data.usd_to}
error={Boolean(errors.usd_to)}
helperText={errors.usd_to}
onChange={(e) => handleChange("usd_to", e.target.value)}
/>
</div>
</div>
)}
{/* 12. State */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">State</label>
{renderMultiSelect({
name: "states",
label: "State",
options: stateOptions,
value: data.states,
getLabel: (opt) => opt.state_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
getLabel: (opt) => opt.state_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* City */}
{/* 13. City */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
City{requiredMark}
</label>
<label className="text-gray-900 text-[15px]">City</label>
{renderMultiSelect({
name: "districts",
label: "City",
options: cityOptions,
value: data.districts,
getLabel: (opt) =>
opt.district_name || opt.city_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
getLabel: (opt) => opt.district_name || opt.city_name || opt.name,
getValue: (opt) => opt.id,
disabled: data.states.length === 0 || cityQuery.isLoading,
})}
</div>
</div>
<Grid
item
xs={12}
size={12}
style={{
marginTop: 40,
display: "flex",
@@ -417,11 +531,13 @@ const PartnerPreferencesForm = ({
justifyContent: "center",
}}
>
{onSkipStep && (
<Button variant="outlined" onClick={onSkipStep}>
Skip
</Button>
)}
<Button variant="contained" color="primary" onClick={handleSubmit}>
Next
{onSkipStep ? "Next" : "Update"}
</Button>
</Grid>
</form>
File diff suppressed because it is too large. Load diff
+314 -319
View File
@@ -1,11 +1,11 @@
import React from "react";
import { useSelector } from 'react-redux';
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from 'react-redux';
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import { Edit2,Info } from 'lucide-react';
import { Edit2, Info } from 'lucide-react';
import {
Card,
CardContent,
@@ -17,9 +17,17 @@ import {
Button,
Grid,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from '@mui/material';
import { usePreviewDetails } from "../hooks/usePreview";
import { isAuthenticated } from "../utills/auth";
import { usePersonalMasters, usePartnerPreferenceMasters } from "../hooks/useMasters";
import horoscopeImg from "../assets/images/horoscopeimg.png";
import { updatePersonalDetails } from "../redux/registrationFormSlice";
const PreviewScreen = ({ onEdit, onSubmit }) => {
const formData = useSelector((state) => state.registerform);
@@ -28,119 +36,75 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
const isLoading = isAuth && apiLoading;
const isError = isAuth && apiError;
const sections = previewData?.personal_details
? [
{
title: "Personal Details",
step: 1,
data: previewData.personal_details,
},
{
title: "Educational & Professional Details",
step: 2,
data: previewData.educational_details,
},
{
title: "Family Details",
step: 3,
data: previewData.family_details,
},
{
title: "Lifestyle & Habits",
step: 4,
data: previewData.lifestyle_details,
},
{
title: "Partner Preferences",
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 dispatch = useDispatch();
const renderValue = (key, value) => {
if (key === "profiles" || key === "profile" || key === "profile_images" || key === "images") {
const list = Array.isArray(value) ? value : (value ? [value] : []);
return (
<Box display="flex" gap={1} 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>
useEffect(() => {
const pd = previewData?.personal_details;
if (pd) {
const images = pd.images || pd.profile_images;
if (images && images.length > 0) {
const formattedImages = images.map(img =>
typeof img === 'string' ? { url: img, preview: img } : img
);
dispatch(updatePersonalDetails({ profiles: formattedImages }));
}
}
}, [previewData, dispatch]);
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const handleConfirmSubmit = () => {
setOpenConfirmDialog(true);
};
const handleProceedSubmit = () => {
setOpenConfirmDialog(false);
onSubmit();
};
const handleCancelSubmit = () => {
setOpenConfirmDialog(false);
};
const renderValue = (value) => {
if (value === null || value === undefined || value === "") return "-";
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 "-";
if (typeof value === "boolean" || value === 1 || value === "1" || value === 0 || value === "0") {
if (value === true || value === 1 || value === "1") return "Yes";
if (value === false || value === 0 || value === "0") return "No";
}
return value || "-";
return value;
};
const DetailRow = ({ label, value }) => (
<Box display="grid" gridTemplateColumns="45% 55%" gap={1} mb={0.8} alignItems="start">
<Typography color="text.secondary" sx={{ fontWeight: 500, fontSize: '0.875rem' }}>
{label}:
</Typography>
<Typography variant="body2" fontWeight={600} sx={{ wordBreak: 'break-word' }}>
{renderValue(value)}
</Typography>
</Box>
);
const SectionTitle = ({ title, mt = 2 }) => (
<Typography
variant="subtitle1"
sx={{
fontWeight: 'bold',
fontSize: '18px',
color: '#111827',
mt: mt,
mb: 2
}}
>
{title}
</Typography>
);
const renderChartGrid = (getDataForCell, title) => {
const renderCell = (i) => {
const items = getDataForCell(i);
@@ -169,7 +133,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
{items && items.length > 2 ? (
<Box>
{items.slice(0, 2).join(', ')}
<Box component="span" sx={{ color: 'primary.main', display: 'block', fontSize:'0.6rem' }}>+{items.length - 2}</Box>
<Box component="span" sx={{ color: 'primary.main', display: 'block', fontSize: '0.6rem' }}>+{items.length - 2}</Box>
</Box>
) : label}
</Box>
@@ -179,12 +143,12 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
return (
<Box>
<Typography variant="subtitle2" align="center" gutterBottom sx={{ fontWeight: 600 }}>{title}</Typography>
<Typography variant="subtitle2" align="center" gutterBottom sx={{ fontWeight: 600, color: '#CC1F1F' }}>{title}</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0.5, maxWidth: '300px', mx: 'auto', p: 1, bgcolor: '#fff3e0', borderRadius: 2 }}>
{renderCell(1)} {renderCell(2)} {renderCell(3)} {renderCell(4)}
{renderCell(5)}
<Box sx={{ gridColumn: 'span 2', gridRow: 'span 2', bgcolor: '#fff', border: '1px solid #eee', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'0.7rem', fontWeight:'bold', color:'#aaa' }}>
{title.toUpperCase()}
<Box sx={{ gridColumn: 'span 2', gridRow: 'span 2', bgcolor: '#fff', border: '1px solid #eee', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', fontWeight: 'bold', color: '#aaa', overflow: 'hidden' }}>
<img src={horoscopeImg} alt={title} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
</Box>
{renderCell(6)}
{renderCell(7)} {renderCell(8)}
@@ -194,253 +158,284 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
);
};
const { data: pm } = usePersonalMasters();
const { data: ppm } = usePartnerPreferenceMasters();
const getNamesFromIds = (ids, options, labelKey = 'name', valueKey = 'id') => {
if (!ids || (Array.isArray(ids) && ids.length === 0)) return null;
if (!options) return Array.isArray(ids) ? ids.join(", ") : ids;
const idArray = Array.isArray(ids) ? ids : String(ids).split(",").map(v => v.trim());
if (idArray.length === 0) return null;
// Check if the first element is already a name
if (idArray[0] && typeof idArray[0] === 'string' && isNaN(Number(idArray[0]))) {
return idArray.join(", ");
}
const names = idArray.map(id => {
const found = options.find(opt => String(opt[valueKey]) === String(id));
if (found) return found[labelKey];
return id;
});
return names.filter(Boolean).join(", ");
};
const pInfo = previewData?.personal_details || formData.personalDetails;
const eInfo = previewData?.educational_details || formData.educationalDetails;
const fInfo = previewData?.family_details || formData.familyDetails;
const lInfo = previewData?.lifestyle_details || formData.lifestyleDetails;
// Helper to check if partner preferences from API are actually empty
const isPartnerApiDataEmpty = (pp) => {
if (!pp) return true;
// Check for common fields that should have values if data was saved
const hasData = pp.preferred_age_from || pp.age_from || pp.preferred_marital_status_ids?.length > 0 || pp.marital_statuses?.length > 0 || pp.preferred_castes_ids?.length > 0 || pp.castes?.length > 0;
return !hasData;
};
const prInfo = (!isPartnerApiDataEmpty(previewData?.partner_preferences)) ? previewData.partner_preferences :
(!isPartnerApiDataEmpty(previewData?.preferred_details)) ? previewData.preferred_details :
(!isPartnerApiDataEmpty(previewData?.partner_details)) ? previewData.partner_details :
(formData.partnerPreferences || {});
return (
<Box p={3} sx={{maxWidth:"1400px", width:"100%"}} mx="auto" >
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 my-10'>
<Box p={3} sx={{ maxWidth: "1400px", width: "100%" }} mx="auto">
<div className='grid grid-cols-1 md:grid-cols-2 gap-6 my-10'>
{isLoading && (
<Box
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#666" }}
>
<Box py={4} display="flex" justifyContent="center" sx={{ color: "#666", gridColumn: 'span 2' }}>
Loading preview...
</Box>
)}
{isError && (
<Box
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#d32f2f" }}
>
<Box py={4} display="flex" justifyContent="center" sx={{ color: "#d32f2f", gridColumn: 'span 2' }}>
Failed to load preview.
</Box>
)}
{!isLoading &&
sections.map((section) => (
<Card key={section.title} variant="outlined" sx={{ borderRadius: 2, background:"#fff5ed" }}>
{!isLoading && (
<>
{/* 1. Personal Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={
<Typography variant="h6" fontWeight="bold">
{section.title}
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => onEdit(section.step)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{padding:"15px 15px", background:"#f5fbff" }}
title={<Typography variant="h6" fontWeight="bold">Personal Details</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(1)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<Divider />
<CardContent sx={{ pt: 1, }}>
{Object.entries(section.data || {}).map(([key, value]) => {
// Filter out ID fields
if (key === 'id' || key.endsWith('_id') || key.endsWith('Id') || key.endsWith('_ids') || key.endsWith('Ids') || key === 'created_at' || key === 'updated_at') {
return null;
}
// Handle Horoscope Chart (Server Data)
if (key === 'horoscope' && value && typeof value === 'object') {
return (
<Box key={key} sx={{ width: '100%', mt: 2, mb: 2, borderTop: '1px solid #e0e0e0', pt: 2, gridColumn: '1 / -1' }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>Horoscope Details</Typography>
<Grid container spacing={4} justifyContent="center">
<Grid item xs={12} md={6}>
{renderChartGrid((i) => {
const val = value[`graha_${i}`];
return val ? val.split(',') : [];
}, 'Rasi')}
</Grid>
<Grid item xs={12} md={6}>
{renderChartGrid((i) => {
const val = value[`amsam_${i}`];
return val ? val.split(',') : [];
}, 'Navamsam')}
</Grid>
</Grid>
<CardContent>
<Box mb={2}>
{/* Photos Preview */}
<Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1, fontSize: '0.75rem' }}>Profile Photos:</Typography>
<Box display="flex" gap={1} flexWrap="wrap">
{(pInfo.images || pInfo.profile_images || pInfo.profiles || []).map((imgObj, index) => {
const src = typeof imgObj === "string" ? imgObj : imgObj?.preview || imgObj?.url;
return src ? (
<img key={index} src={src} alt="Profile" style={{ width: "80px", height: "80px", objectFit: "cover", borderRadius: "50%", border: "1px solid #ccc" }} />
) : null;
})}
{!(pInfo.images || pInfo.profile_images || pInfo.profiles || []).length && <Typography variant="caption">No photos</Typography>}
</Box>
);
}
// Handle Horoscope Chart (Redux Data)
if (key === 'graha' && value) {
return (
<Box key={key} sx={{ width: '100%', mt: 2, mb: 2, gridColumn: '1 / -1' }}>
{renderChartGrid((i) => value[i] || [], 'Rasi')}
</Box>
);
}
if (key === 'amsam' && value) {
return (
<Box key={key} sx={{ width: '100%', mt: 2, mb: 2, gridColumn: '1 / -1' }}>
{renderChartGrid((i) => value[i] || [], 'Navamsam')}
</Box>
);
}
// Handle brothers and sisters arrays specifically
if ((key === 'brothers' || key === 'sisters') && Array.isArray(value) && value.length > 0) {
return (
<Box key={key} sx={{ py: 2, borderBottom: "1px solid #e0e0e0" }}>
<Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1 }}>
{key === 'brothers' ? 'Brothers Details' : 'Sisters Details'}
</Typography>
<style>
{`
.custom-swiper-${key} .swiper-button-next,
.custom-swiper-${key} .swiper-button-prev {
color: #d32f2f;
width: 25px;
height: 25px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.custom-swiper-${key} .swiper-button-next::after,
.custom-swiper-${key} .swiper-button-prev::after {
font-size: 12px;
font-weight: bold;
}
.custom-swiper-${key} .swiper-pagination-bullet-active {
background-color: #d32f2f;
}
`}
</style>
<Swiper
className={`custom-swiper-${key}`}
modules={[Navigation, Pagination]}
spaceBetween={15}
slidesPerView={1}
navigation
pagination={{ clickable: true }}
style={{ padding: "4px 4px 30px 4px" }}
>
{value.map((sibling, idx) => (
<SwiperSlide key={idx}>
<Card variant="outlined" sx={{ bgcolor: '#fff', height: '100%' }}>
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
{Object.entries(sibling).map(([sKey, sVal]) => {
if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null;
let displayValue = sVal;
if (sKey === 'haveChildrens' || sKey === 'have_childrens') {
if (sVal === true || sVal === 1 || sVal === '1') {
displayValue = 'Yes';
} else if (sVal === false || sVal === 0 || sVal === '0') {
displayValue = 'No';
}
}
return (
<Box key={sKey} display="grid" gridTemplateColumns="45% 55%" gap={1} mb={0.8} alignItems="start">
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 500 }}>
{sKey
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase())
.trim()}
</Typography>
<Typography variant="body2" fontWeight={600} sx={{ wordBreak: 'break-word' }}>
{displayValue ?? '-'}
</Typography>
</Box>
)})}
<DetailRow label="Profile Created For" value={pInfo.profile_for} />
<DetailRow label="Gender" value={pInfo.gender} />
<DetailRow label="Name" value={pInfo.name} />
<DetailRow label="Mobile Number" value={pInfo.mobile || pInfo.mobileNumber || pInfo.mobile_number} />
<DetailRow label="Email Id" value={pInfo.email || pInfo.emailId || pInfo.email_id} />
<DetailRow label="Marital Status" value={getNamesFromIds(pInfo.marital_status, pm?.marital_status, 'marital_status_name')} />
<DetailRow label="Height" value={getNamesFromIds(pInfo.height, pm?.heights, 'height_text')} />
<DetailRow label="Weight" value={pInfo.weight} />
<DetailRow label="Complexion" value={getNamesFromIds(pInfo.complexion, pm?.complexion, 'complexion_name')} />
<DetailRow label="Physical Status" value={getNamesFromIds(pInfo.physical_status, pm?.physicalStatus, 'physical_status_name')} />
<DetailRow label="Religion" value={getNamesFromIds(pInfo.religion, pm?.religion, 'religion_name')} />
<DetailRow label="Caste / Community" value={getNamesFromIds(pInfo.caste, pm?.caste, 'caste_name')} />
<DetailRow label="Sub-Sect" value={getNamesFromIds(pInfo.sub_caste, pm?.subCaste, 'sub_caste_name')} />
<DetailRow label="Willing to marry from same sub-sect" value={pInfo.willing_to_marry} />
<DetailRow label="Inter-Caste Parents" value={pInfo.inter_caste_parents === 1 || pInfo.inter_caste_parents === "1" ? "Yes" : "No"} />
{ (pInfo.inter_caste_parents === 1 || pInfo.inter_caste_parents === "1") && <DetailRow label="Inter-Caste Parents Details" value={pInfo.inter_caste_parents_details} />}
<DetailRow label="Gothram" value={pInfo.gothram} />
<DetailRow label="Do you speak Telugu" value={pInfo.do_you_speak_telugu === 1 || pInfo.do_you_speak_telugu === "1" ? "Yes" : "No"} />
<DetailRow label="About" value={pInfo.about_us} />
<DetailRow label="Language Spoken" value={pInfo.known_languages} />
<DetailRow label="Mother Tongue" value={getNamesFromIds(pInfo.mother_language, pm?.languages, 'language')} />
</CardContent>
</Card>
{/* 2. Educational & Professional Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Educational & Professional Details</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(2)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Highest Qualification" value={getNamesFromIds(eInfo.education, ppm?.education || ppm?.educations, 'education_name')} />
<DetailRow label="Field of Study" value={getNamesFromIds(eInfo.study_field, ppm?.study_fields || ppm?.studyFields, 'study_field_name')} />
<DetailRow label="About" value={eInfo.education_detail || eInfo.educationDetail || eInfo.about} />
<DetailRow label="College Name" value={eInfo.college_name} />
<DetailRow label="Employee type" value={getNamesFromIds(eInfo.employee_type, ppm?.employeeTypes || ppm?.employee_type, 'employee_type_name')} />
<DetailRow label="Occupation" value={getNamesFromIds(eInfo.occupation, ppm?.occupation || ppm?.occupations, 'occupation_name')} />
<DetailRow label="Occupation Details" value={eInfo.occupation_detail || eInfo.occupationDetail} />
<DetailRow label="Organization Name" value={eInfo.company_name || eInfo.organizationName || eInfo.companyName} />
<DetailRow label="Income Currency Type" value={eInfo.income_currency} />
<DetailRow label="Annual Income" value={eInfo.annual_income} />
<SectionTitle title="Work Location" />
<DetailRow label="Country" value={eInfo.work_country_name || eInfo.work_country} />
<DetailRow label="State" value={eInfo.work_state_name || eInfo.work_state} />
<DetailRow label="City" value={eInfo.work_district_name || eInfo.work_city} />
<DetailRow label="Address" value={eInfo.work_location || eInfo.address} />
</CardContent>
</Card>
{/* 3. Family Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Family Details</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(3)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Father Name" value={fInfo.father_name || fInfo.fatherName} />
<DetailRow label="Father Occupation" value={fInfo.father_occupation || fInfo.father_occupational || fInfo.fatherOccupation || fInfo.fatherOccupational || fInfo.father_occupation_name} />
<DetailRow label="Mother Name" value={fInfo.mother_name || fInfo.motherName} />
<DetailRow label="Mother Occupation" value={fInfo.mother_occupation || fInfo.mother_occupational || fInfo.motherOccupation || fInfo.motherOccupational || fInfo.mother_occupation_name} />
{/* Siblings */}
{['brothers', 'sisters'].map(type => (
(fInfo[type] && fInfo[type].length > 0) && (
<Box key={type} mt={2} mb={2}>
<Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1, textTransform: 'capitalize' }}>{type} Details:</Typography>
<Swiper modules={[Navigation, Pagination]} spaceBetween={10} slidesPerView={1} navigation pagination={{ clickable: true }} style={{ paddingBottom: '30px' }}>
{fInfo[type].map((sib, i) => (
<SwiperSlide key={i}>
<Card variant="outlined" sx={{ p: 2, bgcolor: '#f9f9f9' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>{type.slice(0, -1)} {i + 1}</Typography>
<DetailRow label="Name" value={sib.name} />
<DetailRow label={type === 'brothers' ? 'Brother Type' : 'Sister Type'} value={sib.type} />
<DetailRow label="Occupation" value={sib.occupation || sib.occupational || sib.occupation_name} />
<DetailRow label="Marital Status" value={sib.marital_status || sib.maritalStatus || sib.marital_status_id} />
<DetailRow label="Has Children" value={sib.has_children || sib.hasChildren || sib.thereHaveChildren} />
<DetailRow label="Additional Details" value={sib.details || sib.additionalDetails || sib.additional_details} />
</Card>
</SwiperSlide>
))}
</Swiper>
</Box>
);
}
// Convert camelCase or camel_Snake_case to readable words
const formattedKey = key
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase())
.trim();
const content = renderValue(key, value);
return (
)
))}
<Box
key={key}
py={0.7}
borderBottom="1px solid #e0e0e0"
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr",
sm: "1fr 1fr",
},
gap: "10px",
alignItems: "center",
}}
>
<Typography color="text.secondary" sx={{ fontWeight: 500 }}>
{formattedKey}:
</Typography>
<DetailRow label="Family Status" value={fInfo.family_status || fInfo.familyStatus} />
<DetailRow label="Native Place" value={fInfo.native_place || fInfo.nativePlace} />
{/* Special Case: Profiles Image Preview */}
{key === "profiles" || key === "profile" || key === "profile_images" || key === "images" ? (
content
) : value ? (
<Typography
sx={{
fontWeight: 600,
wordBreak: "break-word",
textAlign: "left",
}}
>
{content}
</Typography>
) : (
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No data available</Typography>
</Box>
)}
</Box>
);
})}
<SectionTitle title="Location Living" />
<DetailRow label="Country Living" value={fInfo.family_country_name || fInfo.living_country_name} />
<DetailRow label="Residing State" value={fInfo.family_state_name || fInfo.living_state_name} />
<DetailRow label="Residing City" value={fInfo.family_district_name || fInfo.family_city || fInfo.living_city_name} />
<DetailRow label="Address" value={fInfo.address} />
<DetailRow label="Expectations / Requirements Details" value={fInfo.expectation_details || fInfo.expectationDetails} />
<DetailRow label="Willing to go abroad" value={fInfo.willing_to_go_abroad || fInfo.willingToGoAbroad} />
</CardContent>
</Card>
))}
{/* 4. Lifestyle & Horoscope */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Lifestyle & Horoscope</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(4)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Time of Birth" value={lInfo.time_of_birth_formated || lInfo.tob} />
<DetailRow label="Place of Birth" value={lInfo.place_of_birth || lInfo.placeOfBirth} />
<DetailRow label="Birth Star" value={lInfo.star_name || lInfo.star} />
<DetailRow label="Padham" value={lInfo.patham} />
<DetailRow label="Rasi" value={lInfo.raasi_name || lInfo.raasi} />
<DetailRow label="Lagnam" value={lInfo.lagnam_name || lInfo.lagnam} />
<DetailRow label="Panjangam" value={lInfo.panjangam_type} />
<DetailRow label="Date of Birth" value={lInfo.date_of_birth_formated || lInfo.dob} />
<DetailRow label="Dasa Balance" value={lInfo.dasa_balance} />
<DetailRow label="Dasa Duration" value={(lInfo.dasa_years || lInfo.dasa_months || lInfo.dasa_days) ? `${lInfo.dasa_years || 0} Years, ${lInfo.dasa_months || 0} Months, ${lInfo.dasa_days || 0} Days` : "-"} />
</div>
{/* Horoscope Charts */}
{lInfo.horoscope && (
<Box mt={3}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 6 }}>
{renderChartGrid((i) => lInfo.horoscope[`graha_${i}`]?.split(',') || (lInfo.graha && lInfo.graha[i]) || [], 'Rasi')}
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
{renderChartGrid((i) => lInfo.horoscope[`amsam_${i}`]?.split(',') || (lInfo.amsam && lInfo.amsam[i]) || [], 'Navamsam')}
</Grid>
</Grid>
</Box>
)}
</CardContent>
</Card>
<Grid item xs={12} sx={{display:"flex", justifyContent:"center"}}>
{/* 5. What am I looking for in a Partner */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">What am I looking for in a Partner</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(5)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Age Range" value={(prInfo.preferred_age_from || prInfo.age_from) && (prInfo.preferred_age_to || prInfo.age_to) ? `${prInfo.preferred_age_from || prInfo.age_from} - ${prInfo.preferred_age_to || prInfo.age_to}` : "-"} />
<DetailRow label="Height" value={(getNamesFromIds(prInfo.preferred_height_from_id || prInfo.height_from, ppm?.heights, 'height_text') && getNamesFromIds(prInfo.preferred_height_to_id || prInfo.height_to, ppm?.heights, 'height_text')) ? `${getNamesFromIds(prInfo.preferred_height_from_id || prInfo.height_from, ppm?.heights, 'height_text')} - ${getNamesFromIds(prInfo.preferred_height_to_id || prInfo.height_to, ppm?.heights, 'height_text')}` : "-"} />
<DetailRow label="Marital Status" value={getNamesFromIds(prInfo.preferred_marital_statuses || prInfo.marital_statuses || prInfo.preferred_marital_status || prInfo.marital_status, ppm?.maritalStatus || ppm?.marital_status, 'marital_status_name')} />
<DetailRow label="Birth Star" value={getNamesFromIds(prInfo.preferred_birth_stars || prInfo.birth_stars || prInfo.preferred_birth_star || prInfo.birth_star, ppm?.stars || ppm?.star, 'star_name')} />
<DetailRow label="Caste" value={getNamesFromIds(prInfo.preferred_castes || prInfo.castes || prInfo.preferred_caste || prInfo.caste, ppm?.caste || ppm?.castes, 'caste_name')} />
<DetailRow label="Sub - Sect" value={getNamesFromIds(prInfo.preferred_sub_castes || prInfo.sub_castes || prInfo.preferred_sub_caste || prInfo.sub_caste, ppm?.sub_caste || ppm?.sub_castes, 'sub_caste_name')} />
<DetailRow label="Qualification" value={getNamesFromIds(prInfo.preferred_educations || prInfo.educations || prInfo.preferred_education || prInfo.education, ppm?.education || ppm?.educations, 'education_name')} />
<DetailRow label="Occupation" value={getNamesFromIds(prInfo.preferred_occupations || prInfo.occupations || prInfo.preferred_occupation || prInfo.occupation, ppm?.occupation || ppm?.occupations, 'occupation_name')} />
<DetailRow label="Employee Type" value={getNamesFromIds(prInfo.preferred_employee_types || prInfo.employee_types || prInfo.preferred_employee_type || prInfo.employee_type, ppm?.employeeTypes || ppm?.employee_type, 'employee_type_name')} />
<SectionTitle title="Prefer Annual Income" />
{(prInfo.preferred_currencies || prInfo.currencies || []).includes("INR") && (
<DetailRow label="Prefer Annual Income (INR)" value={(prInfo.preferred_inr_from || prInfo.inr_from || prInfo.preferred_inr_range) ? `From : ${prInfo.preferred_inr_from || prInfo.inr_from || ""} \nTo : ${prInfo.preferred_inr_to || prInfo.inr_to || ""}` : "-"} />
)}
{(prInfo.preferred_currencies || prInfo.currencies || []).includes("USD") && (
<DetailRow label="Prefer Annual Income (USD)" value={(prInfo.preferred_usd_from || prInfo.usd_from || prInfo.preferred_usd_range) ? `From : ${prInfo.preferred_usd_from || prInfo.usd_from || ""} \nTo : ${prInfo.preferred_usd_to || prInfo.usd_to || ""}` : "-"} />
)}
<DetailRow label="State" value={getNamesFromIds(prInfo.preferred_states || prInfo.states || prInfo.preferred_state || prInfo.state, ppm?.states, 'state_name')} />
<DetailRow label="City" value={getNamesFromIds(prInfo.preferred_districts || prInfo.districts || prInfo.preferred_district || prInfo.district, ppm?.districts, 'district_name')} />
</CardContent>
</Card>
</>
)}
</div>
<Grid size={12} sx={{ display: "flex", justifyContent: "center" }}>
<Button
variant="contained"
color="success"
size="large"
onClick={onSubmit}
onClick={handleConfirmSubmit}
sx={{ borderRadius: 2 }}
>
Submit full Completed Data
Submit Full Completed Data
</Button>
</Grid>
<Dialog
open={openConfirmDialog}
onClose={handleCancelSubmit}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Confirm Submission"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Once you submit your details, you will not be able to edit the following fields: Place of Birth, Date of Birth, Rasi and Navamsam.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCancelSubmit}>Cancel</Button>
<Button onClick={handleProceedSubmit} autoFocus>OK</Button>
</DialogActions>
</Dialog>
</Box>
);
};
+369 -481
View File
@@ -1,409 +1,214 @@
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Autoplay } from "swiper/modules";
import { ChevronLeft, ChevronRight, Edit2 } from "lucide-react";
import { useSelector, useDispatch } from "react-redux";
import { Navigation, Autoplay, Pagination } from "swiper/modules";
import { ChevronLeft, ChevronRight, Edit2, Info, Image as ImageIcon } from "lucide-react";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import "swiper/css";
import "swiper/css/navigation";
import LazyImage from "../components/common/LazyImage";
import "swiper/css/pagination";
import weddingpromo1 from "../assets/images/weddingpromo1.jpg";
import weddingpromo2 from "../assets/images/weddingpromo2.jpg";
import weddingpromo3 from "../assets/images/weddingpromo3.jpg";
import weddingpromo4 from "../assets/images/weddingpromo4.jpg";
import horoscopeImg from "../assets/images/horoscopeimg.png";
import ProfileCompletion from "../components/profiledashboard/ProfileCompletion";
import { preloadDummyProfile } from "../redux/registrationFormSlice";
import { useEffect } from "react";
import {
Box,
Button,
Card,
CardContent,
CardHeader,
Divider,
IconButton,
Typography,
Grid,
Tooltip,
} from "@mui/material";
import { Info } from "@mui/icons-material";
import { usePreviewDetails } from "../hooks/usePreview";
import { updatePersonalDetails } from "../redux/registrationFormSlice";
import { usePersonalMasters, usePartnerPreferenceMasters } from "../hooks/useMasters";
const images = [
weddingpromo1, // bride in saree
weddingpromo2, // traditional jewellery
weddingpromo3, // flower details
weddingpromo4, // couple hands
weddingpromo1, // bride close-up
weddingpromo3, // groom + bride portrait
weddingpromo2, // wedding decor
weddingpromo4, // bride in temple
weddingpromo1, // traditional ceremony
weddingpromo3, // couple full shot
const promoImages = [
weddingpromo1,
weddingpromo2,
weddingpromo3,
weddingpromo4,
weddingpromo1,
weddingpromo3,
weddingpromo2,
weddingpromo4,
weddingpromo1,
weddingpromo3,
];
const ProfilePreviewPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const {
personalDetails,
educationalDetails,
familyDetails,
lifestyleDetails,
partnerPreferences,
} = useSelector((state) => state.registerform);
const dispatch = useDispatch();
const { data: previewData, isLoading } = usePreviewDetails();
const { data: pm } = usePersonalMasters();
const { data: ppm } = usePartnerPreferenceMasters();
const getNamesFromIds = (ids, options, labelKey = 'name', valueKey = 'id') => {
if (!ids || (Array.isArray(ids) && ids.length === 0)) return null;
if (!options) return Array.isArray(ids) ? ids.join(", ") : ids;
const idArray = Array.isArray(ids) ? ids : String(ids).split(",").map(v => v.trim());
if (idArray.length === 0) return null;
if (idArray[0] && typeof idArray[0] === 'string' && isNaN(Number(idArray[0]))) {
return idArray.join(", ");
}
const names = idArray.map(id => {
const found = options.find(opt => String(opt[valueKey]) === String(id));
if (found) return found[labelKey];
return id;
});
return names.filter(Boolean).join(", ");
};
// For dummy data on first visit
useEffect(() => {
dispatch(preloadDummyProfile());
}, [dispatch]);
if (previewData?.personal_details) {
const pd = previewData.personal_details;
const images = pd.images || pd.profile_images;
if (images && images.length > 0) {
const formattedImages = images.map((img) =>
typeof img === "string" ? { url: img, preview: img } : img
);
dispatch(updatePersonalDetails({ profiles: formattedImages }));
}
}
}, [previewData, dispatch]);
const formData = useSelector((state) => state.registerform);
const renderValue = (value) => {
if (value === null || value === undefined || value === "") return "-";
if (Array.isArray(value)) {
if (value.length === 0) return "-";
return value.join(", ");
}
if (typeof value === "boolean" || value === 1 || value === "1" || value === 0 || value === "0") {
if (value === true || value === 1 || value === "1") return "Yes";
if (value === false || value === 0 || value === "0") return "No";
}
return value;
};
const DetailRow = ({ label, value }) => (
<Box display="grid" gridTemplateColumns="45% 55%" gap={1} mb={0.8} alignItems="start">
<Typography color="text.secondary" sx={{ fontWeight: 500, fontSize: '0.875rem' }}>
{label}:
</Typography>
<Typography variant="body2" fontWeight={600} sx={{ wordBreak: 'break-word' }}>
{renderValue(value)}
</Typography>
</Box>
);
const SectionTitle = ({ title, mt = 2 }) => (
<Typography
variant="subtitle1"
sx={{
fontWeight: 'bold',
fontSize: '18px',
color: '#111827',
mt: mt,
mb: 2
}}
>
{title}
</Typography>
);
const renderChartGrid = (getDataForCell, title) => {
const renderCell = (i) => {
const items = getDataForCell(i);
const label = Array.isArray(items) ? items.join(', ') : '';
return (
<Tooltip key={i} title={label} arrow placement="top" disableHoverListener={!items || items.length === 0}>
<Box
sx={{
border: '1px solid #ccc',
bgcolor: '#fff',
height: '60px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
fontSize: '0.65rem',
fontWeight: 'bold',
overflow: 'hidden',
p: 0.5,
wordBreak: 'break-word',
lineHeight: 1.1,
cursor: items && items.length > 0 ? 'help' : 'default'
}}
>
{items && items.length > 2 ? (
<Box>
{items.slice(0, 2).join(', ')}
<Box component="span" sx={{ color: 'primary.main', display: 'block', fontSize: '0.6rem' }}>+{items.length - 2}</Box>
</Box>
) : label}
</Box>
</Tooltip>
);
};
return (
<Box>
<Typography variant="subtitle2" align="center" gutterBottom sx={{ fontWeight: 600, color: '#CC1F1F' }}>{title}</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0.5, maxWidth: '300px', mx: 'auto', p: 1, bgcolor: '#fff3e0', borderRadius: 2 }}>
{renderCell(1)} {renderCell(2)} {renderCell(3)} {renderCell(4)}
{renderCell(5)}
<Box sx={{ gridColumn: 'span 2', gridRow: 'span 2', bgcolor: '#fff', border: '1px solid #eee', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', fontWeight: 'bold', color: '#aaa', overflow: 'hidden' }}>
<img src={horoscopeImg} alt={title} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
</Box>
{renderCell(6)}
{renderCell(7)} {renderCell(8)}
{renderCell(9)} {renderCell(10)} {renderCell(11)} {renderCell(12)}
</Box>
</Box>
);
};
const handleEditSection = (stepNum) => {
navigate("/profile-edit", { state: { step: stepNum } });
};
const renderField = (label, value, stepNum = 1) => (
<Box
py={0.7}
borderBottom="1px solid #e0e0e0"
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr",
sm: "1fr 1fr",
},
gap: "10px",
alignItems: "center",
}}
>
<Typography color="text.secondary" sx={{ fontWeight: 500 }}>
{label}:
</Typography>
{value ? (
<Typography
sx={{
fontWeight: 600,
wordBreak: "break-word",
textAlign: "left",
}}
>
{value}
</Typography>
) : (
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No data available</Typography>
</Box>
)}
const pInfo = previewData?.personal_details || formData.personalDetails;
const eInfo = previewData?.educational_details || formData.educationalDetails;
const fInfo = previewData?.family_details || formData.familyDetails;
const lInfo = previewData?.lifestyle_details || formData.lifestyleDetails;
const isPartnerApiDataEmpty = (pp) => {
if (!pp) return true;
const hasData = pp.preferred_age_from || pp.age_from || pp.preferred_marital_status_ids?.length > 0 || pp.marital_statuses?.length > 0 || pp.preferred_castes_ids?.length > 0 || pp.castes?.length > 0;
return !hasData;
};
const prInfo = (!isPartnerApiDataEmpty(previewData?.partner_preferences)) ? previewData.partner_preferences :
(!isPartnerApiDataEmpty(previewData?.preferred_details)) ? previewData.preferred_details :
(!isPartnerApiDataEmpty(previewData?.partner_details)) ? previewData.partner_details :
(formData.partnerPreferences || {});
if (isLoading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="50vh">
<Typography>Loading Profile...</Typography>
</Box>
);
const renderPersonalSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader
title={
<Typography variant="h6" fontWeight="bold">
Personal Details
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => handleEditSection(1)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField("Name", personalDetails.name, 1)}
{renderField("Mobile Number", personalDetails.mobileNumber, 1)}
{renderField("Gender", personalDetails.gender, 1)}
{renderField("Date of Birth", personalDetails.dob, 1)}
{renderField("Height", personalDetails.height, 1)}
{renderField("Weight", personalDetails.weight, 1)}
{renderField("Marital Status", personalDetails.maritalStatus, 1)}
{renderField("Religion", personalDetails.religion, 1)}
{renderField("Caste", personalDetails.caste, 1)}
{renderField("Email", personalDetails.email, 1)}
{renderField("State", personalDetails.state, 1)}
{renderField("City", personalDetails.city, 1)}
{renderField("Pincode", personalDetails.pincode, 1)}
{/* Profile Images */}
<Box
py={0.7}
borderBottom="1px solid #e0e0e0"
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: "10px",
alignItems: "center",
}}
>
<Typography color="text.secondary" sx={{ fontWeight: 500 }}>
Profile Photos:
</Typography>
{personalDetails.profiles && personalDetails.profiles.length > 0 ? (
<Box display="flex" flexWrap="wrap" gap={1}>
{personalDetails.profiles.slice(0, 4).map((profile, index) => (
<Box
key={index}
width={60}
height={60}
borderRadius={1}
overflow="hidden"
sx={{ border: "1px solid #ddd", position: "relative" }}
>
{profile.preview || profile.url ? (
<img
src={profile.preview || profile.url}
alt={`Profile ${index + 1}`}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
<Box
display="flex"
alignItems="center"
justifyContent="center"
height="100%"
bgcolor="#f5f5f5"
>
<ImageIcon size={24} />
</Box>
)}
</Box>
))}
{personalDetails.profiles.length > 4 && (
<Typography variant="caption" color="text.secondary">
+{personalDetails.profiles.length - 4} more
</Typography>
)}
</Box>
) : (
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No photos uploaded</Typography>
</Box>
)}
</Box>
</CardContent>
</Card>
);
const renderEducationalSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader
title={
<Typography variant="h6" fontWeight="bold">
Education Details
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => handleEditSection(2)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField("Qualification", educationalDetails.qualification, 2)}
{renderField("Field of Study", educationalDetails.fieldOfStudy, 2)}
{renderField("College Name", educationalDetails.collegeName, 2)}
{renderField("Occupation", educationalDetails.occupation, 2)}
{renderField("Organization", educationalDetails.organization, 2)}
{renderField("Employee Type", educationalDetails.employeeType, 2)}
{renderField("Income", educationalDetails.income, 2)}
{renderField("Work Location", educationalDetails.workLocation, 2)}
</CardContent>
</Card>
);
const renderFamilySection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader
title={
<Typography variant="h6" fontWeight="bold">
Family Details
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => handleEditSection(2)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField("Father Name", familyDetails.fatherName)}
{renderField("Father Occupation", familyDetails.fatherOccupation)}
{renderField("Mother Name", familyDetails.motherName)}
{renderField("Mother Occupation", familyDetails.motherOccupation)}
{renderField("Brother Count", familyDetails.brotherCount)}
{renderField("Sister Count", familyDetails.sisterCount)}
{renderField("Family Status", familyDetails.familyStatus)}
{renderField("Native Place", familyDetails.nativePlace)}
</CardContent>
</Card>
);
const renderLifestyleSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader
title={
<Typography variant="h6" fontWeight="bold">
Lifestyle Details
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => handleEditSection(2)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField(
"Diet",
Array.isArray(lifestyleDetails.diets)
? 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>
</Card>
);
const renderPreferenceSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader
title={
<Typography variant="h6" fontWeight="bold">
Lifestyle Details
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => handleEditSection(2)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<Divider />
<CardContent sx={{ pt: 1 }}>
{renderField("Age Range", partnerPreferences.ageRange)}
{renderField(
"Caste",
Array.isArray(partnerPreferences.castes)
? partnerPreferences.castes.join(", ")
: partnerPreferences.castes
)}
{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>
</Card>
);
return (
<>
<div className="custom-swiper-hero flex items-center justify-center p-4">
<div className="w-full max-w-7xl relative">
{/* Left Arrow */}
<button className="swiper-button-prev-custom absolute left-0 top-1/2 -translate-y-1/2 z-10 w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all">
<ChevronLeft className="w-6 h-6 text-white" />
</button>
{/* Right Arrow */}
<button className="swiper-button-next-custom absolute right-0 top-1/2 -translate-y-1/2 z-10 w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all">
<ChevronRight className="w-6 h-6 text-white" />
</button>
@@ -425,138 +230,221 @@ const ProfilePreviewPage = () => {
loop={true}
className="mySwiper"
>
{images.map((img, idx) => (
{(pInfo.images || pInfo.profile_images || pInfo.profiles || promoImages).map((imgObj, idx) => {
const src = typeof imgObj === "string" ? imgObj : imgObj?.preview || imgObj?.url;
return src ? (
<SwiperSlide key={idx}>
<div className="relative overflow-hidden rounded-3xl w-[100%] ">
<img
src={img}
alt={`Slide ${idx + 1}`}
className="w-full h-full object-cover"
/>
{/* <LazyImage
src={img}
alt={`Slide ${idx + 1}`}
className="w-full h-full object-cover"
/> */}
{/* <div className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-black/50 backdrop-blur-sm px-6 py-2 rounded-full">
<span className="text-white font-semibold text-lg">Slide {idx + 1}</span>
</div> */}
<div className="relative overflow-hidden rounded-3xl w-[100%] h-full">
<img src={src} alt={`Slide ${idx + 1}`} className="w-full h-full object-cover" />
</div>
</SwiperSlide>
))}
) : null;
})}
</Swiper>
</div>
<style>{`
.custom-swiper-hero .swiper {
width: 100%;
overflow: visible;
}
.custom-swiper-hero .swiper-slide {
width: 320px;
height: 320px;
transition: all 0.4s ease;
opacity: 1;
transform: scale(0.85);
}
.custom-swiper-hero .swiper-slide-active {
width: 630px !important;
height: 320px !important;
opacity: 1;
transform: scale(1);
z-index: 2;
}
.custom-swiper-hero .swiper { width: 100%; overflow: visible; }
.custom-swiper-hero .swiper-slide { width: 320px; height: 320px; transition: all 0.4s ease; transform: scale(0.85); }
.custom-swiper-hero .swiper-slide-active { width: 630px !important; height: 320px !important; transform: scale(1); z-index: 2; }
@media only screen and (max-width: 768px) {
.custom-swiper-hero .swiper-slide {
width: 300px;
height: 150px;
}
.custom-swiper-hero .swiper-slide-active {
width: 300px !important;
height: 150px !important;
}
}
.custom-swiper-hero .swiper-slide > div {
width: 100%;
height: 100%;
}
.custom-swiper-hero .swiper-button-prev-custom,
.custom-swiper-hero .swiper-button-next-custom {
cursor: pointer;
}
@media (max-width: 1024px) {
.custom-swiper-hero .swiper-slide {
width: 240px;
height: 340px;
}
.custom-swiper-hero .swiper-slide-active {
width: 380px !important;
height: 480px !important;
}
}
@media (max-width: 768px) {
.custom-swiper-hero .swiper-slide {
width: 180px;
height: 280px;
}
.custom-swiper-hero .swiper-slide-active {
width: 300px !important;
height: 280px !important;
}
.custom-swiper-hero .swiper-button-prev-custom,
.custom-swiper-hero .swiper-button-next-custom {
width: 40px;
height: 40px;
}
}
@media (max-width: 480px) {
.custom-swiper-hero .swiper-slide {
width: 140px;
height: 220px;
}
.custom-swiper-hero .swiper-slide-active {
width: 300px !important;
height: 220px !important;
}
.custom-swiper-hero .swiper-button-prev-custom,
.custom-swiper-hero .swiper-button-next-custom {
width: 36px;
height: 36px;
}
.custom-swiper-hero .swiper-slide { width: 300px; height: 150px; }
.custom-swiper-hero .swiper-slide-active { width: 300px !important; height: 150px !important; }
}
.custom-swiper-hero .swiper-slide > div { width: 100%; height: 100%; }
`}</style>
</div>
<ProfileCompletion />
{/* personal details start */}
<Box p={3} sx={{ maxWidth: "1400px", width: "100%" }} mx="auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 my-5">
{renderPersonalSection()}
{renderEducationalSection()}
{renderFamilySection()}
{renderLifestyleSection()}
{renderPreferenceSection()}
{/* 1. Personal Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Personal Details</Typography>}
action={<IconButton color="primary" onClick={() => handleEditSection(1)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<Box mb={2}>
<Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1, fontSize: '0.75rem' }}>Profile Photos:</Typography>
<Box display="flex" gap={1} flexWrap="wrap">
{(pInfo.images || pInfo.profile_images || pInfo.profiles || []).map((imgObj, index) => {
const src = typeof imgObj === "string" ? imgObj : imgObj?.preview || imgObj?.url;
return src ? (
<img key={index} src={src} alt="Profile" style={{ width: "80px", height: "80px", objectFit: "cover", borderRadius: "50%", border: "1px solid #ccc" }} />
) : null;
})}
{!(pInfo.images || pInfo.profile_images || pInfo.profiles || []).length && <Typography variant="caption">No photos</Typography>}
</Box>
</Box>
<DetailRow label="Profile Created For" value={pInfo.profile_for} />
<DetailRow label="Gender" value={pInfo.gender} />
<DetailRow label="Name" value={pInfo.name} />
<DetailRow label="Mobile Number" value={pInfo.mobile || pInfo.mobileNumber || pInfo.mobile_number} />
<DetailRow label="Email Id" value={pInfo.email || pInfo.emailId || pInfo.email_id} />
<DetailRow label="Marital Status" value={getNamesFromIds(pInfo.marital_status, pm?.marital_status, 'marital_status_name')} />
<DetailRow label="Height" value={getNamesFromIds(pInfo.height, pm?.heights, 'height_text')} />
<DetailRow label="Weight" value={pInfo.weight} />
<DetailRow label="Complexion" value={getNamesFromIds(pInfo.complexion, pm?.complexion, 'complexion_name')} />
<DetailRow label="Physical Status" value={getNamesFromIds(pInfo.physical_status, pm?.physicalStatus, 'physical_status_name')} />
<DetailRow label="Religion" value={getNamesFromIds(pInfo.religion, pm?.religion, 'religion_name')} />
<DetailRow label="Caste / Community" value={getNamesFromIds(pInfo.caste, pm?.caste, 'caste_name')} />
<DetailRow label="Sub-Sect" value={getNamesFromIds(pInfo.sub_caste, pm?.subCaste, 'sub_caste_name')} />
<DetailRow label="Willing to marry from same sub-sect" value={pInfo.willing_to_marry} />
<DetailRow label="Inter-Caste Parents" value={pInfo.inter_caste_parents === 1 || pInfo.inter_caste_parents === "1" ? "Yes" : "No"} />
{ (pInfo.inter_caste_parents === 1 || pInfo.inter_caste_parents === "1") && <DetailRow label="Inter-Caste Parents Details" value={pInfo.inter_caste_parents_details} />}
<DetailRow label="Gothram" value={pInfo.gothram} />
<DetailRow label="Do you speak Telugu" value={pInfo.do_you_speak_telugu === 1 || pInfo.do_you_speak_telugu === "1" ? "Yes" : "No"} />
<DetailRow label="About" value={pInfo.about_us} />
<DetailRow label="Language Spoken" value={pInfo.known_languages} />
<DetailRow label="Mother Tongue" value={getNamesFromIds(pInfo.mother_language, pm?.languages, 'language')} />
</CardContent>
</Card>
{/* 2. Educational & Professional Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Educational & Professional Details</Typography>}
action={<IconButton color="primary" onClick={() => handleEditSection(2)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Highest Qualification" value={getNamesFromIds(eInfo.education, ppm?.education || ppm?.educations, 'education_name')} />
<DetailRow label="Field of Study" value={getNamesFromIds(eInfo.study_field, ppm?.study_fields || ppm?.studyFields, 'study_field_name')} />
<DetailRow label="About" value={eInfo.education_detail || eInfo.educationDetail || eInfo.about} />
<DetailRow label="College Name" value={eInfo.college_name} />
<DetailRow label="Employee type" value={getNamesFromIds(eInfo.employee_type, ppm?.employeeTypes || ppm?.employee_type, 'employee_type_name')} />
<DetailRow label="Occupation" value={getNamesFromIds(eInfo.occupation, ppm?.occupation || ppm?.occupations, 'occupation_name')} />
<DetailRow label="Occupation Details" value={eInfo.occupation_detail || eInfo.occupationDetail} />
<DetailRow label="Organization Name" value={eInfo.company_name || eInfo.organizationName || eInfo.companyName} />
<DetailRow label="Income Currency Type" value={eInfo.income_currency} />
<DetailRow label="Annual Income" value={eInfo.annual_income} />
<SectionTitle title="Work Location" />
<DetailRow label="Country" value={eInfo.work_country_name || eInfo.work_country} />
<DetailRow label="State" value={eInfo.work_state_name || eInfo.work_state} />
<DetailRow label="City" value={eInfo.work_district_name || eInfo.work_city} />
<DetailRow label="Address" value={eInfo.work_location || eInfo.address} />
</CardContent>
</Card>
{/* 3. Family Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Family Details</Typography>}
action={<IconButton color="primary" onClick={() => handleEditSection(3)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Father Name" value={fInfo.father_name || fInfo.fatherName} />
<DetailRow label="Father Occupation" value={fInfo.father_occupation || fInfo.father_occupational || fInfo.fatherOccupation || fInfo.fatherOccupational || fInfo.father_occupation_name} />
<DetailRow label="Mother Name" value={fInfo.mother_name || fInfo.motherName} />
<DetailRow label="Mother Occupation" value={fInfo.mother_occupation || fInfo.mother_occupational || fInfo.motherOccupation || fInfo.motherOccupational || fInfo.mother_occupation_name} />
{['brothers', 'sisters'].map(type => (
(fInfo[type] && fInfo[type].length > 0) && (
<Box key={type} mt={2} mb={2}>
<Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1, textTransform: 'capitalize' }}>{type} Details:</Typography>
<Swiper modules={[Navigation, Pagination]} spaceBetween={10} slidesPerView={1} navigation pagination={{ clickable: true }} style={{ paddingBottom: '30px' }}>
{fInfo[type].map((sib, i) => (
<SwiperSlide key={i}>
<Card variant="outlined" sx={{ p: 2, bgcolor: '#f9f9f9' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>{type.slice(0, -1)} {i + 1}</Typography>
<DetailRow label="Name" value={sib.name} />
<DetailRow label={type === 'brothers' ? 'Brother Type' : 'Sister Type'} value={sib.type} />
<DetailRow label="Occupation" value={sib.occupation || sib.occupational || sib.occupation_name} />
<DetailRow label="Marital Status" value={sib.marital_status || sib.maritalStatus || sib.marital_status_id} />
<DetailRow label="Has Children" value={sib.has_children || sib.hasChildren || sib.thereHaveChildren} />
<DetailRow label="Additional Details" value={sib.details || sib.additionalDetails || sib.additional_details} />
</Card>
</SwiperSlide>
))}
</Swiper>
</Box>
)
))}
<DetailRow label="Family Status" value={fInfo.family_status || fInfo.familyStatus} />
<DetailRow label="Native Place" value={fInfo.native_place || fInfo.nativePlace} />
<SectionTitle title="Location Living" />
<DetailRow label="Country Living" value={fInfo.family_country_name || fInfo.living_country_name} />
<DetailRow label="Residing State" value={fInfo.family_state_name || fInfo.living_state_name} />
<DetailRow label="Residing City" value={fInfo.family_district_name || fInfo.family_city || fInfo.living_city_name} />
<DetailRow label="Address" value={fInfo.address} />
<DetailRow label="Expectations / Requirements Details" value={fInfo.expectation_details || fInfo.expectationDetails} />
<DetailRow label="Willing to go abroad" value={fInfo.willing_to_go_abroad || fInfo.willingToGoAbroad} />
</CardContent>
</Card>
{/* 4. Lifestyle & Horoscope */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Lifestyle & Horoscope</Typography>}
action={<IconButton color="primary" onClick={() => handleEditSection(4)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Time of Birth" value={lInfo.time_of_birth_formated || lInfo.tob} />
<DetailRow label="Place of Birth" value={lInfo.place_of_birth || lInfo.placeOfBirth} />
<DetailRow label="Birth Star" value={lInfo.star_name || lInfo.star} />
<DetailRow label="Padham" value={lInfo.patham} />
<DetailRow label="Rasi" value={lInfo.raasi_name || lInfo.raasi} />
<DetailRow label="Lagnam" value={lInfo.lagnam_name || lInfo.lagnam} />
<DetailRow label="Panjangam" value={lInfo.panjangam_type} />
<DetailRow label="Date of Birth" value={lInfo.date_of_birth_formated || lInfo.dob} />
<DetailRow label="Dasa Balance" value={lInfo.dasa_balance} />
<DetailRow label="Dasa Duration" value={(lInfo.dasa_years || lInfo.dasa_months || lInfo.dasa_days) ? `${lInfo.dasa_years || 0} Years, ${lInfo.dasa_months || 0} Months, ${lInfo.dasa_days || 0} Days` : "-"} />
{lInfo.horoscope && (
<Box mt={3}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 6 }}>
{renderChartGrid((i) => lInfo.horoscope[`graha_${i}`]?.split(',') || (lInfo.graha && lInfo.graha[i]) || [], 'Rasi')}
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
{renderChartGrid((i) => lInfo.horoscope[`amsam_${i}`]?.split(',') || (lInfo.amsam && lInfo.amsam[i]) || [], 'Navamsam')}
</Grid>
</Grid>
</Box>
)}
</CardContent>
</Card>
{/* 5. What am I looking for in a Partner */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">What am I looking for in a Partner</Typography>}
action={<IconButton color="primary" onClick={() => handleEditSection(5)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Age Range" value={(prInfo.preferred_age_from || prInfo.age_from) && (prInfo.preferred_age_to || prInfo.age_to) ? `${prInfo.preferred_age_from || prInfo.age_from} - ${prInfo.preferred_age_to || prInfo.age_to}` : "-"} />
<DetailRow label="Height" value={(getNamesFromIds(prInfo.preferred_height_from_id || prInfo.height_from, ppm?.heights, 'height_text') && getNamesFromIds(prInfo.preferred_height_to_id || prInfo.height_to, ppm?.heights, 'height_text')) ? `${getNamesFromIds(prInfo.preferred_height_from_id || prInfo.height_from, ppm?.heights, 'height_text')} - ${getNamesFromIds(prInfo.preferred_height_to_id || prInfo.height_to, ppm?.heights, 'height_text')}` : "-"} />
<DetailRow label="Marital Status" value={getNamesFromIds(prInfo.preferred_marital_statuses || prInfo.marital_statuses || prInfo.preferred_marital_status || prInfo.marital_status, ppm?.maritalStatus || ppm?.marital_status, 'marital_status_name')} />
<DetailRow label="Birth Star" value={getNamesFromIds(prInfo.preferred_birth_stars || prInfo.birth_stars || prInfo.preferred_birth_star || prInfo.birth_star, ppm?.stars || ppm?.star, 'star_name')} />
<DetailRow label="Caste" value={getNamesFromIds(prInfo.preferred_castes || prInfo.castes || prInfo.preferred_caste || prInfo.caste, ppm?.caste || ppm?.castes, 'caste_name')} />
<DetailRow label="Sub - Sect" value={getNamesFromIds(prInfo.preferred_sub_castes || prInfo.sub_castes || prInfo.preferred_sub_caste || prInfo.sub_caste, ppm?.sub_caste || ppm?.sub_castes, 'sub_caste_name')} />
<DetailRow label="Qualification" value={getNamesFromIds(prInfo.preferred_educations || prInfo.educations || prInfo.preferred_education || prInfo.education, ppm?.education || ppm?.educations, 'education_name')} />
<DetailRow label="Occupation" value={getNamesFromIds(prInfo.preferred_occupations || prInfo.occupations || prInfo.preferred_occupation || prInfo.occupation, ppm?.occupation || ppm?.occupations, 'occupation_name')} />
<DetailRow label="Employee Type" value={getNamesFromIds(prInfo.preferred_employee_types || prInfo.employee_types || prInfo.preferred_employee_type || prInfo.employee_type, ppm?.employeeTypes || ppm?.employee_type, 'employee_type_name')} />
<SectionTitle title="Prefer Annual Income" />
{(prInfo.preferred_currencies || prInfo.currencies || []).includes("INR") && (
<DetailRow label="Prefer Annual Income (INR)" value={(prInfo.preferred_inr_from || prInfo.inr_from || prInfo.preferred_inr_range) ? `From : ${prInfo.preferred_inr_from || prInfo.inr_from || ""} \nTo : ${prInfo.preferred_inr_to || prInfo.inr_to || ""}` : "-"} />
)}
{(prInfo.preferred_currencies || prInfo.currencies || []).includes("USD") && (
<DetailRow label="Prefer Annual Income (USD)" value={(prInfo.preferred_usd_from || prInfo.usd_from || prInfo.preferred_usd_range) ? `From : ${prInfo.preferred_usd_from || prInfo.usd_from || ""} \nTo : ${prInfo.preferred_usd_to || prInfo.usd_to || ""}` : "-"} />
)}
<DetailRow label="State" value={getNamesFromIds(prInfo.preferred_states || prInfo.states || prInfo.preferred_state || prInfo.state, ppm?.states, 'state_name')} />
<DetailRow label="City" value={getNamesFromIds(prInfo.preferred_districts || prInfo.districts || prInfo.preferred_district || prInfo.district, ppm?.districts, 'district_name')} />
</CardContent>
</Card>
</div>
</Box>
{/* personal details end */}
</>
);
};
+72
View File
@@ -0,0 +1,72 @@
import React, { useState, useEffect } from "react";
import { IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Box, Button, TextField } from "@mui/material";
import { X } from "lucide-react";
const ReportModel = ({ open, onClose, onSubmit, actionType }) => {
const [reason, setReason] = useState("");
useEffect(() => {
if (open) {
setReason("");
}
}, [open]);
const handleSubmit = () => {
onSubmit(reason);
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="sm"
>
<DialogTitle sx={{ background: "#f5fbff" }}>
<Box display="flex" alignItems="center" justifyContent="space-between">
<span className="capitalize" style={{ textTransform: 'capitalize' }}>
{actionType === 'block' ? 'Block Profile' : 'Report Profile'}
</span>
<IconButton onClick={onClose} aria-label="close">
<X size={20} />
</IconButton>
</Box>
</DialogTitle>
<DialogContent dividers>
<Box mt={1}>
<TextField
fullWidth
multiline
rows={4}
variant="outlined"
placeholder={`Enter your reason for ${actionType === 'block' ? 'blocking' : 'reporting'} this profile...`}
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 2, background: "#f5fbff" }}>
<Button onClick={onClose} color="inherit">
Cancel
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={!reason.trim()}
sx={{
bgcolor: actionType === 'block' ? '#d32f2f' : '#034E08',
'&:hover': {
bgcolor: actionType === 'block' ? '#b71c1c' : '#023806',
}
}}
>
{actionType === 'block' ? 'Block' : 'Submit Report'}
</Button>
</DialogActions>
</Dialog>
);
};
export default ReportModel;
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { getDashboardDetails } from "../api/dashboard.api";
export const useDashboardQuery = () => {
return useQuery({
queryKey: ["dashboardDetails"],
queryFn: getDashboardDetails,
// Performance settings:
staleTime: 1000 * 60 * 5, // Data is considered fresh for 5 minutes (reduces API calls)
gcTime: 1000 * 60 * 10, // Unused data is garbage collected after 10 minutes
refetchOnWindowFocus: false, // Prevents refetching when user switches tabs
retry: 1, // Retry failed requests once
});
};
+14
View File
@@ -0,0 +1,14 @@
import { useEffect, useState } from "react";
export function useDebounce(value, delay=500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(()=>{
const handler = setTimeout(() => {
setDebouncedValue(value);
},delay);
return () => clearTimeout(handler);
},[value, delay]);
return debouncedValue;
};
+19
View File
@@ -0,0 +1,19 @@
import { useState, useEffect } from "react";
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
+18 -1
View File
@@ -5,6 +5,7 @@ import {
getSubCasteMasters,
getCityMasters,
getStarMasters,
getPathamMasters,
} from "../api/masters.api";
/** Personal details masters (gender, marital status, religion, gothram, raasi, state, etc.) */
@@ -43,7 +44,15 @@ export const useSubCasteMasters = (caste_id) =>
export const useCityMasters = (state_id) =>
useQuery({
queryKey: ["city-masters", state_id],
queryFn: () => getCityMasters(state_id),
queryFn: async () => {
if (Array.isArray(state_id)) {
const results = await Promise.all(
state_id.map((id) => getCityMasters(id))
);
return results;
}
return getCityMasters(state_id);
},
enabled: Array.isArray(state_id) ? state_id.length > 0 : !!state_id,
});
@@ -54,3 +63,11 @@ export const useStarMasters = (raasi_id) =>
queryFn: () => getStarMasters(raasi_id),
enabled: !!raasi_id,
});
/** Patham depends on star */
export const usePathamMasters = (star_id) =>
useQuery({
queryKey: ["patham-masters", star_id],
queryFn: () => getPathamMasters(star_id),
enabled: !!star_id,
});
+80
View File
@@ -0,0 +1,80 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import {
getProfilesFilterList,
getProfilesFilterMasters,
} from "../api/masters.api";
export const useProfiles = (filters = {}) => {
// Remove empty filters
const cleanFilters = Object.entries(filters).reduce((acc, [key, value]) => {
// Skip default boundaries to prevent strict filtering on null values
if (key === "from_age" && value === 18) return acc;
if (key === "to_age" && value === 70) return acc;
if (key === "from_height" && value === 4.0) return acc;
if (key === "to_height" && value === 7.11) return acc;
if (
value !== "" &&
value !== null &&
value !== undefined &&
!(Array.isArray(value) && value.length === 0)
) {
acc[key] = value;
}
return acc;
}, {});
return useInfiniteQuery({
queryKey: ["profiles-filter-list", cleanFilters],
queryFn: ({ pageParam = 1 }) => {
console.log("[API-REQUEST] Calling profiles/lists with filters:", {
...cleanFilters,
page: pageParam,
});
return getProfilesFilterList({
...cleanFilters,
page: pageParam,
});
},
staleTime: 1000 * 60 * 2,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage, allPages) => {
const currentPageData =
lastPage?.data?.data || lastPage?.data || [];
const totalLoaded = allPages.reduce((acc, page) => {
const pageData = page?.data?.data || page?.data || [];
return acc + pageData.length;
}, 0);
const totalRecords =
lastPage?.data?.recordsFiltered ||
lastPage?.recordsFiltered ||
0;
console.log("totalLoaded:", totalLoaded);
console.log("totalRecords:", totalRecords);
if (totalLoaded < totalRecords) {
return allPages.length + 1;
}
return undefined;
}
});
};
export const useProfilesFilterMasters = () =>
useQuery({
queryKey: ["profiles-filter-masters"],
queryFn: getProfilesFilterMasters,
});
+168
View File
@@ -0,0 +1,168 @@
import { useEffect, useRef, useState } from 'react';
const WS_URL = "wss://www.thirukalyanam.amrithaa.net/backend/reverb/app/xk30gjh2ggmel5szmm5w?protocol=7&client=js&version=1.0";
// SINGLETON state to share across all components
let globalSocket = null;
let globalMessages = [];
let globalIsConnected = false;
let globalActiveChannels = new Set();
let subscribers = new Set();
let reconnectTimer = null;
let heartbeatTimer = null;
const notifySubscribers = () => {
subscribers.forEach(callback => callback({
messages: [...globalMessages],
isConnected: globalIsConnected
}));
};
const connect = () => {
if (globalSocket?.readyState === WebSocket.OPEN) return;
if (globalSocket?.readyState === WebSocket.CONNECTING) return;
console.log("[WS] Connecting to:", WS_URL);
const socket = new WebSocket(WS_URL);
globalSocket = socket;
socket.onopen = () => {
console.log("[WS] Open - Waiting for handshake...");
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log("[WS] RAW:", data);
// 1. Handshake
if (data.event === "pusher:connection_established") {
console.log("[WS] Handshake Complete | Socket ID:", data.data ? JSON.parse(data.data).socket_id : "N/A");
globalIsConnected = true;
// Auto-subscribe to all pending channels
if (globalActiveChannels.size > 0) {
console.log(`[WS] Re-subscribing to ${globalActiveChannels.size} channels...`);
globalActiveChannels.forEach(channel => {
socket.send(JSON.stringify({
event: "pusher:subscribe",
data: { channel }
}));
console.log(`[WS] Subscribing to: ${channel}`);
});
}
notifySubscribers();
return;
}
// 2. Heartbeat
if (data.event === "pusher:ping") {
socket.send(JSON.stringify({ event: "pusher:pong", data: {} }));
return;
}
if (data.event === "pusher:pong") return;
// 3. Subscription Succeeded
if (data.event === "pusher_internal:subscription_succeeded") {
console.log(`[WS] ✅ Subscribed to ${data.channel}`);
return;
}
// 4. Subscription Error
if (data.event === "pusher:subscription_error") {
console.error(`[WS] ❌ Subscription failed for ${data.channel}`, data.data);
return;
}
// 5. Regular Events
console.log(`[WS] 📩 Event: ${data.event} | Channel: ${data.channel}`);
globalMessages = [...globalMessages, data];
notifySubscribers();
} catch (err) {
console.error("[WS] Parse Error:", err, event.data);
}
};
socket.onclose = (event) => {
console.log(`[WS] Disconnected (Code: ${event.code}) - Reconnecting in 5s...`);
globalIsConnected = false;
notifySubscribers();
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 5000);
};
socket.onerror = (err) => {
console.error("[WS] Error:", err);
socket.close();
};
// Heartbeat
if (heartbeatTimer) clearInterval(heartbeatTimer);
heartbeatTimer = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ event: "pusher:ping", data: {} }));
}
}, 20000);
};
export const useWebSocket = (channels = []) => {
const [state, setState] = useState({
messages: globalMessages,
isConnected: globalIsConnected
});
useEffect(() => {
// Add this component's listener
const callback = (newState) => setState(newState);
subscribers.add(callback);
// Initialize connection if needed
if (!globalSocket) {
connect();
}
return () => {
subscribers.delete(callback);
};
}, []);
// Handle Dynamic Subscriptions
useEffect(() => {
if (!channels || channels.length === 0) return;
channels.forEach(channel => {
if (channel && !globalActiveChannels.has(channel)) {
globalActiveChannels.add(channel);
if (globalIsConnected && globalSocket?.readyState === WebSocket.OPEN) {
globalSocket.send(JSON.stringify({
event: "pusher:subscribe",
data: { channel }
}));
console.log(`[WS] Dynamic Subscribe: ${channel}`);
}
}
});
// Optional: Cleanup old channels if they are no longer in the provided array
// But for a chat app, we often want to keep listening to notification channels.
// So we'll leave them for now unless we implement a more complex cleanup.
}, [channels]);
const unsubscribe = (channel) => {
if (channel && globalActiveChannels.has(channel)) {
globalActiveChannels.delete(channel);
if (globalIsConnected && globalSocket?.readyState === WebSocket.OPEN) {
globalSocket.send(JSON.stringify({
event: "pusher:unsubscribe",
data: { channel }
}));
console.log(`[WS] Unsubscribed from: ${channel}`);
}
}
};
return { ...state, unsubscribe };
};
+4 -1
View File
@@ -7,8 +7,9 @@ import { ThemeProvider } from "@mui/material/styles";
import theme from "./theme";
import { Provider } from "react-redux";
import { store } from "./redux/store.js";
import { persistor, store } from "./redux/store.js";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PersistGate } from "redux-persist/integration/react";
// Disable noisy logs in production while keeping warnings/errors.
if (import.meta.env.PROD) {
console.log = () => {};
@@ -31,9 +32,11 @@ ReactDOM.createRoot(document.getElementById("root")).render(
<ThemeProvider theme={theme}>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</PersistGate>
</Provider>
</ThemeProvider>
+119 -140
View File
@@ -1,42 +1,57 @@
import React, { useState } from 'react';
import { Tabs, Tab, Box, Chip } from '@mui/material';
import { CheckCircle, Phone, ExpandMore } from '@mui/icons-material';
import React, { useState, useEffect } from 'react';
import { Tabs, Tab, Box, CircularProgress } from '@mui/material';
import { CheckCircle, Phone } from '@mui/icons-material';
import { ChevronDown } from 'lucide-react';
import { getBlockedProfiles, getReportedProfiles, unblockProfile } from '../services/profileActionApi';
import { toast } from 'react-hot-toast';
const BlockedProfile = ({ profile }) => (
const BlockedProfile = ({ profile, onUnblock }) => (
<div className="bg-white border border-1 border-red-100 rounded-lg shadow-sm p-6 mb-4">
<div className="flex items-start gap-4">
<div className="relative flex-shrink-0">
<img
src={profile.image}
src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name}
className="w-32 h-32 rounded-2xl object-cover border-2 border-[#A70710]"
/>
<button className="w-8 h-8 flex justify-center items-center absolute bottom-0 right-0 bg-[#A70710] text-white rounded-full shadow-lg">
{/* <button className="w-8 h-8 flex justify-center items-center absolute bottom-0 right-0 bg-[#A70710] text-white rounded-full shadow-lg">
<Phone className="w-4 h-4" />
</button>
</button> */}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
{/* <div className="flex items-center gap-2 mb-1">
<CheckCircle className="text-green-500 w-5 h-5" />
<span className="text-green-500 font-medium">Verified</span>
</div>
</div> */}
<h2 className="text-2xl font-bold text-gray-900 mb-1">{profile.name}</h2>
<p className="text-gray-500 text-sm mb-3">{profile.id} | Profile Created by Parent</p>
<p className="text-gray-500 text-sm mb-3">{profile.member_id} | Profile Created by Parent</p>
<div className="space-y-1 text-gray-700">
<p className="font-medium">{profile.age} yrs, {profile.height}, {profile.language},</p>
<p className="font-medium">{profile.location},</p>
<p className="font-medium">{profile.education}, {profile.occupation}, ₹ {profile.income}, {profile.state}, India</p>
<p className="font-medium">{profile.age ? `${profile.age} yrs` : ''}{profile.age && profile.height ? ', ' : ''}{profile.height || ''}</p>
<p className="font-medium">
{[profile.district_name, profile.state_name].filter(Boolean).join(', ')}
</p>
<p className="font-medium">
{[
profile.education,
profile.occupation,
profile.annual_income_name ? `₹ ${profile.annual_income_name}` : null
].filter(Boolean).join(', ')}
</p>
</div>
</div>
</div>
<div className="mt-6 flex items-center justify-between border-t border-[#A70710] pt-4">
<p className="text-gray-600">You have blocked this profile</p>
<button className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors">
<button
onClick={() => onUnblock(profile.id)}
className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors"
>
UnBlock
</button>
</div>
@@ -49,7 +64,7 @@ const ReportedProfile = ({ profile, onViewReason }) => {
<div className="flex flex-col sm:flex-row items-start gap-4">
<div className='overflow-hidden w-[100%] h-[100%] max-w-50 max-h-45 rounded-lg flex-shrink-0'>
<img
src={profile.image}
src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name}
className="w-full h-full object-cover "
/>
@@ -57,25 +72,39 @@ const ReportedProfile = ({ profile, onViewReason }) => {
<div className="flex-1 w-full">
<h3 className="text-lg font-bold text-gray-900 mb-1">{profile.name}</h3>
<p className="text-sm text-gray-500 mb-2">
ID : {profile.id} <span className="text-xs ml-1">Last seen {profile.lastSeen}</span>
ID : {profile.member_id} {profile.last_seen_at && <span className="text-xs ml-1">{profile.last_seen_at}</span>}
</p>
<div className="space-y-1.5 text-sm">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-gray-400">•</span>
<span className="text-gray-600">Profile created by Parent</span>
{profile.age && (
<>
<span className="text-gray-400">•</span>
<span className="text-gray-600">{profile.age} yrs</span>
</>
)}
</div>
{profile.caste_name && (
<div className="flex items-center gap-2">
<span className="text-gray-400">•</span>
<span className="text-gray-600">{profile.caste}</span>
<span className="text-gray-600">{profile.caste_name}</span>
</div>
)}
<div className="flex items-center gap-2 flex-wrap">
{profile.occupation && (
<>
<span className="text-gray-400">•</span>
<span className="text-gray-600">{profile.occupation}</span>
</>
)}
{profile.district_name && (
<>
<span className="text-gray-400">•</span>
<span className="text-gray-600">{profile.location}</span>
<span className="text-gray-600">{profile.district_name}</span>
</>
)}
</div>
</div>
@@ -100,20 +129,20 @@ const ReportReasonModal = ({ profile, onClose }) => {
<div className="bg-white rounded-lg shadow-2xl max-w-md w-full p-6 animate-slideUp">
<div className="flex items-start gap-4 mb-4">
<img
src={profile.image}
src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name}
className="w-16 h-20 rounded-lg object-cover flex-shrink-0"
/>
<div>
<h3 className="text-lg font-bold text-gray-900">{profile.name}</h3>
<p className="text-sm text-gray-500">ID : {profile.id}</p>
<p className="text-sm text-gray-500">ID : {profile.member_id}</p>
</div>
</div>
<div className="border-t pt-4">
<h4 className="font-bold text-gray-900 mb-3">Reason For Report</h4>
<p className="text-sm text-gray-600 leading-relaxed bg-gray-50 p-3 rounded-lg mb-4">
{profile.reportReason}
{profile.reason}
</p>
</div>
<div className='w-full flex justify-center'>
@@ -135,116 +164,62 @@ const ReportReasonModal = ({ profile, onClose }) => {
function BlockedProfileListPage() {
const [activeTab, setActiveTab] = useState(0);
const [selectedReport, setSelectedReport] = useState(null);
const blockedProfiles = [
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
}
];
const [blockedProfiles, setBlockedProfiles] = useState([]);
const [reportedProfiles, setReportedProfiles] = useState([]);
const [loading, setLoading] = useState(true);
const reportedProfiles = [
{
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non – IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
},
{
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non – IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
},
{
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non – IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
},
{
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non – IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
const [blockedRes, reportedRes] = await Promise.all([
getBlockedProfiles(),
getReportedProfiles()
]);
if (blockedRes.status === "success") {
setBlockedProfiles(blockedRes.data);
}
];
if (reportedRes.status === "success") {
setReportedProfiles(reportedRes.data);
}
} catch (error) {
console.error("Error fetching data:", error);
toast.error("Failed to load profiles");
} finally {
setLoading(false);
}
};
const handleUnblock = async (profileId) => {
try {
const res = await unblockProfile(profileId);
if (res.status === "success") {
toast.success(res.message || "Profile unblocked successfully");
setBlockedProfiles(prev => prev.filter(p => p.id !== profileId));
} else {
toast.error(res.message || "Failed to unblock profile");
}
} catch (error) {
toast.error("Something went wrong");
}
};
const handleTabChange = (event, newValue) => {
setActiveTab(newValue);
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<CircularProgress color="error" />
</Box>
);
}
return (
<div className=" py-4 md:py-8">
<div className="max-w-[1400px] mx-auto">
@@ -259,14 +234,14 @@ function BlockedProfileListPage() {
textTransform: 'none',
fontSize: '1rem',
fontWeight: 600,
minWidth: 120,
minWidth: 150,
},
'& .Mui-selected': {
color: '#fff !important',
background:"#A70710"
},
'& .MuiTabs-indicator': {
backgroundColor: '#A70710',
backgroundColor: 'transparent',
},
}}
>
@@ -277,28 +252,32 @@ function BlockedProfileListPage() {
<div className="transition-all duration-300">
{activeTab === 0 && (
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-2'>
{blockedProfiles.map((profile, index) => (
<BlockedProfile key={index} profile={profile} />
))}
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-4 px-4'>
{blockedProfiles.length > 0 ? (
blockedProfiles.map((profile, index) => (
<BlockedProfile key={profile.id || index} profile={profile} onUnblock={handleUnblock} />
))
) : (
<div className="col-span-full text-center py-10 text-gray-500">No blocked profiles found.</div>
)}
</div>
)}
{activeTab === 1 && (
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-2'>
{reportedProfiles.map((profile, index) => (
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-4 px-4'>
{reportedProfiles.length > 0 ? (
reportedProfiles.map((profile, index) => (
<ReportedProfile
key={index}
key={profile.id || index}
profile={profile}
onViewReason={setSelectedReport}
/>
))}
</div>
))
) : (
<div className="col-span-full text-center py-10 text-gray-500">No reported profiles found.</div>
)}
</div>
)}
</div>
{/* Report Reason Modal */}
<ReportReasonModal
@@ -307,7 +286,7 @@ function BlockedProfileListPage() {
/>
</div>
<style jsx>{`
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
+498 -375
View File
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { getContactUs } from '../api/contact.api';
import LazyImage from '../components/common/LazyImage';
import InstagramIcon from '@mui/icons-material/Instagram';
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
import FacebookIcon from '@mui/icons-material/Facebook';
import SvgIcon from '@mui/material/SvgIcon';
import { Phone, Mail, ChevronRight } from 'lucide-react';
@@ -39,7 +40,13 @@ const ContactUsPage = () => {
icon: <XIcon fontSize="large" />,
color: "from-gray-800 to-black",
url: contact.x_url
}
},
{
name: "WhatsApp",
icon: <WhatsAppIcon fontSize="large" />,
color: "from-green-500 to-green-600",
url: contact.whatsapp_mobile ? `https://wa.me/${contact.whatsapp_mobile}` : null
},
];
if (isLoading) {
File diff suppressed because it is too large. Load diff
+46 -6
View File
@@ -1,19 +1,59 @@
import React, { useState, useEffect } from 'react';
import { useParams } from "react-router-dom";
import MatrimonyProfile from "../components/profiledetail/MatrimonyProfile"
import PartnerPreferences from "../components/profiledetail/PartnerPreferences"
import MatchingList from "../components/profiledashboard/MatchingList";
import { getProfileDetail } from "../services/profileActionApi";
import { CircularProgress, Box } from "@mui/material";
const ProfileDetailPage = () => {
const { id } = useParams();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const fetchDetail = async (showLoading = true) => {
if (showLoading) setLoading(true);
try {
const res = await getProfileDetail(id);
setData(res);
} catch (error) {
console.error("Failed to fetch profile details:", error);
} finally {
if (showLoading) setLoading(false);
}
};
useEffect(() => {
if (id) {
fetchDetail();
}
}, [id]);
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<CircularProgress color="error" />
</Box>
);
}
if (!data) {
return (
<div className="text-center py-20 text-gray-500 text-xl">
Profile details not found.
</div>
);
}
return (
<>
<div className="w-[100%] max-w-[1400px] mx-auto my-10">
<MatrimonyProfile/>
<PartnerPreferences/>
<MatchingList/>
<MatrimonyProfile data={data} onRefresh={fetchDetail} />
<PartnerPreferences data={data} />
<MatchingList matches={data.all_matches} />
</div>
</>
)
}
);
};
export default ProfileDetailPage
export default ProfileDetailPage;
+245 -154
View File
@@ -1,177 +1,268 @@
// src/pages/SubscriptionHistory.jsx
import React from 'react';
import { ArrowBackIosNew, CalendarToday, AccessTime, CreditCard, Person, Visibility } from '@mui/icons-material';
import { ArrowBackIosNew, CalendarToday, AccessTime, CreditCard, Person, Visibility, History, WorkspacePremium, ReceiptLong, LocalActivity } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { CrownIcon } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { CrownIcon, ShieldCheck, Zap, ArrowRight, CheckCircle2 } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { getSubscriptionHistory } from '../api/subscription.api';
import { CircularProgress, Box, Typography, Chip } from '@mui/material';
const subscriptions = [
{
id: "SUB722HSN",
plan: "Gold Plan",
profileCount: 150,
usedCount: 70,
billingCycle: "Monthly",
expireDate: "19/11/2025",
startDate: "19/10/2025",
startTime: "10:00 AM",
paymentMethod: "UPI Mode",
amount: 1800,
isActive: true
},
{
id: "SUB722HSN",
plan: "Gold Plan",
profileCount: 150,
usedCount: 70,
billingCycle: "Monthly",
expireDate: "19/11/2025",
startDate: "19/10/2025",
startTime: "10:00 AM",
paymentMethod: "UPI Mode",
amount: 1800,
isActive: false
},
const PlanCard = ({ sub, isActive = false, index = 0 }) => {
if (!sub) return null;
{ id: "SUB722HSN",
plan: "Gold Plan",
profileCount: 150,
usedCount: 70,
billingCycle: "Monthly",
expireDate: "19/11/2025",
startDate: "19/10/2025",
startTime: "10:00 AM",
paymentMethod: "UPI Mode",
amount: 1800,
isActive: false
}
];
return (
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
whileHover={{ y: -8, transition: { duration: 0.3 } }}
transition={{ delay: index * 0.1, duration: 0.5 }}
className={`group relative rounded-[32px] overflow-hidden border transition-all duration-500 ${
isActive
? 'border-amber-300 shadow-[0_20px_50px_rgba(255,193,7,0.15)] bg-white'
: 'border-gray-100 bg-white/80 backdrop-blur-sm shadow-xl'
}`}
>
{/* Dynamic Background Pattern */}
<div className={`absolute inset-0 opacity-[0.03] pointer-events-none ${isActive ? 'bg-[radial-gradient(circle_at_50%_50%,#ffc107_0%,transparent_70%)]' : ''}`} />
{/* Decorative Gradient Glow */}
{isActive && (
<div className="absolute -right-20 -top-20 w-40 h-40 bg-amber-400/20 blur-[80px] rounded-full pointer-events-none" />
)}
{/* Header Section */}
<div className={`relative px-6 pt-12 pb-6 text-center ${isActive ? 'bg-gradient-to-b from-amber-50/50 to-transparent' : ''}`}>
{/* Status Badge */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 flex items-center gap-2">
{isActive ? (
<motion.div
animate={{ scale: [1, 1.05, 1] }}
transition={{ repeat: Infinity, duration: 2 }}
className="bg-gradient-to-r from-amber-500 to-orange-600 text-white px-5 py-1.5 rounded-full text-[10px] font-black tracking-widest shadow-lg flex items-center gap-2"
>
<Zap size={12} fill="white" /> ACTIVE PLAN
</motion.div>
) : (
<div className="bg-gray-100 text-gray-500 px-4 py-1 rounded-full text-[10px] font-bold tracking-widest flex items-center gap-2">
<History size={12} /> COMPLETED
</div>
)}
</div>
<h3 className="text-2xl font-black text-gray-900 mb-1">{sub.plan_name}</h3>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-[0.2em]">Transaction ID: #TK{sub.id}</p>
</div>
<div className="px-6 pb-8">
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-3 mb-6">
<div className="relative group/stat p-4 rounded-2xl bg-gray-50 border border-gray-100 transition-colors hover:bg-amber-50/50 overflow-hidden">
<div className="absolute top-0 right-0 p-2 opacity-10 group-hover/stat:opacity-20 transition-opacity">
<Person />
</div>
<p className="text-2xl font-black text-gray-900 mb-0.5">
{sub.count_of_profile_view === -1 ? '∞' : sub.count_of_profile_view}
</p>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-wider">Profile Views</p>
</div>
<div className="relative group/stat p-4 rounded-2xl bg-gray-50 border border-gray-100 transition-colors hover:bg-green-50/50 overflow-hidden">
<div className="absolute top-0 right-0 p-2 opacity-10 group-hover/stat:opacity-20 transition-opacity">
<Visibility />
</div>
<p className="text-2xl font-black text-gray-900 mb-0.5">{sub.used_count_of_profile_view || 0}</p>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-wider">Used Count</p>
</div>
</div>
{/* Benefits/Details List */}
<div className="space-y-2 mb-8">
<DetailRow icon={<CalendarToday className="text-amber-500" />} label="Activated" value={new Date(sub.supscription_start_date).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })} />
<DetailRow icon={<AccessTime className="text-red-500" />} label="Expiry" value={new Date(sub.supscription_expiry_date).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })} />
<DetailRow icon={<ShieldCheck className="text-blue-500" size={16} />} label="Duration" value={sub.plan_validity_days === -1 ? 'Lifetime Access' : `${sub.plan_validity_days} Days`} />
</div>
{/* Price Tag */}
<div className={`p-4 rounded-2xl flex items-center justify-between ${isActive ? 'bg-amber-50 border border-amber-100' : 'bg-gray-50 border border-gray-100'}`}>
<div className="flex items-center gap-2">
<div className={`p-2 rounded-lg ${isActive ? 'bg-amber-500 text-white' : 'bg-gray-200 text-gray-500'}`}>
<CreditCard fontSize="small" />
</div>
<span className="text-xs font-bold text-gray-500">Total Paid</span>
</div>
<span className={`text-xl font-black ${isActive ? 'text-amber-600' : 'text-gray-900'}`}>
₹{sub.plan_amount?.toLocaleString()}
</span>
</div>
</div>
</motion.div>
);
};
const DetailRow = ({ icon, label, value }) => (
<div className="flex items-center justify-between py-2.5 px-4 rounded-xl hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<span className="flex-shrink-0 opacity-80">{icon}</span>
<span className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">{label}</span>
</div>
<span className="text-xs font-bold text-gray-900">{value}</span>
</div>
);
export default function SubscriptionHistory() {
const navigate = useNavigate();
return (
<div className="min-h-screen ">
{/* Royal Header */}
<motion.header
initial={{ y: -100 }}
animate={{ y: 0 }}
className="bg-[#034E08] text-white shadow-2xl rounded-[10px]"
>
<div className="flex items-center justify-between px-5 py-8 safe-area-top relative">
const { data: historyData, isLoading, error } = useQuery({
queryKey: ['subscriptionHistory'],
queryFn: getSubscriptionHistory,
});
<h1 className="text-2xl font-bold">Subscription History</h1>
<div className="w-12" />
if (isLoading) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-white">
<motion.div
animate={{ scale: [1, 1.1, 1], rotate: 360 }}
transition={{ repeat: Infinity, duration: 2 }}
className="mb-6"
>
<WorkspacePremium sx={{ fontSize: 60, color: '#A70710' }} />
</motion.div>
<div className="w-48 h-1 bg-gray-100 rounded-full overflow-hidden relative">
<motion.div
animate={{ left: ['-100%', '100%'] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
className="absolute top-0 bottom-0 w-full bg-[#A70710]"
/>
</div>
</div>
);
}
const currentSub = historyData?.current_subscription;
const history = historyData?.subscription_history || [];
return (
<div className="min-h-screen bg-[#FAFAFA] text-black">
{/* Glass Header */}
<motion.header
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="sticky top-0 z-[100] bg-white/80 backdrop-blur-xl border-b border-gray-100"
>
<div className="max-w-2xl mx-auto px-4 h-20 flex items-center justify-between">
<button
onClick={() => navigate(-1)}
className="w-10 h-10 flex items-center justify-center rounded-2xl hover:bg-gray-100 transition-colors text-gray-600"
>
<ArrowBackIosNew fontSize="small" />
</button>
<div className="text-center">
<h1 className="text-lg font-black tracking-tight">Subscriptions</h1>
<p className="text-[9px] font-bold text-amber-600 tracking-[0.3em] uppercase">Thirukalyanam Premium</p>
</div>
<div className="w-10 h-10 flex items-center justify-center rounded-2xl bg-amber-50 text-amber-600">
<ReceiptLong size={20} />
</div>
</div>
</motion.header>
{/* Current Plan Title */}
<div className="px-6 pt-8">
<motion.h2
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
className="text-3xl font-bold text-gray-900 flex items-center gap-3"
>
<CrownIcon className="text-amber-500 text-4xl" />
Current Plan
</motion.h2>
<main className="max-w-7xl mx-auto px-4 py-10 pb-32">
{/* Hero Active Section */}
<div className="mb-16">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-[20px] bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white shadow-lg shadow-orange-100">
<WorkspacePremium fontSize="large" />
</div>
<div>
<h2 className="text-xl font-black text-gray-900">Current Status</h2>
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest">Active Plan Details</p>
</div>
</div>
{currentSub && (
<div className="bg-green-50 text-green-600 px-4 py-2 rounded-xl hidden sm:flex items-center gap-2 border border-green-100">
<CheckCircle2 size={16} />
<span className="text-[10px] font-black uppercase tracking-wider">Verified Membership</span>
</div>
)}
</div>
<div className=''>
{/* Subscription Cards */}
<div className="px-2 py-8 pb-32 grid grid-cols-1 md:grid-cols-3 gap-2">
{subscriptions.map((sub, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2 }}
className={`relative rounded-3xl overflow-hidden border border-1 border-gray-200 w-full max-w-md ${
sub.isActive
? 'border-amber-400 bg-gradient-to-br from-amber-50 to-pink-50'
: 'border-pink-200 bg-gradient-to-br from-pink-50 to-rose-50 opacity-90'
}`}
>
{/* Crown Badge */}
<div className={` text-[12px] absolute z-[999] -top-1 left-1/2 -translate-x-1/2 px-8 py-2 rounded-b-3xl font-bold text-white shadow-xl flex items-center gap-2 ${
sub.isActive ? 'bg-gradient-to-r from-amber-500 to-red-600' : 'bg-gradient-to-r from-pink-500 to-rose-600'
}`}>
<CrownIcon className="text-[12px]" />
{sub.plan}
{sub.isActive && <span className="ml-2 text-[12px] bg-green-400 text-black px-3 py-1 rounded-full">ACTIVE</span>}
{currentSub ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<PlanCard sub={currentSub} isActive={true} />
<div className="hidden lg:block lg:col-span-2 bg-gradient-to-br from-gray-900 to-black rounded-[40px] p-10 relative overflow-hidden text-white shadow-2xl">
<div className="absolute top-0 right-0 p-10 opacity-20 scale-150">
<CrownIcon size={120} />
</div>
<div className="pt-14 pb-8 px-8">
{/* Subscription ID */}
<div className="text-center mb-6">
<p className="text-sm text-gray-600">Subscription ID</p>
<p className="text-[18px] font-bold text-gray-900">{sub.id}</p>
</div>
{/* Stats Row */}
<div className="grid grid-cols-2 gap-6 mb-4">
<div className="bg-white/70 backdrop-blur rounded-2xl p-5 text-center ">
<Person className="text-4xl text-amber-600 mx-auto mb-2" />
<p className="text-[18px] font-bold text-gray-900">{sub.profileCount}</p>
<p className="text-sm text-gray-600">Profile Count</p>
</div>
<div className="bg-white/70 backdrop-blur rounded-2xl p-5 text-center ">
<Visibility className="text-4xl text-green-600 mx-auto mb-2" />
<p className="text-[18px] font-bold text-gray-900">{sub.usedCount}</p>
<p className="text-sm text-gray-600">Used Count</p>
</div>
</div>
{/* Details Grid */}
<div className="space-y-2 text-lg">
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<CalendarToday className="text-amber-600" /> Billing Cycle
</span>
<span className="font-bold text-gray-900 text-[14px]">{sub.billingCycle}</span>
</div>
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<CalendarToday className="text-red-600" /> Expire Date
</span>
<span className="font-bold text-red-600 text-[14px]">{sub.expireDate}</span>
</div>
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<AccessTime className="text-green-600" /> Start Date & Time
</span>
<span className="font-bold text-gray-900 text-[14px]">{sub.startDate} | {sub.startTime}</span>
</div>
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<CreditCard className="text-purple-600" /> Payment Method
</span>
<span className="font-bold text-purple-700 text-[14px]">{sub.paymentMethod}</span>
</div>
</div>
{/* Total Amount */}
<div className="mt-8 text-center flex gap-4 items-center justify-center flex-wrap">
<p className="text-lg text-gray-600">Total Plan Amount</p>
<p className="text-[18px] font-bold text-gray-900 bg-white/70 backdrop-blur px-4 py-2 rounded-[10px]">
₹{sub.amount.toLocaleString()}
<div className="relative z-10 h-full flex flex-col justify-center">
<h3 className="text-3xl font-black mb-4 tracking-tight">Premium Benefits Active</h3>
<p className="text-gray-400 mb-8 max-w-md leading-relaxed text-sm">
You are currently enjoying the full suite of Thirukalyanam premium features.
Your account is prioritized in searches and you have direct access to contact details.
</p>
<div className="flex flex-wrap gap-3">
{['Verified Profile', 'Priority Search', 'Direct Contact', 'Chat Enabled'].map((item) => (
<div key={item} className="px-4 py-2 bg-white/10 backdrop-blur-md rounded-xl text-[10px] font-bold tracking-widest uppercase flex items-center gap-2">
<CheckCircle2 size={12} className="text-green-400" /> {item}
</div>
</div>
</motion.div>
))}
</div>
</div>
{/* Final Message */}
{/* <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
className="fixed bottom-0 left-0 right-0 bg-gradient-to-r from-red-600 to-rose-700 text-white py-8 text-center shadow-2xl"
</div>
</div>
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="p-10 rounded-[40px] bg-white border-2 border-dashed border-gray-100 text-center max-w-xl mx-auto"
>
<p className="text-xl font-bold">Your journey to forever is fully powered</p>
<p className="text-green-200 mt-2">Enjoy unlimited matches with your Gold Plan</p>
</motion.div> */}
<div className="w-20 h-20 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-6">
<ShieldCheck size={40} className="text-gray-200" />
</div>
<h3 className="text-lg font-black text-gray-900 mb-2">No Active Membership</h3>
<p className="text-sm text-gray-400 mb-8 max-w-[240px] mx-auto leading-relaxed">
Unlock premium matchmaking features to find your soulmate faster.
</p>
<button
onClick={() => navigate('/subscription-plan')}
className="group relative w-full py-4 bg-gray-900 text-white rounded-2xl font-black flex items-center justify-center gap-2 overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-amber-400 to-orange-500 translate-y-full group-hover:translate-y-0 transition-transform duration-500" />
<span className="relative z-10">UPGRADE NOW</span>
<ArrowRight size={18} className="relative z-10 transition-transform group-hover:translate-x-1" />
</button>
</motion.div>
)}
</div>
{/* Timeline History Section */}
<div className="relative">
<div className="flex items-center gap-4 mb-8">
<div className="w-14 h-14 rounded-[20px] bg-white shadow-xl flex items-center justify-center text-gray-400">
<History fontSize="large" />
</div>
<div>
<h2 className="text-xl font-black text-gray-900">Purchase History</h2>
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest">Your payment journey</p>
</div>
</div>
{history.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{history.map((sub, index) => (
<PlanCard key={sub.id} sub={sub} index={index + 1} />
))}
</div>
) : (
<div className="py-20 text-center">
<div className="inline-block p-6 rounded-full bg-gray-50 mb-4">
<LocalActivity className="text-gray-200" sx={{ fontSize: 40 }} />
</div>
<p className="text-sm font-bold text-gray-300 uppercase tracking-widest">No Previous History</p>
</div>
)}
</div>
</main>
</div>
);
}
+254 -124
View File
@@ -1,64 +1,177 @@
import React, { useState } from 'react';
import { ArrowBackIosNew, Check, Star, AutoAwesome } from '@mui/icons-material';
import { ArrowBackIosNew, Check, Star, AutoAwesome, InfoOutlined } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getSubscriptionPlans, purchaseSubscription } from '../api/subscription.api';
import { toast } from 'react-hot-toast';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress } from '@mui/material';
const loadRazorpayScript = () => {
return new Promise((resolve) => {
if (window.Razorpay) {
resolve(true);
return;
}
const script = document.createElement("script");
script.src = "https://checkout.razorpay.com/v1/checkout.js";
script.onload = () => resolve(true);
script.onerror = () => resolve(false);
document.body.appendChild(script);
});
};
export default function SubscriptionPlan() {
const navigate = useNavigate();
const [isAnnual, setIsAnnual] = useState(true);
const queryClient = useQueryClient();
const [purchaseLoading, setPurchaseLoading] = useState(false);
const [confirmDialog, setConfirmDialog] = useState({ open: false, plan: null });
const plans = [
{
name: "Gold Plan",
monthlyPrice: 1800,
annualPrice: 1299,
color: "from-amber-400 to-yellow-600",
button: "from-amber-500 to-orange-600",
features: [
"Unlimited Profile Views",
"Send Personalized Messages",
"Featured Profile for 30 Days",
"Priority Customer Support",
"Verified Badge",
"Horoscope Matching",
"100+ Daily Matches"
]
},
{
name: "Diamond Plan",
monthlyPrice: 2800,
annualPrice: 1999,
color: "from-purple-500 to-pink-600",
button: "from-purple-600 to-pink-700",
popular: true,
features: [
"Everything in Gold",
"Top of Search Results",
"Profile Highlight in Gold Border",
"Personal Relationship Manager",
"Video Call with Matches",
"Astro Compatibility Report",
"200+ Premium Matches Daily"
]
},
{
name: "Platinum Plan",
monthlyPrice: 4800,
annualPrice: 3499,
color: "from-emerald-500 to-teal-600",
button: "from-emerald-600 to-teal-700",
features: [
"Everything in Diamond",
"Guaranteed 10+ Interests/Month",
"Exclusive VIP Events Invite",
"Profile Boost Every Week",
"Family Member Login Access",
"Lifetime Profile Visibility",
"Dedicated Matchmaking Expert"
]
const { data: plansData, isLoading, error } = useQuery({
queryKey: ['subscriptionPlans'],
queryFn: getSubscriptionPlans,
});
const plans = plansData?.data || [];
const handlePurchase = async (plan) => {
setPurchaseLoading(true);
try {
console.log("Initiating purchase for plan:", plan.id);
const res = await purchaseSubscription(plan.id);
console.log("Purchase Initiation Response:", res);
// Support both res.success (older API) and res.status === 'success' (newer API)
if (res.success || res.status === 'success') {
const data = res.data;
// Ensure Razorpay script is loaded
const isLoaded = await loadRazorpayScript();
if (!isLoaded || !window.Razorpay) {
toast.error("Razorpay SDK failed to load. Please check your internet connection.");
setPurchaseLoading(false);
return;
}
if (!data.order_id || !data.key_id) {
toast.error("Invalid order details received from server.");
setPurchaseLoading(false);
return;
}
const options = {
key: data.key_id,
amount: data.amount,
currency: "INR",
name: "Thirukalyanam",
description: data.plan_name || plan.plan_name,
order_id: data.order_id,
handler: async (response) => {
try {
toast.loading("Verifying payment...", { id: "verify-payment" });
const verifyRes = await purchaseSubscription(plan.id, {
razorpay_payment_id: response.razorpay_payment_id,
razorpay_order_id: response.razorpay_order_id,
razorpay_signature: response.razorpay_signature,
});
if (verifyRes.success || verifyRes.status === 'success') {
toast.success("Subscription activated successfully!", { id: "verify-payment" });
queryClient.invalidateQueries(['subscriptionPlans']);
queryClient.invalidateQueries(['headerDetails']);
queryClient.invalidateQueries(['dashboardDetails']);
} else {
toast.error(verifyRes.message || "Verification failed", { id: "verify-payment" });
}
} catch (err) {
console.error("Verification Error:", err);
toast.error("Verification failed", { id: "verify-payment" });
} finally {
setPurchaseLoading(false);
}
},
prefill: {
name: data.name || "",
email: data.email || "",
contact: data.mobile || data.mobile_number || "",
},
theme: {
color: "#A70710",
},
modal: {
ondismiss: function() {
console.log("Payment modal closed by user");
setPurchaseLoading(false);
}
}
};
console.log("Opening Razorpay with options:", options);
const rzp = new window.Razorpay(options);
rzp.open();
} else {
toast.error(res.message || "Failed to initiate purchase");
setPurchaseLoading(false);
}
} catch (err) {
console.error("Purchase Error:", err);
toast.error(err.response?.data?.message || "Something went wrong during purchase initiation");
setPurchaseLoading(false);
}
};
const getPlanFeatures = (plan) => {
const features = [
{ text: `Profile Views: ${plan.count_of_profile_view === -1 ? 'Unlimited' : plan.count_of_profile_view}`, available: true },
{ text: `Validity: ${plan.plan_validity_days === -1 ? 'Unlimited' : `${plan.plan_validity_days} Days`}`, available: true },
{ text: "View Mobile Number", available: plan.can_view_mobile_number },
{ text: "View Email ID", available: plan.can_view_email },
{ text: "View Raasi", available: plan.can_view_raasi },
{ text: "View Star", available: plan.can_view_star },
{ text: "View Father Name", available: plan.can_view_father_name },
{ text: "View Mother Name", available: plan.can_view_mother_name },
{ text: "View DOB", available: plan.can_view_dob },
{ text: "View TOB", available: plan.can_view_tob },
{ text: "Start Chat", available: plan.can_start_chat },
{ text: "Send Interest", available: plan.can_send_interest },
{ text: "Accept Interest", available: plan.can_accept_interest },
];
return features;
};
const getPlanColors = (index) => {
const colors = [
{ color: "from-amber-400 to-yellow-600", button: "bg-[#A70710]" },
{ color: "from-purple-500 to-pink-600", button: "bg-[#A70710]" },
{ color: "from-emerald-500 to-teal-600", button: "bg-[#A70710]" },
{ color: "from-blue-500 to-indigo-600", button: "bg-[#A70710]" },
];
return colors[index % colors.length];
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<CircularProgress sx={{ color: '#A70710' }} />
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center text-red-500 text-center px-4">
<div>
<p className="text-xl font-bold mb-2">Error loading plans</p>
<p>Please check your connection or try again later.</p>
</div>
</div>
);
}
return (
<div className="min-h-screen mt-4 text-black overflow-hidden relative z-20">
@@ -70,113 +183,130 @@ export default function SubscriptionPlan() {
>
<div className="absolute inset-0 bg-[#034E08] rounded-[15px]" />
<div className="relative flex items-center justify-between px-5 py-8 safe-area-top">
<button onClick={() => navigate(-1)} className="text-white">
<ArrowBackIosNew />
</button>
<h1 className="text-2xl font-bold text-center text-white">Subscription Plan</h1>
<div className="w-12" />
</div>
</motion.header>
{/* Annual / Monthly Toggle */}
<div className="flex justify-center -mt-6 relative z-10">
<motion.div
whileTap={{ scale: 0.95 }}
className="bg-[#A70710] backdrop-blur-xl rounded-full p-2 flex gap-2 shadow-2xl"
// style={{background:"linear-gradient(98.05deg, #FAFBFF 0%, #FCC4FF 100%)"}}
>
<button
onClick={() => setIsAnnual(true)}
className={`px-8 py-4 rounded-full font-bold text-lg transition-all ${isAnnual ? 'bg-white text-purple-900 shadow-lg' : 'text-white/80'}`}
>
ANNUAL PLAN
<span className="block text-xs font-normal mt-1 text-green-300">Save up to 40%</span>
</button>
<button
onClick={() => setIsAnnual(false)}
className={`px-8 py-4 rounded-full font-bold text-lg transition-all ${!isAnnual ? 'bg-white text-purple-900 shadow-lg' : 'text-white/80'}`}
>
MONTHLY PLAN
</button>
</motion.div>
</div>
{/* Pricing Cards */}
<div className="px-5 py-10 max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{plans.map((plan, index) => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{plans.map((plan, index) => {
const planColors = getPlanColors(index);
const features = getPlanFeatures(plan);
const isBuyable = plan.is_can_buyable === 1 && !plan.is_current_plan;
return (
<motion.div
key={index}
key={plan.id}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2 }}
transition={{ delay: index * 0.1 }}
className={`relative backdrop-blur-xl rounded-3xl overflow-hidden border ${
plan.popular ? 'border-yellow-400 shadow-2xl shadow-yellow-500/30 scale-105' : 'border-white/30'
plan.is_current_plan ? 'border-yellow-400 shadow-2xl scale-105' : 'border-white/30'
}`}
style={{background:"linear-gradient(98.05deg, #FAFBFF 0%, #FCC4FF 100%)"}}
style={{ background: "linear-gradient(98.05deg, #FAFBFF 0%, #FCC4FF 100%)" }}
>
{plan.popular && (
{plan.is_current_plan && (
<div className="absolute -top-1 left-1/2 -translate-x-1/2 bg-gradient-to-r from-yellow-400 to-amber-500 text-purple-900 px-8 py-2 rounded-b-2xl font-bold text-sm flex items-center gap-2">
<AutoAwesome className="text-lg" /> MOST POPULAR <AutoAwesome className="text-lg" />
<Star className="text-lg" /> CURRENT PLAN <Star className="text-lg" />
</div>
)}
<div className="mt-6 p-8 text-center">
<h3 className="text-3xl font-bold mb-4">{plan.name}</h3>
<div className="text-[25px] font-bold mb-2">
₹{isAnnual ? plan.annualPrice.toLocaleString() : plan.monthlyPrice.toLocaleString()}
<span className="text-[18px] font-normal text-black"> /{isAnnual ? 'month' : 'month'}</span>
<h3 className="text-2xl font-bold mb-4">{plan.plan_name}</h3>
<div className="text-[32px] font-bold mb-2">
₹{plan.plan_amount}
<span className="text-[18px] font-normal text-gray-600">
/{plan.plan_validity_days === -1 ? 'Lifetime' : `${plan.plan_validity_days} days`}
</span>
</div>
{isAnnual && (
<p className="text-[#00903F] font-bold text-lg mb-6">
Billed annually @ ₹{(plan.annualPrice * 12).toLocaleString()}
</p>
)}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`w-[fit-content] py-4 px-8 rounded-full text-white font-semibold text-[16px] shadow-xl bg-gradient-to-r ${plan.button} hover:shadow-2xl transition-all`}
style={{background:"#A70710"}}
disabled={!isBuyable || purchaseLoading}
whileHover={isBuyable ? { scale: 1.05 } : {}}
whileTap={isBuyable ? { scale: 0.95 } : {}}
onClick={() => setConfirmDialog({ open: true, plan })}
className={`w-full py-4 px-8 rounded-xl text-white font-semibold text-[16px] shadow-xl transition-all ${
isBuyable ? planColors.button : 'bg-gray-400 cursor-not-allowed opacity-70'
}`}
>
Select Plan
{purchaseLoading ? <CircularProgress size={24} color="inherit" /> :
plan.is_current_plan ? "Current Plan" : "Select Plan"}
</motion.button>
<div className="mt-8 text-left space-y-4">
<h4 className="text-xl font-bold text-black">Features</h4>
{plan.features.map((feature, i) => (
<div key={i} className="flex items-center gap-3">
<Check className="text-green-400 text-2xl" />
<span className="text-black">{feature}</span>
<div className="mt-8 text-left space-y-3">
<h4 className="text-lg font-bold text-black border-b border-gray-200 pb-2">Features</h4>
<div className="max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
{features.map((feature, i) => (
<div key={i} className="flex items-center gap-3 py-1">
<div className={`rounded-full p-0.5 ${feature.available ? 'bg-green-100' : 'bg-red-100'}`}>
<Check className={`text-xl ${feature.available ? 'text-green-600' : 'text-red-600'}`}
sx={{ fontSize: 16, visibility: feature.available ? 'visible' : 'hidden' }} />
{!feature.available && <span className="w-4 h-4 flex items-center justify-center text-red-600 font-bold text-xs">✕</span>}
</div>
<span className={`text-sm ${feature.available ? 'text-black' : 'text-gray-400 line-through'}`}>
{feature.text}
</span>
</div>
))}
</div>
</div>
</div>
</motion.div>
))}
);
})}
</div>
{/* Final CTA */}
{/* <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="text-center mt-16"
>
<h2 className="text-3xl font-bold mb-4">Find Your Perfect Life Partner</h2>
<p className="text-xl text-white/80 max-w-2xl mx-auto mb-10">
Choose the plan that’s right for your sacred journey. Every premium member gets closer to their soulmate.
<div className="text-center mt-12 mb-8">
<p className="text-gray-600 max-w-2xl mx-auto italic">
Choose the plan that's right for you. Find your perfect life partner with our premium plans.
</p>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="bg-gradient-to-r from-red-600 to-rose-700 px-12 py-6 rounded-full text-2xl font-bold shadow-2xl hover:shadow-red-500/50 transition-all"
>
Choose This Plan
</motion.button>
</motion.div> */}
</div>
</div>
<div className="h-32" />
{/* Confirmation Dialog */}
<Dialog
open={confirmDialog.open}
onClose={() => setConfirmDialog({ open: false, plan: null })}
PaperProps={{
sx: { borderRadius: '20px', padding: '10px' }
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#1B5E20', fontWeight: 'bold' }}>
<InfoOutlined /> Note
</DialogTitle>
<DialogContent>
<p className="text-[#DF1D46] font-semibold text-center text-lg">
If you choose a new plan, your current plan will expire and the new plan will be activated.
</p>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center', gap: 2, pb: 3 }}>
<Button
onClick={() => setConfirmDialog({ open: false, plan: null })}
variant="outlined"
sx={{ borderRadius: '10px', color: 'gray', borderColor: 'gray' }}
>
Cancel
</Button>
<Button
onClick={() => {
const plan = confirmDialog.plan;
setConfirmDialog({ open: false, plan: null });
handlePurchase(plan);
}}
variant="contained"
sx={{ borderRadius: '10px', bgcolor: '#A70710', '&:hover': { bgcolor: '#8e060d' } }}
>
Ok
</Button>
</DialogActions>
</Dialog>
<div className="h-20" />
</div>
);
}
+43 -28
View File
@@ -11,12 +11,15 @@ import weddingpromo2 from "../assets/images/weddingpromo2.jpg";
import weddingpromo3 from "../assets/images/weddingpromo3.jpg";
import weddingpromo4 from "../assets/images/weddingpromo4.jpg";
import { useDashboardQuery } from "../hooks/useDashboardQuery";
const NewJoinedProfile = lazy(() => import("../components/profiledashboard/NewJoinedProfile"));
const ProfileCompletion = lazy(() => import("../components/profiledashboard/ProfileCompletion"));
const MatrimonyArticles = lazy(() => import("../components/profiledashboard/MatrimonyArticles"));
const MatchingList = lazy(() => import("../components/profiledashboard/MatchingList"));
const VideoSwiperGallery = lazy(() => import("../components/profiledashboard/VideoSwiperGallery"));
const Profilecardemo = lazy(() => import("../components/ui/ProfileCardDemo"));
const DailyRecommendedCard = lazy(() => import("../components/profiledashboard/DailyRecommendedCard"));
const images = [
@@ -48,6 +51,15 @@ const SectionFallback = ({ height = 280 }) => (
const UserDashboardHome = () => {
const { data, isLoading } = useDashboardQuery();
const dashboardData = data?.data;
const sliderImages = dashboardData?.image_sliders?.length > 0
? dashboardData.image_sliders
: images.map((img, index) => ({ id: index, image: img }));
if (isLoading) return <SectionFallback height={600} />;
return (
<>
<div className="custom-swiper-hero flex items-center justify-center p-4">
@@ -92,11 +104,11 @@ const UserDashboardHome = () => {
},
}}
>
{images.map((img, idx) => (
<SwiperSlide key={idx}>
{sliderImages.map((slide, idx) => (
<SwiperSlide key={slide.id || idx}>
<div className="relative overflow-hidden rounded-3xl w-[100%]">
<img
src={img}
src={slide.image}
alt={`Slide ${idx + 1}`}
className="w-full h-full object-cover"
loading={idx === 0 ? "eager" : "lazy"}
@@ -120,13 +132,13 @@ const UserDashboardHome = () => {
<style>{`
.custom-swiper-hero .swiper {
.custom-swiper-hero .swiper {
width: 100%;
margin: 0 auto;
}
}
/* ================= MOBILE (≤768px) ================= */
@media (max-width: 768px) {
/* ================= MOBILE (≤768px) ================= */
@media (max-width: 768px) {
.custom-swiper-hero .swiper {
overflow: hidden;
}
@@ -144,10 +156,10 @@ const UserDashboardHome = () => {
transform: scale(1);
opacity: 1;
}
}
}
/* ================= TABLET & DESKTOP (≥769px) ================= */
@media (min-width: 769px) {
/* ================= TABLET & DESKTOP (≥769px) ================= */
@media (min-width: 769px) {
.custom-swiper-hero .swiper {
overflow: visible;
}
@@ -171,19 +183,19 @@ const UserDashboardHome = () => {
opacity: 1;
z-index: 2;
}
}
}
/* ================= COMMON ================= */
.custom-swiper-hero .swiper-slide > div {
/* ================= COMMON ================= */
.custom-swiper-hero .swiper-slide > div {
width: 100%;
height: 100%;
}
}
.custom-swiper-hero .swiper-slide img {
.custom-swiper-hero .swiper-slide img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
@@ -206,28 +218,31 @@ const UserDashboardHome = () => {
`}</style>
</div>
<Suspense fallback={<SectionFallback height={320} />}>
<Profilecardemo />
<DailyRecommendedCard profiles={dashboardData?.daily_recommended} />
</Suspense>
{/* <DailyRecommendedCard/> */}
<Suspense fallback={<SectionFallback height={220} />}>
<ProfileCompletion />
<ProfileCompletion
percentage={dashboardData?.profile_complete_percentage}
missingDetails={dashboardData?.non_filled_sections}
becomePaidMember={dashboardData?.become_paid_member}
/>
</Suspense>
{/* <DailyRecommendedCard/> */}
<Suspense fallback={<SectionFallback height={280} />}>
<MatrimonyArticles />
<MatrimonyArticles articles={dashboardData?.blogs} />
</Suspense>
<Suspense fallback={<SectionFallback height={320} />}>
<MatchingList />
<MatchingList matches={dashboardData?.all_matches} />
</Suspense>
<Suspense fallback={<SectionFallback height={320} />}>
<NewJoinedProfile profiles={dashboardData?.new_joined} />
</Suspense>
{/* <PaidMemberCard/> */}
{/* <NewJoinedProfile/> */}
{/* <CustomerSupportCard/> */}
<Suspense fallback={<SectionFallback height={240} />}>
<VideoSwiperGallery />
<VideoSwiperGallery videos={dashboardData?.youtube_videos} />
</Suspense>
</>
)
+7
View File
@@ -74,6 +74,13 @@ const LoginPage = () => {
localStorage.setItem("access_token", token);
setAccessToken(token);
// Store profile_id and user_id for WebSocket channels
const profileId = data?.profile_id || data?.data?.profile_id;
const userId = data?.user_id || data?.data?.user_id;
if (profileId) localStorage.setItem("profile_id", profileId);
if (userId) localStorage.setItem("user_id", userId);
toast.success("Login Successful!");
navigate("/dashboard-home");
} else {
+40 -96
View File
@@ -1,29 +1,28 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
age: [18, 70],
height: [4.0, 7.11],
maritalStatus: "All Profile",
motherTongue: [],
religion: "Hindu",
matchesWithHoroscope: false,
from_age: 18,
to_age: 70,
from_height: 4.0,
to_height: 7.11,
marital_status: [],
religion: [],
caste: [],
subCaste: [],
star: "Any",
dasham: "Doesn't matter",
occupation: "Any",
annualIncome: "Any",
employeeType: "Any",
education: "Any",
state: "Any",
country: "Any",
citizenship: "Any",
eatingHabits: "Any",
smokingHabits: "Doesn't matter",
drinkingHabits: "Doesn't matter",
familyType: "Any",
familyStatus: "Any",
familyValue: "Any",
sub_caste: [],
star: [],
occupation: [],
annual_income: [],
employee_type: [],
education: [],
state: [],
district: [],
diet: "",
family_type: [],
filter_type: "",
page: 1,
isPaidMember: false,
search: "",
};
const filterSlice = createSlice({
@@ -31,73 +30,24 @@ const filterSlice = createSlice({
initialState,
reducers: {
setAge: (state, action) => {
state.age = action.payload;
state.from_age = action.payload[0];
state.to_age = action.payload[1];
},
setHeight: (state, action) => {
state.height = action.payload;
state.from_height = action.payload[0];
state.to_height = action.payload[1];
},
setMaritalStatus: (state, action) => {
state.maritalStatus = action.payload;
},
setMotherTongue: (state, action) => {
state.motherTongue = action.payload;
state.marital_status = Array.isArray(action.payload) ? action.payload : [action.payload];
},
setReligion: (state, action) => {
state.religion = action.payload;
},
setMatchesWithHoroscope: (state, action) => {
state.matchesWithHoroscope = action.payload;
state.religion = Array.isArray(action.payload) ? action.payload : [action.payload];
},
setCaste: (state, action) => {
state.caste = action.payload;
},
setSubCaste: (state, action) => {
state.subCaste = action.payload;
},
setStar: (state, action) => {
state.star = action.payload;
},
setDasham: (state, action) => {
state.dasham = action.payload;
},
setOccupation: (state, action) => {
state.occupation = action.payload;
},
setAnnualIncome: (state, action) => {
state.annualIncome = action.payload;
},
setEmployeeType: (state, action) => {
state.employeeType = action.payload;
},
setEducation: (state, action) => {
state.education = action.payload;
},
setState: (state, action) => {
state.state = action.payload;
},
setCountry: (state, action) => {
state.country = action.payload;
},
setCitizenship: (state, action) => {
state.citizenship = action.payload;
},
setEatingHabits: (state, action) => {
state.eatingHabits = action.payload;
},
setSmokingHabits: (state, action) => {
state.smokingHabits = action.payload;
},
setDrinkingHabits: (state, action) => {
state.drinkingHabits = action.payload;
},
setFamilyType: (state, action) => {
state.familyType = action.payload;
},
setFamilyStatus: (state, action) => {
state.familyStatus = action.payload;
},
setFamilyValue: (state, action) => {
state.familyValue = action.payload;
state.sub_caste = action.payload;
},
// universal update
@@ -105,7 +55,16 @@ const filterSlice = createSlice({
return { ...state, ...action.payload };
},
resetFilters: () => initialState,
resetFilters: (state) => {
// Reset all filters but preserve the paid member status
return { ...initialState, isPaidMember: state.isPaidMember };
},
setPage: (state, action) => {
state.page = action.payload;
},
setPaidMemberStatus: (state, action) => {
state.isPaidMember = action.payload;
},
},
});
@@ -113,28 +72,13 @@ export const {
setAge,
setHeight,
setMaritalStatus,
setMotherTongue,
setReligion,
setMatchesWithHoroscope,
setCaste,
setSubCaste,
setStar,
setDasham,
setOccupation,
setAnnualIncome,
setEmployeeType,
setEducation,
setState,
setCountry,
setCitizenship,
setEatingHabits,
setSmokingHabits,
setDrinkingHabits,
setFamilyType,
setFamilyStatus,
setFamilyValue,
updateFilter,
resetFilters,
setPage,
setPaidMemberStatus,
} = filterSlice.actions;
export default filterSlice.reducer;
+126 -41
View File
@@ -5,53 +5,84 @@ const registrationformSlice = createSlice({
initialState: {
personalDetails: {
name: "",
mobileNumber: "",
mobile: "",
email: "",
gender: "",
dob: "",
height: "",
weight: "",
maritalStatus: "",
religion: "",
profileFor: "",
caste: "",
subCaste: "",
marital_status: "",
religion: 1, // Default Hindu
profile_for: "",
caste: 1, // Default Naidu
sub_caste: "",
willing_to_marry: "",
inter_caste_parents: "",
inter_caste_parents_details: "",
gothram: "",
raasi: "",
star: "",
bloodGroup: "",
email: "",
do_you_speak_telugu: "",
about_us: "",
known_languages: [],
mother_language: "",
complexion: "",
physical_status: "",
password: "",
confirmPassword: "",
dob: "",
raasi: "",
star: "",
state: "",
city: "",
pincode: "",
profiles: [],
verifiedMobileNumber: "",
},
educationalDetails: {
collegeName: "",
employeeType: "",
qualification: "",
fieldOfStudy: "",
study_field: "",
education: "",
education_detail: "",
college_name: "",
employee_type: "",
occupation: "",
organization: "",
income: "",
workLocation: "",
occupation_detail: "",
company_name: "",
income_currency: "INR",
annual_income: "",
work_country: 1,
work_city: "",
work_state: "",
work_district: "",
address: "",
},
familyDetails: {
fatherName: "",
fatherOccupation: "",
motherName: "",
motherOccupation: "",
brotherCount: 0,
sisterCount: 0,
brotherCount: "",
sisterCount: "",
brothers: [],
sisters: [],
familyStatus: "",
nativePlace: "",
familyCountry: "",
familyState: "",
familyDistrict: "",
familyCity: "",
address: "",
expectationDetails: "",
willingToGoAbroad: "",
},
lifestyleDetails: {
diets: [],
hobbies: [],
dayOfBirth: "",
raasi: "",
star: "",
patham: "",
lagnam: "",
panjangam_type: "",
dasa_balance: "",
dasa_years: "",
dasa_months: "",
dasa_days: "",
dob: "",
tob: "",
placeOfBirth: "",
@@ -85,13 +116,22 @@ const registrationformSlice = createSlice({
},
},
partnerPreferences: {
ageRange: "",
age_from: "",
age_to: "",
height_from: "",
height_to: "",
marital_statuses: [],
birth_stars: [],
castes: [],
subCastes: [],
occupations: [],
sub_castes: [],
educations: [],
hobbies: [],
annualIncome: "",
occupations: [],
employee_types: [],
currencies: [],
inr_from: "",
inr_to: "",
usd_from: "",
usd_to: "",
states: [],
districts: [],
},
@@ -114,14 +154,43 @@ const registrationformSlice = createSlice({
state.partnerPreferences = { ...state.partnerPreferences, ...action.payload };
},
submitForm: (state) => {
console.log("Form Submitted:", {
personalDetails: state.personalDetails,
educationalDetails: state.educationalDetails,
familyDetails: state.familyDetails,
lifestyleDetails: state.lifestyleDetails,
partnerPreferences: state.partnerPreferences,
});
clearAllStepsFrom: (state, action) => {
const step = action.payload;
if (step <= 2) {
state.educationalDetails = {
study_field: "", education: "", education_detail: "", college_name: "",
employee_type: "", occupation: "", occupation_detail: "", company_name: "",
income_currency: "INR", annual_income: "", work_country: "", work_state: "",
work_district: "", work_city: "", address: "",
};
}
if (step <= 3) {
state.familyDetails = {
fatherName: "", fatherOccupation: "", motherName: "", motherOccupation: "",
brotherCount: "", sisterCount: "", brothers: [], sisters: [],
familyStatus: "", nativePlace: "", familyCountry: "", familyState: "",
familyDistrict: "", familyCity: "", address: "", expectationDetails: "",
willingToGoAbroad: "",
};
}
if (step <= 4) {
state.lifestyleDetails = {
dayOfBirth: "", raasi: "", star: "", patham: "", lagnam: "", panjangam_type: "",
dasa_balance: "", dasa_years: "", dasa_months: "", dasa_days: "",
dob: "", 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: [] },
};
}
if (step <= 5) {
state.partnerPreferences = {
age_from: "", age_to: "", height_from: "", height_to: "",
marital_statuses: [], birth_stars: [], castes: [], sub_castes: [],
educations: [], occupations: [], employee_types: [],
currencies: [], inr_from: "", inr_to: "", usd_from: "", usd_to: "",
states: [], districts: [],
};
}
},
@@ -219,8 +288,16 @@ preloadDummyProfile: (state) => {
};
state.lifestyleDetails = {
...state.lifestyleDetails,
diets: [1],
hobbies: [1, 3],
dayOfBirth: "Monday",
raasi: 1,
star: 1,
patham: "1",
lagnam: 1,
panjangam_type: "Thirukanitham",
dasa_balance: "SURIYAN",
dasa_years: "5",
dasa_months: "2",
dasa_days: "10",
dob: "1995-05-01",
tob: "09:30",
placeOfBirth: "Chennai",
@@ -255,13 +332,20 @@ preloadDummyProfile: (state) => {
};
state.partnerPreferences = {
...state.partnerPreferences,
ageRange: 2,
age_from: 25,
age_to: 30,
height_from: 1,
height_to: 10,
marital_statuses: [11],
birth_stars: [1, 2],
castes: [1],
subCastes: [1],
sub_castes: [1],
occupations: [57],
educations: [14],
hobbies: [1, 3],
annualIncome: 1,
employee_types: [3],
currencies: ["INR"],
inr_from: "100000",
inr_to: "120000",
states: [31],
districts: [1],
};
@@ -279,6 +363,7 @@ export const {
updateFamilyDetails,
updateLifestyleDetails,
updatePartnerPreferences,
clearAllStepsFrom,
submitForm,
preloadDummyProfile,
} = registrationformSlice.actions;
+31 -2
View File
@@ -1,9 +1,38 @@
import { configureStore } from "@reduxjs/toolkit";
import registerformReducer from "./registrationFormSlice";
import filtersReducer from "./filterSlice";
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import storage from "redux-persist/lib/storage"; // defaults to localStorage for web
const persistConfig = {
key: "filters",
storage,
};
const persistedFiltersReducer = persistReducer(persistConfig, filtersReducer);
export const store = configureStore({
reducer: {
registerform:registerformReducer,
filters:filtersReducer,
registerform: registerformReducer,
filters: persistedFiltersReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, "registerform/updatePersonalDetails"],
ignoredActionPaths: ["payload.profiles", "payload.file"],
ignoredPaths: ["registerform.personalDetails.profiles"],
},
}),
});
export const persistor = persistStore(store);
+2 -10
View File
@@ -3,18 +3,10 @@ import { Route, Routes } from 'react-router-dom';
import UserRoutes from './UserRoutes';
import PublicRoutes from './PublicRoutes';
import ScrollToTop from '../components/common/ScrollToTop';
import Skeleton from "../components/common/Skeleton";
import { SkeletonPage } 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>
<SkeletonPage />
);
const AppRoutes = () => {
+3 -2
View File
@@ -19,9 +19,10 @@ const PublicRoutes = () => {
<>
<Route element={<HomeLayout />}>
<Route path="/" element={<HomePage />} />
<Route element={<PublicGuard />}>
{/* <Route element={<PublicGuard />}>
</Route> */}
<Route path="/registration" element={<StepperForm />} />
</Route>
</Route>
<Route element={<PublicGuard />}>
+3
View File
@@ -55,8 +55,11 @@ const UserRoutes = () => {
<Route element={<ProfileLayout />}>
<Route path="/chat" element={<ChatUI />} />
<Route path="/chat/:chatId" element={<ChatUI />} />
</Route>
<Route element={<ProfileLayout />}>
<Route path="/horoscoper-generate" element={<HoroscopeGenerator />} />
</Route>
+76
View File
@@ -0,0 +1,76 @@
import axiosInstance from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
export const getChatList = async (searchValue = "") => {
try {
// Add timestamp to prevent caching
const response = await axiosInstance.get(`${API_ENDPOINTS.CHAT_LIST}?search_value=${searchValue}&_t=${Date.now()}`);
return response.data;
} catch (error) {
console.error("Error fetching chat list:", error);
throw error;
}
};
export const getChatMessages = async (chatId, page = 1) => {
try {
const response = await axiosInstance.get(`${API_ENDPOINTS.CHAT_MESSAGES(chatId)}?page=${page}`);
return response.data;
} catch (error) {
console.error("Error fetching chat messages:", error);
throw error;
}
};
export const sendMessage = async (chatId, message) => {
try {
// Correct endpoint based on user request: chat/message/send?chat_id={id}&message={text}
const response = await axiosInstance.post(`chat/message/send?chat_id=${chatId}&message=${encodeURIComponent(message)}`);
return response.data;
} catch (error) {
console.error("Error sending message:", error);
throw error;
}
};
export const getCallHistoryMyList = async () => {
try {
const response = await axiosInstance.get(`call_my_list`);
return response.data;
} catch (error) {
console.error("Error fetching my call history:", error);
throw error;
}
};
export const getCallHistoryOthersList = async () => {
try {
const response = await axiosInstance.get(`call_others_list`);
return response.data;
} catch (error) {
console.error("Error fetching others call history:", error);
throw error;
}
};
export const deleteCallHistory = async (ids) => {
try {
const queryStr = ids.map((id, index) => `ids[${index}]=${id}`).join('&');
const response = await axiosInstance.get(`call_list_delete?${queryStr}`);
return response.data;
} catch (error) {
console.error("Error deleting call history:", error);
throw error;
}
};
export const deleteChats = async (ids) => {
try {
const queryStr = ids.map((id, index) => `ids[${index}]=${id}`).join('&');
const response = await axiosInstance.post(`chat/delete?${queryStr}`);
return response.data;
} catch (error) {
console.error("Error deleting chats:", error);
throw error;
}
};
+78
View File
@@ -0,0 +1,78 @@
import axiosInstance from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
export const getBlockedProfiles = async () => {
try {
const response = await axiosInstance.get(API_ENDPOINTS.BLOCK_PROFILE_LIST);
return response.data;
} catch (error) {
console.error("Error fetching blocked profiles:", error);
throw error;
}
};
export const getReportedProfiles = async () => {
try {
const response = await axiosInstance.get(API_ENDPOINTS.REPORT_PROFILE_LIST);
return response.data;
} catch (error) {
console.error("Error fetching reported profiles:", error);
throw error;
}
};
export const unblockProfile = async (profileId) => {
try {
const response = await axiosInstance.post(`${API_ENDPOINTS.BLOCK_PROFILE}?profile_id=${profileId}`);
return response.data;
} catch (error) {
console.error("Error unblocking profile:", error);
throw error;
}
};
export const getProfileDetail = async (profile_id) => {
try {
const response = await axiosInstance.get(`${API_ENDPOINTS.PROFILE_DETAIL}?profile_id=${profile_id}`);
return response.data;
} catch (error) {
console.error("Error fetching profile detail:", error);
throw error;
}
};
export const getInterestList = async (tab, type) => {
try {
const response = await axiosInstance.get(`${API_ENDPOINTS.INTEREST_LIST}?tab=${tab}&type=${type}`);
return response.data;
} catch (error) {
console.error("Error fetching interest list:", error);
throw error;
}
};
export const updateInterestStatus = async (id, status) => {
try {
const response = await axiosInstance.post(`${API_ENDPOINTS.UPDATE_INTEREST_STATUS}?id=${id}&status=${status}`);
return response.data;
} catch (error) {
console.error("Error updating interest status:", error);
throw error;
}
};
export const reportProfile = async (profileId, reason) => {
const response = await axiosInstance.post(`${API_ENDPOINTS.REPORT_PROFILE}?profile_id=${profileId}&reason=${reason}`);
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to report");
}
return response.data;
};
export const blockProfile = async (profileId, reason) => {
const response = await axiosInstance.post(`${API_ENDPOINTS.BLOCK_PROFILE}?profile_id=${profileId}&reason=${reason}`);
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to block");
}
return response.data;
};
+44
View File
@@ -0,0 +1,44 @@
import axiosInstance, { apiForFiles } from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
export const shortlistProfile = async (profileId) => {
try {
const formData = new FormData();
formData.append("profile_id", profileId);
const response = await apiForFiles.post(API_ENDPOINTS.SHORTLIST_API, formData);
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to shortlist");
}
return response.data;
} catch (error) {
const message = error.response?.data?.message || error.message || "Failed to shortlist";
throw new Error(message);
}
};
export const sendInterest = async (profileId) => {
try {
const formData = new FormData();
formData.append("profile_id", profileId);
// Using apiForFiles which is pre-configured for multipart/form-data
// This is more robust for PHP backends expecting form data
const response = await apiForFiles.post(API_ENDPOINTS.INTEREST_SEND, formData);
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to send interest");
}
return response.data;
} catch (error) {
const message = error.response?.data?.message || error.message || "Failed to send interest";
throw new Error(message);
}
};
export const declineProfile = async (profileId) => {
const response = await axiosInstance.post(`decline?profile_id=${profileId}`);
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to decline profile");
}
return response.data;
};
+1 -1
View File
@@ -53,7 +53,7 @@ class ErrorBoundary extends Component {
</p>
{/* Show error details in development */}
{process.env.NODE_ENV === "development" && this.state.error && (
{import.meta.env.DEV && this.state.error && (
<details className="mb-4 text-sm text-gray-500">
<summary className="cursor-pointer font-medium">
Error Details (Development)
+15
View File
@@ -1,7 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from "path"
import { fileURLToPath } from "url"
import tailwindcss from '@tailwindcss/vite'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(),],
@@ -10,4 +15,14 @@ export default defineConfig({
"@": path.resolve(__dirname, "src"),
},
},
server: {
proxy: {
'/backend': {
target: 'https://www.thirukalyanam.amrithaa.net',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/backend/, '/backend') // keep /backend
}
}
}
})