Compare commits

...

10 Commits

64 changed files with 6096 additions and 5935 deletions

9
.gitignore vendored
View File

@ -22,3 +22,12 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Environment variables
.env
.env.local
.env.*.local
# Build artifacts
dist.zip

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 ## Features
- [@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 - 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
dist.zip

Binary file not shown.

88
package-lock.json generated
View File

@ -31,6 +31,9 @@
"react-lazy-load-image-component": "^1.6.3", "react-lazy-load-image-component": "^1.6.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"react-select": "^5.10.2",
"react-window": "^2.2.7",
"redux-persist": "^6.0.0",
"swiper": "^12.0.3", "swiper": "^12.0.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
}, },
@ -1719,6 +1722,31 @@
"integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==",
"license": "Apache-2.0" "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": { "node_modules/@grpc/grpc-js": {
"version": "1.9.15", "version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
@ -7139,6 +7167,12 @@
"node": ">= 0.4" "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": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -8053,6 +8087,27 @@
"react-dom": ">=18" "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": { "node_modules/react-smooth": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", "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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -8205,6 +8270,15 @@
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "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": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
@ -9069,6 +9143,20 @@
"punycode": "^2.1.0" "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": { "node_modules/use-sync-external-store": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",

View File

@ -33,6 +33,9 @@
"react-lazy-load-image-component": "^1.6.3", "react-lazy-load-image-component": "^1.6.3",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"react-select": "^5.10.2",
"react-window": "^2.2.7",
"redux-persist": "^6.0.0",
"swiper": "^12.0.3", "swiper": "^12.0.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
}, },

View File

@ -1,57 +1,71 @@
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
LOGOUT: "logout", LOGOUT: "logout",
TERMS_AND_POLICIES_PRIVACY:"terms-and-policies", TERMS_AND_POLICIES_PRIVACY: "terms-and-policies",
// registration api's // registration api's
PERSONAL_DETAILS_MASTER :"personal_details_masters", PERSONAL_DETAILS_MASTER: "personal_details_masters",
CASTE_MASTER : "get_caste_masters", CASTE_MASTER: "get_caste_masters",
SUB_CASTE_MASTER : "get_sub_caste_masters", SUB_CASTE_MASTER: "get_sub_caste_masters",
CITY_MASTER : "get_district_masters", CITY_MASTER: "get_district_masters",
STAR_MASTER : "get_star_masters", STAR_MASTER: "get_star_masters",
MOBILE_SEND_OTP: "send_otp", MOBILE_SEND_OTP: "send_otp",
MOBILE_VERIFY_OTP: "verify_otp", MOBILE_VERIFY_OTP: "verify_otp",
EDUCATION_DETAILS_MASTER: "educational_details_masters", EDUCATION_DETAILS_MASTER: "educational_details_masters",
EDUCATION_LIST_API:"get_education", 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_STEP1: "register", // register api
REGISTER_STEP2:"update_educational_details", // educational details updated api REGISTER_STEP2: "update_educational_details", // educational details updated api
REGSITER_STEP3:"update_family_details", // family details updated api REGSITER_STEP3: "update_family_details", // family details updated api
REGISTER_STEP4:"update_lifestyle_details", // lifestyle details updated api REGISTER_STEP4: "update_lifestyle_details", // lifestyle details updated api
REGISTER_STEP5:"update_preferred_details", // partner preference details updated api REGISTER_STEP5: "update_preferred_details", // partner preference details updated api
PREVIEW_DETAILS: "get_preview_details", PREVIEW_DETAILS: "get_preview_details",
REVIEWS: "reviews", REVIEWS: "reviews",
// edit api's autopapulated // edit api's autopapulated
EDIT_PERSONAL_DETAILS: "get_personal_details", EDIT_PERSONAL_DETAILS: "get_personal_details",
EDIT_EDUCATION_DETAILS: "get_educational_details", EDIT_EDUCATION_DETAILS: "get_educational_details",
EDIT_FAMILY_DETAILS: "get_family_details", EDIT_FAMILY_DETAILS: "get_family_details",
EDIT_LIFESTYLE_DETAILS: "get_lifestyle_details", EDIT_LIFESTYLE_DETAILS: "get_lifestyle_details",
EDIT_PREFERED_PARTNER_DETAILS: "get_preferred_details", EDIT_PREFERED_PARTNER_DETAILS: "get_preferred_details",
// delete api // delete api
DELETE_ACCOUNT: "delete_account", DELETE_ACCOUNT: "delete_account",
PHONE_NUMBER_VISIBILITY: "get_phone_number_visibility", PHONE_NUMBER_VISIBILITY: "get_phone_number_visibility",
UPDATE_PHONE_NUMBER_VISIBILITY: "update_phone_number_visibility", UPDATE_PHONE_NUMBER_VISIBILITY: "update_phone_number_visibility",
CHAT_ALERT_NOTIFICATION:"get_chat_alert_notification", CHAT_ALERT_NOTIFICATION: "get_chat_alert_notification",
UPDATE_CHAT_ALERT_NOTIFICATION:"update_chat_alert_notification", UPDATE_CHAT_ALERT_NOTIFICATION: "update_chat_alert_notification",
PROFILE_PROTECT_API:"get_profile_protection", PROFILE_PROTECT_API: "get_profile_protection",
UPDATE_PROFILE_PROTECT_API:"update_profile_protection", UPDATE_PROFILE_PROTECT_API: "update_profile_protection",
MATCH_ALERT:"get_match_alert", MATCH_ALERT: "get_match_alert",
UPDATE_MATCH_ALERT:"update_match_alert", UPDATE_MATCH_ALERT: "update_match_alert",
WHO_CAN_VIEW_MESSAGE:"get_who_can_message_me", WHO_CAN_VIEW_MESSAGE: "get_who_can_message_me",
UPDATE_WHO_CAN_VIEW_MESSAGE:"update_who_can_message_me", UPDATE_WHO_CAN_VIEW_MESSAGE: "update_who_can_message_me",
CONTACT_US:"get_contact_us", CONTACT_US: "get_contact_us",
BE_SAFE_ONLINE:"get_be_safe_online", BE_SAFE_ONLINE: "get_be_safe_online",
NOTIFICATION_LIST:"notification/lists", NOTIFICATION_LIST: "notification/lists",
NOTIFICATION_COUNT:"notification/un_read_count", 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: "update_interest_status",
CHAT_LIST: "chat/lists",
CHAT_MESSAGES: (id) => `chat/${id}/messages`,
UNREAD_CHAT_COUNT: "chat/un_read_chat_count",
}; };

7
src/api/dashboard.api.js Normal file
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;
};

View File

@ -63,3 +63,17 @@ export const getPartnerPreferenceMasters = async () => {
); );
return res.data; 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;
};

View File

@ -5,3 +5,9 @@ export const getPreviewDetails = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.PREVIEW_DETAILS); const res = await axiosInstance.get(API_ENDPOINTS.PREVIEW_DETAILS);
return res.data; return res.data;
}; };
export const getHeaderDetails = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.HEADER_API);
return res.data;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

View File

@ -242,7 +242,7 @@ const navigate = useNavigate();
> >
<div onClick={(e) => e.stopPropagation()} className="bg-white rounded-2xl shadow-xl overflow-hidden select-none"> <div onClick={(e) => e.stopPropagation()} className="bg-white rounded-2xl shadow-xl overflow-hidden select-none">
<div className="relative"> <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 <img
src={profile.image} src={profile.image}

View File

@ -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>
);
}

View File

@ -0,0 +1,142 @@
import React, { useState } from "react";
import { Crown, Bookmark, Receipt, Sparkles, MoonStar, IdCard } from "lucide-react";
import CakeIcon from "@mui/icons-material/Cake";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { motion } from "framer-motion";
import { useNavigate } from "react-router-dom";
export default function ProfileCardUI({ profile }) {
const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
// 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 (
<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={(e) => {
e.stopPropagation();
// Shortlist logic here
}}
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 text-gray-600" />
<span className="text-[12px] font-medium text-gray-700">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-4 pt-2 pb-4 flex flex-col gap-3 bg-white">
<div className="flex items-center gap-2 text-gray-600">
<VisibilityIcon sx={{ fontSize: 18 }} />
<span className="text-[13px] font-medium">Last seen: {profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"}</span>
</div>
<div className="grid grid-cols-2 gap-y-2 gap-x-4">
<div className="flex items-center gap-2">
<CakeIcon sx={{ fontSize: 18, color: "#374151" }} />
<span className="text-[14px] font-semibold text-gray-900">{profile.age ? `${profile.age} yrs` : "-"}</span>
</div>
<div className="flex items-center gap-2">
<AccessibilityNewIcon sx={{ fontSize: 18, color: "#374151" }} />
<span className="text-[14px] font-semibold text-gray-900">{profile.height ? `${profile.height} cm` : "-"}</span>
</div>
<div className="flex items-center gap-2 col-span-2">
<Receipt className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900 truncate">{profile.annual_income_name || "N/A"}</span>
</div>
</div>
<div className="flex items-center gap-4 text-gray-700">
<div className="flex items-center gap-1.5" title="Raasi">
<MoonStar className="w-4 h-4" />
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.raasi_name || "-"}</span>
</div>
<div className="flex items-center gap-1.5" title="Star">
<Sparkles className="w-4 h-4" />
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.star_name || "-"}</span>
</div>
<div className="flex items-center gap-1.5" title="Caste">
<IdCard className="w-4 h-4" />
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.caste_name || "-"}</span>
</div>
</div>
<div className="flex items-center gap-2">
<LocationOnIcon sx={{ fontSize: 18, color: "#DC2626" }} />
<span className="text-[14px] font-semibold text-gray-900 truncate">
{profile.district_name || profile.location || "-"}
{profile.state_name ? `, ${profile.state_name}` : ""}
</span>
</div>
<div className="flex gap-3 mt-2">
<button
onClick={(e) => { e.stopPropagation(); }}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-red-50 border border-red-200 text-red-700 rounded-full font-medium text-sm hover:bg-red-100 transition-colors active: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={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-full font-medium text-sm border transition-colors active:scale-95 ${isLiked ? "bg-green-100 border-green-300 text-green-700" : "bg-green-50 border-green-200 text-green-700 hover:bg-green-100"}`}
onClick={(e) =>{ e.stopPropagation(); setIsLiked(!isLiked); }}
>
{isLiked ? (
<>
<svg className="w-4 h-4 text-red-500 fill-current" viewBox="0 0 24 24"><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"/></svg>
Sent
</>
) : (
<>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/></svg>
Interest
</>
)}
</button>
</div>
</div>
</div>
);
}

View File

@ -4,6 +4,7 @@ import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import SwipeableDrawer from "@mui/material/SwipeableDrawer"; import SwipeableDrawer from "@mui/material/SwipeableDrawer";
import { useWebSocket } from "../../hooks/useWebSocket";
import List from "@mui/material/List"; import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from "@mui/material/ListItemButton";
@ -19,7 +20,7 @@ import Button from "@mui/material/Button";
import LazyImage from "./LazyImage"; import LazyImage from "./LazyImage";
import Logo from "../../assets/images/logo.png"; import Logo from "../../assets/images/logo.png";
import { useLocation, useNavigate } from "react-router-dom"; 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 { useTheme, useMediaQuery, ListItemIcon } from "@mui/material";
import { Home, Users, Heart, MessageCircle, Search, Bell } from "lucide-react"; import { Home, Users, Heart, MessageCircle, Search, Bell } from "lucide-react";
import { isAuthenticated } from "../../utills/auth"; import { isAuthenticated } from "../../utills/auth";
@ -27,13 +28,15 @@ import userimg from "../../assets/images/bride1.jpg"
import axiosInstance, { logoutAPI } from "../../api/axiosInstance"; import axiosInstance, { logoutAPI } from "../../api/axiosInstance";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { API_ENDPOINTS } from "../../api/apiEndpoints"; 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 = [ const NAV_LINKS = [
// { label: "Home", path: "/" }, // { label: "Home", path: "/" },
{ label: "Matches", path: "/matches" }, { label: "Matches", path: "/matches" },
// { label: "ProfileCard", path: "/profile-card" }, // { label: "ProfileCard", path: "/profile-card" },
{ label: "Interest", path: "/interest" }, { label: "Interest", path: "/interest" },
{ label: "Horoscope", path: "/horoscoper-generate" }, // { label: "Horoscope", path: "/horoscoper-generate" },
{ label: "Messages", path: "/chat" }, { label: "Messages", path: "/chat" },
{ label: "Search", path: "/matches" }, { label: "Search", path: "/matches" },
{ label: "Notifications", path: "/notifications" } { label: "Notifications", path: "/notifications" }
@ -158,6 +161,15 @@ const ProfileHeader = () => {
const [profileDrawerOpen, setProfileDrawerOpen] = useState(false); const [profileDrawerOpen, setProfileDrawerOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [logoutModalOpen, setLogoutModalOpen] = 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 theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up("md")); const isDesktop = useMediaQuery(theme.breakpoints.up("md"));
@ -179,11 +191,105 @@ const ProfileHeader = () => {
return res.data; return res.data;
}, },
enabled: !!auth, 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 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) => { const handleMenuClick = (item) => {
if (item.action === "delete") { if (item.action === "delete") {
@ -200,7 +306,7 @@ const ProfileHeader = () => {
const deleteAccountMutation = useMutation({ const deleteAccountMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
return await axiosInstance.delete(API_ENDPOINTS.DELETE_ACCOUNT); return await axiosInstance.post(API_ENDPOINTS.DELETE_ACCOUNT);
}, },
onSuccess: (response) => { onSuccess: (response) => {
toast.success(response?.data?.message || "Account deleted successfully"); toast.success(response?.data?.message || "Account deleted successfully");
@ -310,10 +416,17 @@ const ProfileHeader = () => {
{getNavIcon(index)} {getNavIcon(index)}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={ <ListItemText primary={
<div className="flex items-center justify-between"> <div className="flex items-center justify-between w-full pr-4">
{label} <span>{label}</span>
{label === "Notifications" && notificationCount > 0 && ( {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> </div>
} /> } />
@ -359,7 +472,10 @@ const ProfileHeader = () => {
items={NAV_LINKS.map(link => link.label)} items={NAV_LINKS.map(link => link.label)}
color="#034E08" color="#034E08"
activeItem={currentLabel} activeItem={currentLabel}
badges={{ "Notifications": notificationCount }} badges={{
"Notifications": notificationCount,
"Messages": chatCount
}}
onItemClick={(item) => { onItemClick={(item) => {
setSelectedItem(item); setSelectedItem(item);
const link = NAV_LINKS.find(l => l.label === item); const link = NAV_LINKS.find(l => l.label === item);
@ -369,16 +485,16 @@ const ProfileHeader = () => {
</Box> </Box>
{(auth ? ( {(auth ? (
<Box sx={{ flexGrow: 0 }}> <Box key="user-menu-box" sx={{ flexGrow: 0 }}>
<Tooltip title="Account Menu"> <Tooltip title="Account Menu">
<IconButton onClick={toggleProfileDrawer(true)}> <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> </IconButton>
</Tooltip> </Tooltip>
</Box> </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>))} onClick={() => navigate("/login")}>Sign In / Sign Up</button>))}

View File

@ -52,10 +52,6 @@ export const SkeletonText = ({
height = 12, height = 12,
className = "", className = "",
}) => { }) => {
useEffect(() => {
injectSkeletonStyles();
}, []);
return ( return (
<div className={className} style={{ display: "grid", gap }}> <div className={className} style={{ display: "grid", gap }}>
{Array.from({ length: lines }).map((_, idx) => ( {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; export default Skeleton;

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" className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16 max-w-6xl mx-auto"
> >
{features.map((feature, index) => ( {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 <BorderBeam
colorFrom="#ff0000ff" colorFrom="#ff0000ff"
colorTo="#338105ff" colorTo="#338105ff"

View File

@ -1,15 +1,10 @@
import React, { useState } from "react"; import React, { useEffect } from "react";
import { Crown, Bookmark, CurrencyIcon, Currency, Wallet, Receipt, Sparkles, MoonStar, IdCard, RockingChair, LocateFixed, School, WorkflowIcon } from "lucide-react"; import { useInView } from "react-intersection-observer";
import CakeIcon from "@mui/icons-material/Cake"; import { RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react";
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 PersonIcon from "@mui/icons-material/Person"; import PersonIcon from "@mui/icons-material/Person";
import StarIcon from "@mui/icons-material/Star"; import StarIcon from "@mui/icons-material/Star";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import PersonAddIcon from "@mui/icons-material/PersonAdd"; import PersonAddIcon from "@mui/icons-material/PersonAdd";
import { motion } from "framer-motion";
import FilterModal from "../../feature/FilterModal"; import FilterModal from "../../feature/FilterModal";
import bride1 from "../../assets/images/bride1.jpg"; import bride1 from "../../assets/images/bride1.jpg";
import bride2 from "../../assets/images/bride2.jpg"; import bride2 from "../../assets/images/bride2.jpg";
@ -23,260 +18,135 @@ import groom4 from "../../assets/images/groom4.jpg";
import horoscope from "../../assets/images/horoscopeicon.png"; import horoscope from "../../assets/images/horoscopeicon.png";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Button, Fab } from "@mui/material"; import toast from "react-hot-toast";
import MessageIcon from "@mui/icons-material/Message"; import { useSelector, useDispatch } from "react-redux";
import PhoneIcon from "@mui/icons-material/Phone"; import { updateFilter } from "../../redux/filterSlice";
// Profile Card Component import { useProfiles } from "../../hooks/useProfiles";
function ProfileCard({ profile }) { import ProfileCardUI from "../common/ProfileCardUI";
const [isLiked, setIsLiked] = useState(false); import ProfileCardSkeleton from "../common/ProfileCardSkeleton";
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>
);
}
// Main Component // Main Component
export default function MatchesInterface() { export default function MatchesInterface() {
const [showSkeleton, setShowSkeleton] = React.useState(false);
const navigate = useNavigate(); 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 = [ const tabs = [
{ {
id: "your-matches", id: "all_matches",
icon: <PersonIcon className="w-6 h-6" />, icon: <PersonIcon className="w-6 h-6" />,
title: "Your Matches", title: "Your Matches",
description: "View all the profiles that match your preferences", description: "View all the profiles that match your preferences",
category: "All Matches", category: "All Matches",
}, },
{ {
id: "shortlisted-by-you", id: "shorlisted_by_you",
icon: <StarIcon className="w-6 h-6" />, icon: <StarIcon className="w-6 h-6" />,
title: "Shortlisted by you", title: "Shortlisted by you",
description: "Matches you have shortlisted", description: "Matches you have shortlisted",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "viewed-you", id: "viewed_you",
icon: <VisibilityIcon className="w-6 h-6" />, icon: <VisibilityIcon className="w-6 h-6" />,
title: "Viewed you", title: "Viewed you",
description: "Matches who have viewed your profile", description: "Matches who have viewed your profile",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "shortlisted-you", id: "shorlisted_you",
icon: <PersonAddIcon className="w-6 h-6" />, icon: <PersonAddIcon className="w-6 h-6" />,
title: "Shortlisted you", title: "Shortlisted you",
description: "Matches who have shortlisted your profile", description: "Matches who have shortlisted your profile",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "viewed-by-you", id: "viewed_by_you",
icon: <VisibilityIcon className="w-6 h-6" />, icon: <VisibilityIcon className="w-6 h-6" />,
title: "Viewed by you", title: "Viewed by you",
description: "Matches you have viewed", description: "Matches you have viewed",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "newly-joined", id: "newly_joined",
icon: <RockingChair className="w-6 h-6" />, icon: <RockingChair className="w-6 h-6" />,
title: "Newly Joined", title: "Newly Joined",
description: "Matches who Joined within the last 30 days", description: "Matches who Joined within the last 30 days",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "location", id: "location_matches",
icon: <LocateFixed className="w-6 h-6" />, icon: <LocateFixed className="w-6 h-6" />,
title: "Location matches", title: "Location matches",
description: "Matches near your location", description: "Matches near your location",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "education", id: "education_matches",
icon: <School className="w-6 h-6" />, icon: <School className="w-6 h-6" />,
title: "Education matches", title: "Education matches",
description: "Matches near your education match", description: "Matches near your education match",
category: "Based on activity", category: "Based on activity",
}, },
{ {
id: "job", id: "job_matches",
icon: <WorkflowIcon className="w-6 h-6" />, icon: <WorkflowIcon className="w-6 h-6" />,
title: "Job matches", title: "Job matches",
description: "Matches near your job", description: "Matches near your job",
@ -284,89 +154,6 @@ export default function MatchesInterface() {
}, },
]; ];
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 = ""; let currentCategory = "";
return ( return (
@ -381,7 +168,7 @@ export default function MatchesInterface() {
<div className="w-full md:w-80"> <div className="w-full md:w-80">
<div <div
className="rounded-[10px] border border-gray-200 bg-white my-6 className="relative 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]" shadow-lg h-[400px] md:h-[600px] overflow-y-auto md:sticky md:top-[150px]"
> >
@ -406,7 +193,9 @@ export default function MatchesInterface() {
</h2> </h2>
)} )}
<div <div
onClick={() => setSelectedTab(tab.id)} onClick={() => {
dispatch(updateFilter({ filter_type: tab.id }));
}}
className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${ className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${
selectedTab === tab.id selectedTab === tab.id
? "bg-green-50 border-l-4 border-green-600" ? "bg-green-50 border-l-4 border-green-600"
@ -472,19 +261,58 @@ export default function MatchesInterface() {
{tabs.find((t) => t.id === selectedTab)?.title} {tabs.find((t) => t.id === selectedTab)?.title}
</h1> </h1>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<img <div className="relative cursor-pointer" onClick={() => {
src={horoscope} if (isPaidMember) {
onClick={() => {
navigate("/horoscoper-generate"); navigate("/horoscoper-generate");
}} } else {
/> toast.error("Star Matching is locked for free members");
}
}}>
<img
src={horoscope}
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 /> <FilterModal />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-2"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-2">
{profiles.map((profile) => ( {isLoading && !isFetchingNextPage ? (
<ProfileCard key={profile.id} profile={profile} /> [...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> </div>
</div> </div>

View File

@ -1,9 +1,23 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Search } from 'lucide-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() { export default function SearchUI() {
const dispatch = useDispatch();
const searchFromStore = useSelector((state) => state.filters.search);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false); 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 // Sample suggestions data - you can replace with dynamic data
const allSuggestions = [ const allSuggestions = [

View File

@ -379,7 +379,7 @@ const DailyRecommendedCard = () => {
</div> </div>
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style jsx global>{` <style>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useRef, useState, useEffect } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { import {
@ -10,141 +10,84 @@ import {
import { import {
Crown, Crown,
Bookmark, Bookmark,
User,
Briefcase,
MapPin,
X, X,
Send,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Heart,
Eye,
} from "lucide-react"; } 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";
// Import Swiper styles // Import Swiper styles
import "swiper/css"; import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
import "swiper/css/effect-coverflow"; import "swiper/css/effect-coverflow";
import { useNavigate } from "react-router-dom"; 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 ProfileCard = ({ profile }) => {
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\"",
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,
},
];
// Profile Card Component
const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate(); 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 ( return (
<motion.div <motion.div
@ -152,140 +95,110 @@ const MatchingList = () => {
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
onClick={() => navigate(`/profile-details/${profile.id}`)} onClick={() => navigate(`/profile-details/${id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-2 border-gray-200" className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer"
> >
{/* Profile Image Section */} {/* IMAGE SECTION */}
<div className="relative"> <div className="relative">
{/* Premium Badge */} <img
{profile.isPremium && ( src={image}
<motion.div alt="profile"
initial={{ scale: 0 }} className="w-full h-[320px] object-cover"
animate={{ scale: 1 }} onError={(e) => {
transition={{ delay: 0.2, type: "spring" }} e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
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> {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 */} <div
<motion.button 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"}`}
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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// shortlist logic if (!shortlistMutation.isPending) {
shortlistMutation.mutate(id);
}
}} }}
> >
<Bookmark className="w-4 h-4" /> <Bookmark size={14} fill={isShortlisted ? "#000" : "none"} />
<span className="text-[12px] font-medium">Shortlist</span> {shortlistMutation.isPending ? "..." : "Shortlist"}
</motion.button>
<div
classname=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
style={{ height: "300px" }}
>
<img
src={profile.image}
alt={profile.name}
className="w-full h-full object-cover"
/>
</div>
{/* <LazyImage
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=600&h=800&fit=crop&crop=faces,top"
alt="Profile"
className="w-full h-90 object-cover"
/> */}
{/* White Gradient Overlay at bottom of image */}
<div
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>
</div> </div>
{/* Stats and Follow Section */} {/* CONTENT */}
<div <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]">
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2 " <h2 className="text-center text-[22px] font-semibold mb-1">
style={{ {name}
background: "rgb(255, 255, 255)", </h2>
}}
>
<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"> <div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" /> <p>ID: {idNumber}</p>
<span className="text-[14px] font-600 text-gray-900"> <p className="flex items-center gap-0.5">
Height: {profile.height} <Eye size={12} /> {lastSeen}
</span> </p>
</div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex flex-wrap justify-center gap-2">
<div className="flex items-center gap-2"> {[
<AccountBalanceWalletIcon className="w-4 h-4 text-gray-700" /> age,
<span className="text-[14px] font-600 text-gray-900"> height,
{profile.salary} salary,
</span> location,
</div> caste,
<div className="flex items-center gap-2"> zodiac1,
<LocationOnIcon className="w-4 h-4 text-gray-700" /> zodiac2,
<span className="text-[14px] font-600 text-gray-900"> ]
{profile.location} .filter(Boolean)
</span> .map((v, i) => (
</div> <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>
<div className="flex items-center gap-4"> <div className="flex gap-4 mt-[15px] justify-center">
<div className="flex items-center gap-2"> <button
<TempleHinduIcon className="w-4 h-4 text-gray-700" /> 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" : ""}`}
<span className="text-[14px] font-600 text-gray-900"> onClick={(e) => {
{profile.caste} e.stopPropagation();
</span> if (!declineMutation.isPending) declineMutation.mutate(id);
</div> }}
<div className="flex items-center gap-2"> disabled={declineMutation.isPending}
<img src={Image} alt="" className="w-4 h-4 text-gray-700" /> >
<span className="text-[14px] font-600 text-gray-900"> <X size={18} /> {declineMutation.isPending ? "..." : "Decline"}
{profile.zodiac1} </button>
</span>
</div> <button
</div> 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" : ""}`}
<div className="flex items-center gap-4"> onClick={(e) => {
<div className="flex items-center gap-2"> e.stopPropagation();
<img src={Image1} alt="" className="w-4 h-4 text-gray-700" /> if (!interestMutation.isPending) interestMutation.mutate(id);
<span className="text-[14px] font-600 text-gray-900"> }}
{profile.zodiac2} disabled={interestMutation.isPending}
</span> >
</div> <Heart size={18} /> {interestMutation.isPending ? "..." : "Interest"}
</button>
</div> </div>
</div> </div>
</motion.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 ( return (
<> <>
<div <div
@ -341,8 +254,8 @@ const MatchingList = () => {
}} }}
className="pb-16" className="pb-16"
> >
{profiles.map((profile) => ( {displayProfiles.map((profile, index) => (
<SwiperSlide key={profile.id}> <SwiperSlide key={profile.id || index}>
<ProfileCard profile={profile} /> <ProfileCard profile={profile} />
</SwiperSlide> </SwiperSlide>
))} ))}
@ -383,7 +296,10 @@ const MatchingList = () => {
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} 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" 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 View All Matches
</motion.button> </motion.button>
@ -391,7 +307,7 @@ const MatchingList = () => {
</div> </div>
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style jsx global>{` <style>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -5,48 +5,13 @@ import "swiper/css/navigation";
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react'; import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react';
import { useRef } from "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 MatrimonyArticles = ({ articles }) => {
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 swiperRef = useRef(null); const swiperRef = useRef(null);
const displayArticles = articles || [];
if (displayArticles.length === 0) return null;
return ( return (
<> <>
<div className="custom-article-swiper py-10 px-2 max-w-[1400px] mx-auto my-10"> <div className="custom-article-swiper py-10 px-2 max-w-[1400px] mx-auto my-10">
@ -70,51 +35,19 @@ const MatrimonyArticles = () => {
loop={true} loop={true}
className="mySwiper" className="mySwiper"
> >
{articleData.map((item, index) => ( {displayArticles.map((item) => (
<SwiperSlide key={index}> <SwiperSlide key={item.id}>
<div className="w-full max-w-[600px] rounded-[10px] overflow-hidden border border-2 border-[#f2f2f2] bg-white"> <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="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 <img
src={item.img} src={item.image}
className="w-full h-88 object-cover" className="w-full h-full object-cover"
alt={item.title} alt={`Article ${item.id}`}
/> />
</div> </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>
</div> </div>

View File

@ -1,7 +1,19 @@
import { Phone, MessageCircle, ThumbsUp, Eye } from 'lucide-react'; import { Phone, MessageCircle, ThumbsUp, Eye } from 'lucide-react';
import promogirl from "../../assets/images/mobile.png" 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 ( return (
<> <>
@ -19,50 +31,31 @@ return (
{/* Subheading with offer */} {/* Subheading with offer */}
<p className="text-[16px] lg:text-[16px] text-gray-900 mb-4"> <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> </p>
{/* Features List */} {/* Features List */}
<div className="space-y-2 mb-2"> <div className="space-y-2 mb-2">
<div className="flex items-start gap-4"> {points.map((point, index) => {
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1"> const Icon = icons[index % icons.length];
<Phone className="w-5 h-5 text-gray-700" /> return (
</div> <div key={index} className="flex items-start gap-4">
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium"> <div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
Call/WhatsApp matches <Icon className="w-5 h-5 text-gray-700" />
</p> </div>
</div> <p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
{point}
<div className="flex items-start gap-4"> </p>
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1"> </div>
<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
</p>
</div>
</div> </div>
{/* CTA Button */} {/* 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 See membership plans
</button> </button>
</div> </div>
@ -86,4 +79,3 @@ return (
</>); </>);
}; };
export default MembershipCard export default MembershipCard

View File

@ -1,116 +1,88 @@
import { useRef, useState } from 'react'; import { useRef, useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules'; import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules';
import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react'; import {
import CakeIcon from "@mui/icons-material/Cake"; Crown,
import HeightIcon from "@mui/icons-material/Height"; Bookmark,
import GroupsIcon from "@mui/icons-material/Groups"; X,
import TempleHinduIcon from "@mui/icons-material/TempleHindu"; ChevronLeft,
import SchoolIcon from "@mui/icons-material/School"; ChevronRight,
import LocationOnIcon from "@mui/icons-material/LocationOn"; Heart,
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew"; Eye,
import profilebg from "../../assets/images/profilebg.jpg"; } from 'lucide-react';
// Import Swiper styles // Import Swiper styles
import 'swiper/css'; import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import 'swiper/css/pagination'; import 'swiper/css/pagination';
import 'swiper/css/effect-coverflow'; import 'swiper/css/effect-coverflow';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
const NewJoinedProfile = () => { import { updateFilter } from "../../redux/filterSlice";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
const swiperRef = useRef(null); import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
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
const ProfileCard = ({ profile }) => { 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 ( return (
<motion.div <motion.div
@ -118,173 +90,110 @@ const ProfileCard = ({ profile }) => {
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
onClick={() => navigate(`/profile-details/${profile.id}`)} onClick={() => navigate(`/profile-details/${id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200" className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer"
> >
{/* IMAGE SECTION */}
<div className="relative"> <div className="relative">
{profile.isPremium && ( <img
<motion.div src={image}
initial={{ scale: 0 }} alt="profile"
animate={{ scale: 1 }} className="w-full h-[320px] object-cover"
transition={{ delay: 0.2, type: 'spring' }} onError={(e) => {
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg" e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
> }}
<Crown className="w-5 h-5 text-white" /> />
</motion.div>
{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>
)} )}
<motion.button <div
whileHover={{ scale: 1 }} 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"}`}
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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!shortlistMutation.isPending) {
shortlistMutation.mutate(id);
}
}} }}
> >
<Bookmark className="w-4 h-4" /> <Bookmark size={14} fill={isShortlisted ? "#000" : "none"} />
<span className="text-[12px] font-medium">Shortlist</span> {shortlistMutation.isPending ? "..." : "Shortlist"}
</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%)",
}}
></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> </div>
<div {/* CONTENT */}
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2" <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]">
style={{ background: "rgb(255, 255, 255)" }} <h2 className="text-center text-[22px] font-semibold mb-1">
> {name}
<div className="flex items-center gap-4"> </h2>
<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"> <div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" /> <p>ID: {idNumber}</p>
<span className="text-[14px] font-600 text-gray-900"> <p className="flex items-center gap-0.5">
Height: {profile.height} <Eye size={12} /> {lastSeen}
</span> </p>
</div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex flex-wrap justify-center gap-2">
<div className="flex items-center gap-2"> {[
<GroupsIcon className="w-4 h-4 text-gray-700" /> age,
<span className="text-[14px] font-600 text-gray-900"> height,
{profile.religion} salary,
</span> location,
</div> caste,
</div> zodiac1,
zodiac2,
<div className="flex items-center gap-4"> ]
<div className="flex items-center gap-2"> .filter(Boolean)
<SchoolIcon className="w-4 h-4 text-gray-700" /> .map((v, i) => (
<span className="text-[14px] font-600 text-gray-900"> <span
{profile.education} key={i}
</span> className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
</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 {v}
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" </span>
strokeLinecap="round" ))}
strokeLinejoin="round" </div>
/>
</svg> <div className="flex gap-4 mt-[15px] justify-center">
)} {/* <button
Interest 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> </button>
</div> </div>
</div> </div>
</motion.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 ( return (
<> <>
@ -301,10 +210,10 @@ const ProfileCard = ({ profile }) => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-center mb-12" className="text-center mb-12"
> >
<h1 className="text-[20px] text-[#034E08] sm:text-[22px] lg:text-[24px] font-bold mb-3"> <h1 className="text-[20px] text-[#000000] sm:text-[22px] lg:text-[24px] font-semibold mb-3">
New Joined New Joined
</h1> </h1>
<p className="text-gray-600 text-[12px]">Find your perfect match today</p> <p className="text-gray-900 text-[12px]">Find your perfect match today</p>
</motion.div> </motion.div>
{/* Swiper Container */} {/* Swiper Container */}
@ -319,10 +228,6 @@ const ProfileCard = ({ profile }) => {
disableOnInteraction: false, disableOnInteraction: false,
pauseOnMouseEnter: true pauseOnMouseEnter: true
}} }}
pagination={{
clickable: true,
dynamicBullets: true
}}
loop={true} loop={true}
speed={800} speed={800}
breakpoints={{ breakpoints={{
@ -341,8 +246,8 @@ const ProfileCard = ({ profile }) => {
}} }}
className="pb-16" className="pb-16"
> >
{profiles.map((profile) => ( {displayProfiles.map((profile, index) => (
<SwiperSlide key={profile.id}> <SwiperSlide key={profile.id || index}>
<ProfileCard profile={profile} /> <ProfileCard profile={profile} />
</SwiperSlide> </SwiperSlide>
))} ))}
@ -383,14 +288,18 @@ const ProfileCard = ({ profile }) => {
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} 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" 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.button>
</motion.div> </motion.div>
</div> </div>
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style jsx global>{` <style>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -9,8 +9,7 @@ import { useNavigate } from "react-router-dom";
import AstroChatUI from "./AstroChatUI"; import AstroChatUI from "./AstroChatUI";
import MembershipCard from "./MembershipCard"; import MembershipCard from "./MembershipCard";
const ProfileCompletion = () => { const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember }) => {
const [completeness, setCompleteness] = useState(30);
const navigate = useNavigate(); const navigate = useNavigate();
const cards = [ const cards = [
@ -95,7 +94,7 @@ const ProfileCompletion = () => {
transition={{ delay: 0.6, type: "spring", stiffness: 200 }} transition={{ delay: 0.6, type: "spring", stiffness: 200 }}
className="text-lg sm:text-xl font-bold text-gray-900" className="text-lg sm:text-xl font-bold text-gray-900"
> >
{completeness}% {percentage}%
</motion.span> </motion.span>
</div> </div>
@ -103,7 +102,7 @@ const ProfileCompletion = () => {
<div className="relative h-3 bg-gray-200 rounded-full overflow-hidden"> <div className="relative h-3 bg-gray-200 rounded-full overflow-hidden">
<motion.div <motion.div
initial={{ width: 0 }} initial={{ width: 0 }}
animate={{ width: `${completeness}%` }} animate={{ width: `${percentage}%` }}
transition={{ delay: 0.8, duration: 1, ease: "easeOut" }} 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" className="absolute top-0 left-0 h-full bg-gradient-to-r from-red-500 to-red-600 rounded-full"
/> />
@ -123,22 +122,24 @@ const ProfileCompletion = () => {
</Box> */} </Box> */}
<AstroChatUI/> <AstroChatUI/>
<div className="my-4 rounded-2xl bg-green-50 border border-1 border-green-200 p-4 ">
<motion.div <div className="my-4 rounded-2xl bg-green-50 border border-1 border-green-200 p-4 ">
variants={container} <motion.div
initial="hidden" variants={container}
animate="show" initial="hidden"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6" animate="show"
> className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"
>
{cards.map((card, index) => ( {/* NOTE: The cards are static for now. To make them dynamic,
the `missingDetails` prop should be an array of objects to map over. */}
<div {cards.map((card, index) => (
onClick={() => navigate(card.url)} <div
key={card.id}
className=" border border-1 border-red-50 bg-white rounded-3xl hover:bg-red-50 hover:border-2 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 flex flex-col items-center space-x-2 h-32 justify-center transition-colors duration-500
cursor-pointer "> cursor-pointer "
>
{/* Icon Container */} {/* Icon Container */}
<motion.div <motion.div
initial={{ rotate: -180, opacity: 0 }} initial={{ rotate: -180, opacity: 0 }}
@ -163,15 +164,12 @@ const ProfileCompletion = () => {
{card.title} {card.title}
</motion.h3> </motion.h3>
</div> </div>
))}
</motion.div>
<MembershipCard becomePaidMember={becomePaidMember} />
))}
</motion.div>
<MembershipCard/>
</div> </div>
</div> </div>
{/* Additional Info Section */} {/* Additional Info Section */}
{/* <motion.div {/* <motion.div

View File

@ -2,113 +2,31 @@ import React, { useState, useRef } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay } from 'swiper/modules'; import { Navigation, Pagination, Autoplay } from 'swiper/modules';
import { Play, X, Heart, Share2, Eye, ChevronLeft, ChevronRight } from 'lucide-react'; import { Play, X, 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 Swiper styles // Import Swiper styles
import 'swiper/css'; import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import 'swiper/css/pagination'; import 'swiper/css/pagination';
const VideoSwiperGallery = () => { const VideoSwiperGallery = ({ videos }) => {
const [selectedVideo, setSelectedVideo] = useState(null); const [selectedVideo, setSelectedVideo] = useState(null);
const swiperRef = useRef(null); const swiperRef = useRef(null);
// Video data with online sources const displayVideos = videos || [];
const videos = [
{ if (displayVideos.length === 0) return null;
id: 1,
title: 'Priya & Rahul - Wedding Story', const getEmbedUrl = (url) => {
thumbnail: weddingImg1, if (!url) return '';
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', let embedUrl = url;
views: '2.4K', if (url.includes('youtu.be/')) {
likes: '142', embedUrl = url.replace('youtu.be/', 'www.youtube.com/embed/');
duration: '3:45' } else if (url.includes('youtube.com/watch?v=')) {
}, embedUrl = url.replace('youtube.com/watch?v=', 'youtube.com/embed/');
{
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'
} }
]; return embedUrl.split('?')[0] + '?autoplay=1';
};
const VideoCard = ({ video }) => { const VideoCard = ({ video }) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@ -129,12 +47,12 @@ const VideoSwiperGallery = () => {
<div className="relative aspect-video"> <div className="relative aspect-video">
<img <img
src={video.thumbnail} src={video.thumbnail}
alt={video.title} alt="Video Thumbnail"
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110" className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/> />
{/* Overlay */} {/* 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 */} {/* Play Button */}
<motion.div <motion.div
@ -146,28 +64,6 @@ const VideoSwiperGallery = () => {
<Play className="w-7 h-7 text-[#034E08] group-hover:text-white ml-1" fill="currentColor" /> <Play className="w-7 h-7 text-[#034E08] group-hover:text-white ml-1" fill="currentColor" />
</div> </div>
</motion.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>
</div> </div>
</motion.div> </motion.div>
@ -202,36 +98,13 @@ const VideoSwiperGallery = () => {
</button> </button>
{/* Video Player */} {/* Video Player */}
<div className="bg-[#034E08] rounded-2xl overflow-y-scroll h-[100%] max-h-[480px] shadow-2xl"> <div className="bg-black rounded-2xl overflow-hidden shadow-2xl">
<video <iframe
src={selectedVideo.videoUrl} src={getEmbedUrl(selectedVideo.youtube_url)}
controls allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
autoPlay allowFullScreen
className="w-full aspect-video" className="w-full aspect-video"
> ></iframe>
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>
</div> </div>
</motion.div> </motion.div>
</motion.div> </motion.div>
@ -290,7 +163,7 @@ const VideoSwiperGallery = () => {
}} }}
className="pb-16" className="pb-16"
> >
{videos.map((video) => ( {displayVideos.map((video) => (
<SwiperSlide key={video.id}> <SwiperSlide key={video.id}>
<VideoCard video={video} /> <VideoCard video={video} />
</SwiperSlide> </SwiperSlide>
@ -343,7 +216,7 @@ const VideoSwiperGallery = () => {
{selectedVideo && <VideoModal />} {selectedVideo && <VideoModal />}
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style jsx global>{` <style>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -1,9 +1,8 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef, useEffect } from "react";
import { import {
Heart, Heart,
X, X,
ChevronRight, ChevronRight,
SkipForward,
Bookmark, Bookmark,
MessageCircle, MessageCircle,
Ban, Ban,
@ -19,8 +18,10 @@ import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
import "swiper/css/thumbs"; import "swiper/css/thumbs";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { sendInterest, shortlistProfile } from "../../services/shortlistapi";
import { toast } from "react-hot-toast";
const MatrimonyProfile = () => { const MatrimonyProfile = ({ data }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
@ -28,26 +29,17 @@ const MatrimonyProfile = () => {
const mainSwiperRef = useRef(null); const mainSwiperRef = useRef(null);
const modalSwiperRef = useRef(null); const modalSwiperRef = useRef(null);
const profile = { if (!data) return null;
name: "Sudharshan M",
id: "M8355880", const profile = data.profile;
verified: true, const personal = data.personalDetails;
lastSeen: "Last seen few hour ago", const family = data.familyDetails;
age: "30 yrs", const education = data.educationalDetails;
height: "5'5\"", const lifestyle = data.lifestyleDetails;
caste: "Brahmin",
education: "Engineer - Non IT", const profileImages = personal.images && personal.images.length > 0
location: "Chennai", ? personal.images
maritalStatus: "Never Married", : [profile.profile_picture || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"];
createdBy: "Profile created by sibling",
images: [
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=600&h=800&fit=crop",
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=600&h=800&fit=crop",
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=600&h=800&fit=crop",
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=600&h=800&fit=crop",
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=600&h=800&fit=crop",
],
};
const openModal = (index) => { const openModal = (index) => {
setIsModalOpen(true); setIsModalOpen(true);
@ -58,6 +50,31 @@ const MatrimonyProfile = () => {
}, 100); }, 100);
}; };
const handleSendInterest = async () => {
try {
const res = await sendInterest(profile.id);
toast.success(res.message || "Interest sent successfully");
} catch (error) {
toast.error(error.message || "Failed to send interest");
}
};
const handleShortlist = async () => {
try {
const res = await shortlistProfile(profile.id);
toast.success(res.message || "Profile shortlisted successfully");
} catch (error) {
toast.error(error.message || "Failed to shortlist");
}
};
const safeVal = (val, key) => {
if (typeof val === 'object' && val !== null) {
return val[key] || "N/A";
}
return val || "N/A";
};
return ( return (
<div className=""> <div className="">
<div <div
@ -76,16 +93,12 @@ const MatrimonyProfile = () => {
prevEl: ".swiper-button-prev-custom", prevEl: ".swiper-button-prev-custom",
nextEl: ".swiper-button-next-custom", nextEl: ".swiper-button-next-custom",
}} }}
pagination={{
type: "fraction",
el: ".swiper-pagination-custom",
}}
onSwiper={(swiper) => { onSwiper={(swiper) => {
mainSwiperRef.current = swiper; mainSwiperRef.current = swiper;
}} }}
className="h-full w-full" className="h-full w-full"
> >
{profile.images.map((img, idx) => ( {profileImages.map((img, idx) => (
<SwiperSlide key={idx}> <SwiperSlide key={idx}>
<div <div
className="w-[320px] h-[330px] cursor-pointer" className="w-[320px] h-[330px] cursor-pointer"
@ -95,35 +108,21 @@ const MatrimonyProfile = () => {
src={img} src={img}
alt={`${profile.name} ${idx + 1}`} alt={`${profile.name} ${idx + 1}`}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300" className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/> />
</div> </div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
{/* Swiper Navigation Buttons */}
<button className="swiper-button-prev-custom absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 text-white p-2 rounded-full hover:bg-black/70 transition-colors"> <button className="swiper-button-prev-custom absolute left-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 text-white p-2 rounded-full hover:bg-black/70 transition-colors">
<ChevronLeft className="w-5 h-5" /> <ChevronLeft className="w-5 h-5" />
</button> </button>
<button className="swiper-button-next-custom absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 text-white p-2 rounded-full hover:bg-black/70 transition-colors"> <button className="swiper-button-next-custom absolute right-2 top-1/2 -translate-y-1/2 z-10 bg-black/50 text-white p-2 rounded-full hover:bg-black/70 transition-colors">
<ChevronRight className="w-5 h-5" /> <ChevronRight className="w-5 h-5" />
</button> </button>
{/* Pagination */}
{/* <div className="swiper-pagination-custom absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/50 text-white px-3 py-1 rounded-full text-sm z-10"></div> */}
{/* Thumbnail Navigation */}
{/* <div className="absolute bottom-0 right-3 flex flex-row gap-2 z-10">
{profile.images.slice(0, 4).map((img, idx) => (
<div
key={idx}
className="w-12 h-12 rounded-lg overflow-hidden border-2 border-white cursor-pointer hover:scale-110 transition-transform shadow-lg"
onClick={() => mainSwiperRef.current?.slideTo(idx)}
>
<img src={img} alt="" className="w-full h-full object-cover" />
</div>
))}
</div> */}
</div> </div>
</div> </div>
@ -144,7 +143,7 @@ const MatrimonyProfile = () => {
/> />
</svg> </svg>
</div> </div>
<span className="text-[#034E08] font-semibold">ID verified</span> <span className="text-[#034E08] font-semibold">{profile.approved ? "ID verified" : "Pending Verification"}</span>
</div> </div>
<div className="relative"> <div className="relative">
<button <button
@ -161,10 +160,16 @@ const MatrimonyProfile = () => {
</button> </button>
{showMenu && ( {showMenu && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-xl border z-10"> <div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-xl border z-10">
<button className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3"> <button
onClick={() => { setShowMenu(false); handleShortlist(); }}
className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3"
>
<Bookmark className="w-4 h-4" /> Shortlist <Bookmark className="w-4 h-4" /> Shortlist
</button> </button>
<button className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3"> <button
onClick={() => { setShowMenu(false); navigate("/chat"); }}
className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3"
>
<MessageCircle className="w-4 h-4" /> Send Message <MessageCircle className="w-4 h-4" /> Send Message
</button> </button>
<button className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3"> <button className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3">
@ -182,44 +187,47 @@ const MatrimonyProfile = () => {
{profile.name} {profile.name}
</h1> </h1>
<p className="text-gray-500 text-sm mb-4"> <p className="text-gray-500 text-sm mb-4">
{profile.id} | {profile.lastSeen} {profile.member_id} | {profile.last_seen_at}
</p> </p>
<div className="space-y-2 mb-6 text-gray-700"> <div className="space-y-2 mb-6 text-gray-700">
<p className="flex flex-wrap gap-2"> <p className="flex flex-wrap gap-2 items-center">
<span className="font-semibold">{profile.maritalStatus}</span>
<span></span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{profile.createdBy} Profile created by {personal.profile_for || "N/A"}
</span> </span>
<span></span> {(personal.age || profile.age) && (
<span>{profile.age}</span> <>
<span></span> <span></span>
<span>{profile.height}</span> <span className="text-sm">{personal.age || profile.age} yrs</span>
<span></span> </>
<span>{profile.caste}</span> )}
</p> {(profile.religion || profile.caste || profile.sub_caste || profile.college_name) && (
<p> <>
<span className="font-semibold">{profile.education}</span> <span></span>
<span> </span> <span className="text-sm">
<span>{profile.location}</span> {[
safeVal(profile.religion, 'religion_name'),
safeVal(profile.caste, 'caste_name'),
safeVal(profile.sub_caste, 'sub_caste_name'),
profile.college_name
].filter(v => v !== "N/A" && v !== undefined).join(" / ")}
</span>
</>
)}
</p> </p>
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex justify-start gap-3"> <div className="flex justify-start gap-3">
<button <button
// onClick={()=>{ className="w-[fit-content] border-2 border-gray-300 text-gray-700 py-2 px-6 rounded-full hover:bg-gray-50 transition-colors flex items-center justify-center gap-2 text-sm"
// navigate("/chat") >
// }}
className="w-[fit-content] border-2 border-gray-300 text-gray-700 py-2 px-6 rounded-full hover:bg-gray-50 transition-colors flex items-center justify-center gap-2 text-sm">
<X className="w-5 h-5" /> Don't Show <X className="w-5 h-5" /> Don't Show
{/* Message */}
</button> </button>
{/* <button className="w-[fit-content] border-2 border-orange-500 text-[#034E08] py-2 px-6 rounded-full hover:bg-orange-50 transition-colors flex items-center justify-center gap-2 text-sm"> <button
<SkipForward className="w-5 h-5" /> Skip onClick={handleSendInterest}
</button> */} className="w-[fit-content] bg-[#034E08] text-white py-2 px-6 rounded-full hover:bg-[#A70710] transition-colors flex items-center justify-center gap-2 font-semibold text-sm"
<button className="w-[fit-content] bg-[#034E08] text-white py-2 px-6 rounded-full hover:bg-[#A70710] transition-colors flex items-center justify-center gap-2 font-semibold text-sm"> >
<Heart className="w-5 h-5" /> Send Interest <Heart className="w-5 h-5" /> Send Interest
</button> </button>
</div> </div>
@ -227,7 +235,7 @@ const MatrimonyProfile = () => {
</div> </div>
</div> </div>
{/* Image Modal with Swiper */} {/* Image Modal */}
{isModalOpen && ( {isModalOpen && (
<div <div
style={{ backdropFilter: "blur(5px)" }} style={{ backdropFilter: "blur(5px)" }}
@ -243,7 +251,6 @@ const MatrimonyProfile = () => {
<div className="max-w-4xl w-full bg-white p-4 rounded-md"> <div className="max-w-4xl w-full bg-white p-4 rounded-md">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
{/* Main Modal Swiper */}
<div <div
className="relative bg-gray-900 rounded-lg overflow-hidden" className="relative bg-gray-900 rounded-lg overflow-hidden"
style={{ height: "65vh" }} style={{ height: "65vh" }}
@ -271,20 +278,22 @@ const MatrimonyProfile = () => {
}} }}
className="h-full w-full" className="h-full w-full"
> >
{profile.images.map((img, idx) => ( {profileImages.map((img, idx) => (
<SwiperSlide key={idx}> <SwiperSlide key={idx}>
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<img <img
src={img} src={img}
alt={`${profile.name} ${idx + 1}`} alt={`${profile.name} ${idx + 1}`}
className="max-w-full max-h-full object-contain" className="max-w-full max-h-full object-contain"
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/> />
</div> </div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
{/* Modal Navigation Buttons */}
<button className="modal-swiper-button-prev absolute left-4 top-1/2 -translate-y-1/2 z-10 bg-black/50 text-white p-3 rounded-full hover:bg-black/70 transition-colors"> <button className="modal-swiper-button-prev absolute left-4 top-1/2 -translate-y-1/2 z-10 bg-black/50 text-white p-3 rounded-full hover:bg-black/70 transition-colors">
<ChevronLeft className="w-6 h-6" /> <ChevronLeft className="w-6 h-6" />
</button> </button>
@ -295,24 +304,13 @@ const MatrimonyProfile = () => {
</div> </div>
<div> <div>
{/* Top Info Bar */}
<div className="bg-white rounded-t-lg p-4 mb-2"> <div className="bg-white rounded-t-lg p-4 mb-2">
<div className="flex items-center justify-between"> <h3 className="font-bold text-lg">{profile.name}</h3>
<div className="swiper-pagination-modal text-lg font-semibold"></div> <p className="text-sm text-gray-600">
<div className="text-right"> {profile.member_id} | Profile created by {personal.profile_for}
<h3 className="font-bold text-lg">{profile.name}</h3> </p>
<p className="text-sm text-gray-600">
{profile.id} | {profile.createdBy}
</p>
<p className="text-sm">
{profile.age} {profile.height} {profile.caste} BE
{profile.education} {profile.location}
</p>
</div>
</div>
</div> </div>
{/* Thumbnail Swiper */}
<Swiper <Swiper
modules={[Thumbs]} modules={[Thumbs]}
watchSlidesProgress watchSlidesProgress
@ -321,25 +319,28 @@ const MatrimonyProfile = () => {
slidesPerView={5} slidesPerView={5}
className="mb-2" className="mb-2"
> >
{profile.images.map((img, idx) => ( {profileImages.map((img, idx) => (
<SwiperSlide key={idx}> <SwiperSlide key={idx}>
<div className="w-full h-16 rounded-lg overflow-hidden border-2 border-white cursor-pointer hover:border-orange-500 transition-colors"> <div className="w-full h-16 rounded-lg overflow-hidden border-2 border-white cursor-pointer hover:border-orange-500 transition-colors">
<img <img
src={img} src={img}
alt="" alt=""
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/> />
</div> </div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
{/* Bottom Action Bar */}
<div className="bg-white p-4 rounded-b-lg mt-2 text-center"> <div className="bg-white p-4 rounded-b-lg mt-2 text-center">
<p className="text-sm text-gray-600 mb-2"> <p className="text-sm text-gray-600 mb-2">Like this member?</p>
Like this member? <button
</p> onClick={handleSendInterest}
<button className="bg-[#034E08] text-white px-8 py-2 rounded-full hover:bg-orange-700 transition-colors font-semibold"> className="bg-[#034E08] text-white px-8 py-2 rounded-full hover:bg-orange-700 transition-colors font-semibold"
>
Send Interest Send Interest
</button> </button>
</div> </div>
@ -351,19 +352,11 @@ const MatrimonyProfile = () => {
<div className="w-[100%] max-w-[1200px] mx-auto my-10 grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="w-[100%] max-w-[1200px] mx-auto my-10 grid grid-cols-1 gap-4 md:grid-cols-2">
{/* Personal Information Section */} {/* Personal Information Section */}
<div className=" border border-gray-200 rounded-lg bg-pink-50/30"> <div className="border border-gray-200 rounded-lg bg-pink-50/30">
<div className="flex items-center gap-2 mb-4 p-3 py-3 bg-green-100"> <div className="flex items-center gap-2 mb-4 p-3 py-3 bg-green-100">
<div className="bg-pink-100 p-2 rounded-full"> <div className="bg-pink-100 p-2 rounded-full">
<svg <svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
className="w-5 h-5 text-[#A70710]" <path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg> </svg>
</div> </div>
<h3 className="font-semibold text-lg">Personal Information</h3> <h3 className="font-semibold text-lg">Personal Information</h3>
@ -371,188 +364,103 @@ const MatrimonyProfile = () => {
<div className="p-5 mb-6 space-y-3 text-sm"> <div className="p-5 mb-6 space-y-3 text-sm">
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Age</span> <span className="text-gray-600 w-40">Name</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">30 Years and 8 months</span> <span className="ml-3 text-gray-900">{profile.name}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Gender</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.gender || profile.type || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Date of Birth</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.dob || profile.dob || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Place of Birth</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.place_of_birth || personal.place_of_birth || profile.place_of_birth || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Time of Birth</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.time_of_birth || personal.time_of_birth || "N/A"}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Height</span> <span className="text-gray-600 w-40">Height</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">5'5"</span> <span className="ml-3 text-gray-900">{profile.height ? `${profile.height} ft` : "N/A"}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Weight</span> <span className="text-gray-600 w-40">Weight</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">97 Kg</span> <span className="ml-3 text-gray-900">{profile.weight ? `${profile.weight} Kg` : "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Body Type</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Average</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Spoken Languages</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">
Tamil (Mother Tongue), English, Hindi
</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Profile Created By</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Sibling</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Marital Status</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Never Married</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Lives In</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Chennai, Tamil Nadu</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Eating Habits</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Vegetarian</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Religion</span> <span className="text-gray-600 w-40">Religion</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Hindu</span> <span className="ml-3 text-gray-900">{personal.religion || safeVal(profile.religion, 'religion_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Profile Created By</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.profile_for || safeVal(profile.profile_for, 'profile_for_name')}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Caste</span> <span className="text-gray-600 w-40">Caste</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Brahmin - Iyer</span> <span className="ml-3 text-gray-900">{personal.caste || safeVal(profile.caste, 'caste_name')}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Subcaste</span> <span className="text-gray-600 w-40">Sub Caste</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Brahacharmam</span> <span className="ml-3 text-gray-900">{personal.sub_caste || safeVal(profile.sub_caste, 'sub_caste_name')}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Gothra(m)</span> <span className="text-gray-600 w-40">Gothram</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Kashyapa / Kaashyapa</span> <span className="ml-3 text-gray-900">{personal.gothram || safeVal(profile.gothram, 'gothram_name')}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Dosha(m)</span> <span className="text-gray-600 w-40">Rasi</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Don't know</span> <span className="ml-3 text-gray-900">{personal.raasi || safeVal(profile.raasi, 'raasi_name')}</span>
</div>
<div className="flex items-center">
<span className="text-gray-600 w-40">Date Of Birth</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> 23-12-1991</span>
{/* <button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
/>
</svg>
Upgrade to view
</button> */}
</div>
<div className="flex items-center">
<span className="text-gray-600 w-40">Star</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Piscus</span>
{/* <button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
/>
</svg>
Upgrade to view
</button> */}
</div>
<div className="flex items-center">
<span className="text-gray-600 w-40">Rassi</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> Revathy</span>
{/* <button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
/>
</svg>
Upgrade to view
</button> */}
</div>
{/* <div className="flex items-center">
<span className="text-gray-600 w-40">Horoscope</span>
<span className="text-gray-400">:</span>
<button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path
fillRule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clipRule="evenodd"
/>
</svg>
Upgrade to view
</button>
</div> */}
<div className="flex">
<span className="text-gray-600 w-40">Employment</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Employed in private</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Income</span> <span className="text-gray-600 w-40">Birth Star</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> 4 - 5 Lakhs</span> <span className="ml-3 text-gray-900">{personal.star || safeVal(profile.star, 'star_name')}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Education</span> <span className="text-gray-600 w-40">Known Languages</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">BE</span> <span className="ml-3 text-gray-900">{personal.known_languages || "N/A"}</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Occupation</span> <span className="text-gray-600 w-40">Speaks Telugu</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Engineer - Non IT</span> <span className="ml-3 text-gray-900">{personal.do_you_speak_telugu === 1 ? "Yes" : personal.do_you_speak_telugu === 0 ? "No" : "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">City</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.district || safeVal(profile.district, 'district_name') || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Pin Code</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.pincode || profile.zip || "N/A"}</span>
</div> </div>
</div> </div>
</div> </div>
{/* Family Information Section */} {/* Family Information Section */}
<div className="border border-gray-200 rounded-lg bg-pink-50/30 "> <div className="border border-gray-200 rounded-lg bg-pink-50/30">
<div className="flex items-center gap-2 p-3 bg-pink-100"> <div className="flex items-center gap-2 p-3 bg-pink-100">
<div className="bg-white p-2 rounded-full"> <div className="bg-white p-2 rounded-full">
<svg <svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
className="w-5 h-5 text-[#A70710]"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" /> <path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg> </svg>
</div> </div>
@ -561,104 +469,44 @@ const MatrimonyProfile = () => {
<div className="p-5 space-y-3 text-sm"> <div className="p-5 space-y-3 text-sm">
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Parents</span> <span className="text-gray-600 w-40">Father Name</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> <span className="ml-3 text-gray-900">{family.father_name || profile.father_name || "N/A"}</span>
Father Passed Away, Mother is a Home Maker
</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Ancestral Origin</span> <span className="text-gray-600 w-40">Father Occupation</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Rameshwaram</span> <span className="ml-3 text-gray-900">{family.father_occupation || profile.father_occupation || "N/A"}</span>
</div> </div>
</div> <div className="flex">
{/* Contact Information Section */} <span className="text-gray-600 w-40">Mother Name</span>
<div className="my-8"> <span className="text-gray-400">:</span>
<div className="flex items-center gap-2 p-3 bg-pink-100"> <span className="ml-3 text-gray-900">{family.mother_name || profile.mother_name || "N/A"}</span>
<div className="bg-white p-2 rounded-full">
<svg
className="w-5 h-5 text-[#A70710]"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
</svg>
</div>
<h3 className="font-semibold text-lg">Contact Information</h3>
</div> </div>
<div className="flex">
<div className="p-5 space-y-3 text-sm"> <span className="text-gray-600 w-40">Mother Occupation</span>
<div className="flex items-center"> <span className="text-gray-400">:</span>
<span className="text-gray-600 w-40">Mobile Number</span> <span className="ml-3 text-gray-900">{family.mother_occupation || profile.mother_occupation || "N/A"}</span>
<span className="text-gray-400">:</span>
<div className="ml-3 flex items-center gap-2">
<svg
className="w-3 h-3 text-green-600"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
</svg>
<svg
className="w-3 h-3 text-red-600"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="text-gray-900">+91 99</span>
<button className="text-[#034E08] hover:text-orange-700 text-xs font-medium">
Upgrade to view
</button>
</div>
</div>
</div> </div>
</div> <div className="flex">
<span className="text-gray-600 w-40">Siblings</span>
{/* About Myself Section */} <span className="text-gray-400">:</span>
<div className="my-8"> <span className="ml-3 text-gray-900">{family.brother_count || profile.brother_count} Brothers, {family.sister_count || profile.sister_count} Sisters</span>
<div className="flex items-center gap-2 p-3 bg-pink-100">
<div className="bg-white p-2 rounded-full">
<svg
className="w-5 h-5 text-[#A70710]"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clipRule="evenodd"
/>
</svg>
</div>
<h3 className="font-semibold text-lg">About Myself</h3>
</div> </div>
<div className="flex">
<div className="p-5 space-y-4 text-sm"> <span className="text-gray-600 w-40">Family Type</span>
<div> <span className="text-gray-400">:</span>
<h4 className="font-semibold text-gray-900 mb-2"> <span className="ml-3 text-gray-900">{family.family_status || safeVal(profile.family_type, 'family_type_name') || "N/A"}</span>
About Sudharshan M </div>
</h4> <div className="flex">
<p className="text-gray-700 leading-relaxed"> <span className="text-gray-600 w-40">Settled</span>
I am making this profile for my brother. He completed his <span className="text-gray-400">:</span>
bachelor's degree and is now working as a project engineer - <span className="ml-3 text-gray-900">{family.settled || "N/A"}</span>
non IT. We belong to a middle class, nuclear family with </div>
traditional values, currently settled in Chennai. <div className="flex">
</p> <span className="text-gray-600 w-40">Native Place</span>
</div> <span className="text-gray-400">:</span>
<div> <span className="ml-3 text-gray-900">{family.native_place || profile.native_place || "N/A"}</span>
<h4 className="font-semibold text-gray-900 mb-2">
What we are looking for
</h4>
<p className="text-gray-700">
Traditional, homely girl with moderate values
</p>
</div>
</div> </div>
</div> </div>
@ -666,11 +514,7 @@ const MatrimonyProfile = () => {
<div className="my-8"> <div className="my-8">
<div className="flex items-center gap-2 p-3 bg-pink-100"> <div className="flex items-center gap-2 p-3 bg-pink-100">
<div className="bg-white p-2 rounded-full"> <div className="bg-white p-2 rounded-full">
<svg <svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
className="w-5 h-5 text-[#A70710]"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" /> <path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" />
</svg> </svg>
</div> </div>
@ -679,55 +523,108 @@ const MatrimonyProfile = () => {
<div className="p-5 space-y-3 text-sm"> <div className="p-5 space-y-3 text-sm">
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Cuisine</span> <span className="text-gray-600 w-40">Diet</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.diet || safeVal(profile.diet, 'diet_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Place of Birth</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.place_of_birth || personal.place_of_birth || profile.place_of_birth || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Time of Birth</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.time_of_birth || personal.time_of_birth || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Panjangam Type</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.panjangam_type || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Dasa Balance</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{lifestyle.dasa_balance || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Dasa Period</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> <span className="ml-3 text-gray-900">
Chinese, North Indian, South Indian {lifestyle.dasa_years || "0"} Years, {lifestyle.dasa_months || "0"} Months, {lifestyle.dasa_days || "0"} Days
</span> </span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Books</span> <span className="text-gray-600 w-40">Age</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> <span className="ml-3 text-gray-900">{(personal.age || profile.age || lifestyle.age) ? `${personal.age || profile.age || lifestyle.age} Years` : "N/A"}</span>
History, Philosophy / Spiritual
</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Hobbies</span> <span className="text-gray-600 w-40">Hobbies</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Cooking</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Movies</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900"> <span className="ml-3 text-gray-900">
Anime, Comedy, Sci-Fi {lifestyle.hobbies && lifestyle.hobbies.length > 0 ? lifestyle.hobbies.join(", ") : "N/A"}
</span> </span>
</div> </div>
<div className="flex"> </div>
<span className="text-gray-600 w-40">Sports</span> </div>
<span className="text-gray-400">:</span> </div>
<span className="ml-3 text-gray-900">Yoga / Meditation</span>
</div> {/* Educational Details Section */}
<div className="flex"> <div className="border border-gray-200 rounded-lg bg-pink-50/30">
<span className="text-gray-600 w-40">Smoking Habits</span> <div className="flex items-center gap-2 p-3 bg-pink-100">
<span className="text-gray-400">:</span> <div className="bg-white p-2 rounded-full">
<span className="ml-3 text-gray-900">Doesn't Smoke</span> <svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
</div> <path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" />
<div className="flex"> </svg>
<span className="text-gray-600 w-40">Drinking Habits</span> </div>
<span className="text-gray-400">:</span> <h3 className="font-semibold text-lg">Educational Details</h3>
<span className="ml-3 text-gray-900">Doesn't Drink</span> </div>
</div>
<div className="p-5 space-y-3 text-sm">
<div className="flex">
<span className="text-gray-600 w-40">Highest Qualification</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{education.education || safeVal(profile.education, 'education_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Field of Study</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{education.study_field || safeVal(profile.study_field, 'study_field_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">College Name</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{profile.college_name || education.college_name || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Occupation</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{education.occupation || safeVal(profile.occupation, 'occupation_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Organization Name</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{education.company_name || profile.company_name || "N/A"}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Employee Type</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{education.employee_type || safeVal(profile.employee_type, 'employee_type_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Annual Income</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{education.annual_income || safeVal(profile.annual_income, 'annual_income_name')}</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Work Location</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{profile.work_location || education.work_location || "N/A"}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View File

@ -1,24 +1,29 @@
import React from 'react'; import React from 'react';
import { Check, X } from 'lucide-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 = [ const basicPreferences = [
{ label: "Preferred Bride's Age", value: "22-29 yrs", match: true }, { label: "Preferred Groom's Age", value: pref.preferred_age_range || "Any", match: matchDetails.age },
{ label: "Preferred Height", value: "5'0\" - 5'5\"", match: false }, { 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: "Never Married", match: true }, { label: "Preferred Marital Status", value: pref.preferred_marital_statuses?.join(", ") || "Any", match: matchDetails.marital_status },
{ label: "Preferred Mother Tongue", value: "Tamil", match: true }, { label: "Preferred Mother Tongue", value: pref.preferred_mother_tongues?.join(", ") || "Any", match: matchDetails.mother_tongue },
{ label: "Preferred Physical Status", value: "Normal", match: true }, { label: "Preferred Education", value: pref.preferred_educations?.join(", ") || "Any", match: matchDetails.education },
{ label: "Preferred Eating Habits", value: "Vegetarian", match: false }, { label: "Preferred Employee Type", value: pref.preferred_employee_types?.join(", ") || "Any", match: true }, // Not in matchDetails?
{ label: "Preferred Smoking Habits", value: "Doesn't Matter", match: true },
{ label: "Preferred Drinking Habits", value: "Doesn't Matter", match: true },
]; ];
const religiousPreferences = [ const religiousPreferences = [
{ label: "Preferred Religion", value: "Hindu", match: true }, { label: "Preferred Caste", value: pref.preferred_castes?.join(", ") || "Any", match: matchDetails.caste },
{ label: "Preferred Caste", value: "Brahmin - Iyer", match: false }, { label: "Preferred Sub-caste", value: pref.preferred_sub_castes?.join(", ") || "Any", match: matchDetails.sub_caste },
{ label: "Preferred Subcaste", value: "Any", match: false }, { label: "Preferred State", value: pref.preferred_states?.join(", ") || "Any", match: true },
{ label: "Preferred Star", value: "Any", match: true }, { label: "Preferred City", value: pref.preferred_districts?.join(", ") || "Any", match: true },
{ label: "Preferred Dosham", value: "No Dosham", 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 }) => ( const PreferenceItem = ({ label, value, match }) => (
@ -46,7 +51,7 @@ const PartnerPreferences = () => {
<div className="text-center mb-6 "> <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"> <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> <span className="text-pink-400"></span>
His Partner Preferences Partner Preferences
<span className="text-pink-400"></span> <span className="text-pink-400"></span>
</h1> </h1>
</div> </div>
@ -56,65 +61,52 @@ const PartnerPreferences = () => {
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<img <img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=male1" src={data.my_profile || "https://api.dicebear.com/7.x/avataaars/svg?seed=male1"}
alt="Profile" alt="Your Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100" className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100 object-cover"
/> />
<div> <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"> <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>
<p className="text-xs text-gray-500">of his preferences</p> <p className="text-xs text-gray-500">of preferences match</p>
</div> </div>
</div> </div>
<img <img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=female1" src={data.profile.profile_picture || "https://api.dicebear.com/7.x/avataaars/svg?seed=female1"}
alt="Your Profile" alt="Partner Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100" className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100 object-cover"
/> />
</div> </div>
</div> </div>
<div className='grid grid-cols-1 gap-2 md:grid-cols-2 mb-8 pt-4'> <div className='grid grid-cols-1 gap-2 md:grid-cols-2 mb-8 pt-4'>
{/* Basic Preferences Section */}
{/* Basic Preferences Section */} <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<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">
<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>
<h2 className="text-lg font-bold text-gray-800">Basic Preferences</h2> </div>
<div className="flex items-center gap-2"> <div className="space-y-1 p-6">
<span className="text-sm text-gray-600">You match</span> {basicPreferences.map((pref, index) => (
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center"> <PreferenceItem key={index} {...pref} />
<Check className="w-4 h-4 text-green-600" /> ))}
</div>
</div> </div>
</div> </div>
<div className="space-y-1 p-6">
{basicPreferences.map((pref, index) => ( {/* Other Preferences Section */}
<PreferenceItem key={index} {...pref} /> <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">Professional & Location</h2>
</div>
<div className="space-y-1 p-6">
{religiousPreferences.map((pref, index) => (
<PreferenceItem key={index} {...pref} />
))}
</div>
</div> </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 */} {/* Footer Note */}
<div className="text-center mt-6 text-sm text-gray-500"> <div className="text-center mt-6 text-sm text-gray-500">
<p>Preferences are used to find compatible matches</p> <p>Preferences are used to find compatible matches</p>

View File

@ -1,73 +1,222 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { Heart, X, Crown, Bookmark, Eye } from "lucide-react"; import { Heart, X, Crown, Bookmark, Eye, Clock, ChevronLeft, ChevronRight } from "lucide-react";
// Import your images import { Swiper, SwiperSlide } from "swiper/react";
import Profile1 from "../../assets/images/bride1.jpg"; import { Navigation, Pagination } from "swiper/modules";
import Profile2 from "../../assets/images/bride2.jpg"; import "swiper/css";
import Profile3 from "../../assets/images/bride3.jpg"; import "swiper/css/navigation";
import Profile4 from "../../assets/images/bride4.jpg"; import "swiper/css/pagination";
import { motion } from "framer-motion"; 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";
const ProfileCardItem = ({ profile }) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [isShortlisted, setIsShortlisted] = useState(profile?.is_shortlisted === 1);
export default function ProfileCard() { useEffect(() => {
// Sample data for multiple cards with image paths setIsShortlisted(profile?.is_shortlisted === 1);
const profiles = [ }, [profile?.is_shortlisted]);
{
id: 1, const shortlistMutation = useMutation({
name: "Jerome Bell", mutationFn: shortlistProfile,
idNumber: "KI2847596", onMutate: () => {
lastSeen: "4 Nov 2025", setIsShortlisted((prev) => !prev);
salary: "5-10",
age: "22 yrs",
height: "5'2\"",
location: "Chennai",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile1,
}, },
{ onSuccess: (data) => {
id: 2, toast.success(data.message || "Profile shortlisted successfully.");
name: "Neha Singh", queryClient.invalidateQueries();
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,
}, },
{ onError: (error) => {
id: 3, setIsShortlisted(profile?.is_shortlisted === 1);
name: "Priya Sharma", toast.error(error.message || "Failed to update shortlist status.");
idNumber: "KI2847598", }
lastSeen: "3 Nov 2025", });
salary: "6-11",
age: "24 yrs", const interestMutation = useMutation({
height: "5'4\"", mutationFn: sendInterest,
location: "Mumbai", onSuccess: (data) => {
caste: "Brahmin", toast.success(data.message || "Interest sent successfully.");
zodiac1: "Aries", queryClient.invalidateQueries();
zodiac2: "Scorpio",
image: Profile3,
}, },
{ onError: (error) => {
id: 4, toast.error(error.message || "Failed to send interest.");
name: "Kavya Iyer", }
idNumber: "KI2847599", });
lastSeen: "2 Nov 2025",
salary: "7-10", const declineMutation = useMutation({
age: "23 yrs", mutationFn: declineProfile,
height: "5'3\"", onSuccess: (data) => {
location: "Bangalore", toast.success(data.message || "Profile declined.");
caste: "Brahmin", queryClient.invalidateQueries();
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile4,
}, },
]; 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-wrap justify-center gap-2">
{[
age,
height,
salary,
location,
caste,
zodiac1,
zodiac2,
]
.filter(Boolean)
.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 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 ( return (
<div className="h-auto py-8 px-4"> <div className="h-auto py-8 px-4">
@ -84,78 +233,79 @@ export default function ProfileCard() {
</h1> </h1>
<p className="text-gray-900 text-[12px]"> <p className="text-gray-900 text-[12px]">
Find your perfect match today Find your perfect match today
</p> </p>
</motion.div> </motion.div>
{/* CARDS GRID */} {/* CARDS GRID */}
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full max-w-[1400px] grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="w-full max-w-[1400px] relative px-2 md:px-12">
{profiles.map((profile) => ( {!showTimer ? (
<div <>
key={profile.id} {/* Custom Navigation Arrows */}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md" <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 */} {displayProfiles.map((profile, index) => (
<div className="relative"> <SwiperSlide key={profile.id || index}>
<img <ProfileCardItem profile={profile} />
src={profile.image} </SwiperSlide>
alt="profile" ))}
className="w-full h-[320px] object-cover" {/* 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">
<div className="absolute top-4 left-4 w-9 h-9 rounded-full bg-[#8b0000] flex items-center justify-center"> <h3 className="text-xl font-bold mb-4 text-gray-800 text-center">You've seen all matches!</h3>
<Crown size={18} color="#fff" /> <button
</div> onClick={handleShowTimer}
className="px-6 py-2 bg-[#8b0000] text-white rounded-full font-semibold shadow-lg hover:bg-red-800 transition"
<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 View Next Batch Timer
</div>
</div>
{/* CONTENT */}
<div className="px-4 py-4 -mt-[60px] bg-white/65 backdrop-blur-[25px] rounded-t-[15px] shadow-[0_-10px_30px_rgba(0,0,0,0.15)] relative z-[2]">
<h2 className="text-center text-[22px] font-semibold mb-1">
{profile.name}
</h2>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8">
<p>ID: {profile.idNumber}</p>
<p className="flex items-center gap-0.5">
<Eye size={12} /> {profile.lastSeen}
</p>
</div>
<div className="flex flex-wrap justify-center gap-2">
{[
profile.age,
profile.height,
profile.salary + " LPA",
profile.location,
profile.caste,
profile.zodiac1,
profile.zodiac2,
].map((v, i) => (
<span
key={i}
className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
>
{v}
</span>
))}
</div>
<div className="flex gap-4 mt-[15px] justify-center">
<button className="px-2 py-1 rounded-[20px] border border-red-200 bg-red-50 flex items-center gap-1.5 font-semibold text-red-900">
<X size={18} /> Decline
</button>
<button className="px-2 py-1 rounded-[20px] border border-green-200 bg-green-50 text-green-900 flex items-center gap-1.5 font-semibold">
<Heart size={18} /> Interest
</button> </button>
</div> </div>
</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>
</div> <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>
</motion.div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -75,12 +75,12 @@ const AdvancedDropzone = ({ value, onChange }) => {
<> <>
<Dropzone <Dropzone
onChange={updateFiles} onChange={updateFiles}
minHeight="195px" minHeight="200px"
value={extFiles} value={extFiles}
accept="image/*" accept="image/*"
maxFiles={3} maxFiles={3}
maxFileSize={10 * 1024 * 1024} 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={{ uploadConfig={{
url: BASE_URL + "/file", url: BASE_URL + "/file",
cleanOnUpload: true, cleanOnUpload: true,
@ -155,9 +155,9 @@ const AdvancedDropzone = ({ value, onChange }) => {
}} }}
aria-label="Move left" aria-label="Move left"
> >
<ArrowLeftIcon fontSize="small" /> <ArrowLeftIcon fontSize="small" sx={{fontSize:"30px"}} />
</button> </button>
<span {/* <span
style={{ style={{
color: "#fff", color: "#fff",
fontSize: 12, fontSize: 12,
@ -170,7 +170,7 @@ const AdvancedDropzone = ({ value, onChange }) => {
title={file.name} title={file.name}
> >
{file.name} {file.name}
</span> </span> */}
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
@ -197,7 +197,7 @@ const AdvancedDropzone = ({ value, onChange }) => {
}} }}
aria-label="Move right" aria-label="Move right"
> >
<ArrowRightIcon fontSize="small" /> <ArrowRightIcon fontSize="small" sx={{fontSize:"30px"}} />
</button> </button>
</div> </div>
)} )}

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 { useDispatch, useSelector } from "react-redux";
import { updateEducationalDetails } from "../redux/registrationFormSlice"; import { updateEducationalDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import { import {
TextField, TextField,
Button, Button,
@ -9,454 +9,412 @@ import {
InputLabel, InputLabel,
Select, Select,
MenuItem, MenuItem,
FormHelperText,
InputAdornment,
Box,
Typography,
} from "@mui/material"; } from "@mui/material";
import { useEducationMasters, useEducationList } from "../hooks/useMasters"; import { useEducationMasters, useEducationList } from "../hooks/useMasters";
import { useCityMasters } from "../hooks/useDependentMasters";
import { toast } from "react-hot-toast";
const EducationalDetailsForm = ({ const EducationalDetailsForm = ({
onSubmitStep, onSubmitStep,
onSkipStep, onSkipStep,
errors, errors: externalErrors,
onFieldChange, isEditMode,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.educationalDetails); const data = useSelector((state) => state.registerform.educationalDetails);
const inputRef = useRef(null); const [localErrors, setLocalErrors] = useState({});
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>; const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: educationMasters, isLoading: isEducationMastersLoading } = const { data: educationMasters, isLoading: isEducationMastersLoading } =
useEducationMasters(); useEducationMasters();
const educationListQuery = useEducationList(data.fieldOfStudy); const educationListQuery = useEducationList(data.study_field);
const districtQuery = useCityMasters(data.work_state);
const studyFieldOptions = useMemo(() => { const studyFieldOptions = educationMasters?.studyFields || [];
const raw = educationMasters; const qualificationOptions = educationListQuery.data?.education || educationListQuery.data?.data || [];
if (!raw) return []; const occupationOptions = educationMasters?.occupation || [];
if (Array.isArray(raw)) return raw; const employeeTypeOptions = educationMasters?.employeeType || [];
return raw.studyFields || raw.study_fields || raw.fieldOfStudy || []; const countryOptions = educationMasters?.country || [];
}, [educationMasters]); const stateOptions = educationMasters?.state || [];
const districtOptions = districtQuery.data?.districts || districtQuery.data || [];
const qualificationOptions = useMemo(() => { const isUnemployed = data.employee_type === 11;
const raw = educationListQuery.data; const isIndia = Number(data.work_country) === 1;
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 handleChange = (field, value) => { const handleChange = (field, value) => {
const updates = { [field]: value }; const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "fieldOfStudy") { if (field === "study_field") {
updates.qualification = ""; updates.education = "";
fieldsToClear.push("qualification");
} }
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)); dispatch(updateEducationalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear); setLocalErrors((prev) => ({ ...prev, [field]: "" }));
if (!isEditMode) {
dispatch(clearAllStepsFrom(3));
}
};
const validateForm = () => {
const newErrors = {};
if (!data.study_field) newErrors.study_field = "Required";
if (!data.education) newErrors.education = "Required";
if (!data.education_detail) newErrors.education_detail = "Required";
if (!data.employee_type) newErrors.employee_type = "Required";
if (!isUnemployed) {
if (!data.occupation) newErrors.occupation = "Required";
if (!data.occupation_detail) newErrors.occupation_detail = "Required";
if (!data.income_currency) newErrors.income_currency = "Required";
if (!data.annual_income) newErrors.annual_income = "Required";
if (!data.work_country) newErrors.work_country = "Required";
if (isIndia) {
if (!data.work_state) newErrors.work_state = "Required";
if (!data.work_district) newErrors.work_district = "Required";
} else {
if (!data.work_city) newErrors.work_city = "Required";
}
}
if (!data.address) newErrors.address = "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 = () => { 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(); onSubmitStep();
}; };
return ( return (
<> <div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8"> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<form noValidate autoComplete="off" style={{ padding: 16 }}> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> {/* 1. Field of Study */}
{/* Field of Study */} <div className="flex flex-col gap-2">
<div className="flex flex-col gap-4"> <label className="text-gray-900 text-[15px]">Field of Study{requiredMark}</label>
<label className="text-gray-900 text-[15px]"> <FormControl fullWidth error={Boolean(localErrors.study_field)} id="study_field">
Field of Study{requiredMark} <InputLabel>Select Field of Study</InputLabel>
</label> <Select
<FormControl value={data.study_field}
fullWidth label="Select Field of Study"
variant="outlined" onChange={(e) => handleChange("study_field", e.target.value)}
error={Boolean(errors.fieldOfStudy)}
> >
<InputLabel id="fieldOfStudy-label"> {studyFieldOptions.map((opt) => (
Select Field of Study <MenuItem key={opt.id} value={opt.id}>{opt.study_field_name}</MenuItem>
</InputLabel> ))}
<Select </Select>
labelId="fieldOfStudy-label" {localErrors.study_field && <FormHelperText>{localErrors.study_field}</FormHelperText>}
label="Select Field of Study" </FormControl>
name="fieldOfStudy"
value={data.fieldOfStudy}
onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
inputRef={inputRef}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{studyFieldOptions.map((field) => (
<MenuItem key={field.id ?? field} value={field.id ?? field}>
{getOptionLabel(field, "Field of Study")}
</MenuItem>
))}
</Select>
{errors.fieldOfStudy && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.fieldOfStudy}
</p>
)}
</FormControl>
</div>
{/* Highest Qualification */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Highest Educational Qualification{requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.qualification)}
>
<InputLabel id="qualification-label">
Select Highest Qualification
</InputLabel>
<Select
labelId="qualification-label"
label="Select Highest Qualification"
name="qualification"
value={data.qualification}
onChange={(e) => handleChange("qualification", e.target.value)}
disabled={
!data.fieldOfStudy || educationListQuery.isLoading
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{qualificationOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Qualification")}
</MenuItem>
))}
</Select>
{errors.qualification && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.qualification}
</p>
)}
</FormControl>
</div>
{/* College Name */}
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
College Name
</label>
<TextField
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"
/>
</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>
<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"
label="Select Employee Type"
name="employeeType"
value={data.employeeType}
onChange={(e) => handleChange("employeeType", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{employeeTypeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Employee Type")}
</MenuItem>
))}
</Select>
{errors.employeeType && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.employeeType}
</p>
)}
</FormControl>
</div>
{/* Annual Income */}
<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>
<Select
labelId="income-label"
label="Select Annual Income"
name="income"
value={data.income}
onChange={(e) => handleChange("income", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{annualIncomeOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}>
{getOptionLabel(item, "Annual Income")}
</MenuItem>
))}
</Select>
{errors.income && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.income}
</p>
)}
</FormControl>
</div>
{/* 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>
) : (
<TextField
fullWidth
name="workLocation"
label="Work Location"
value={data.workLocation}
onChange={(e) => handleChange("workLocation", e.target.value)}
error={Boolean(errors.workLocation)}
helperText={errors.workLocation}
placeholder="Enter Work Location"
variant="outlined"
/>
)}
</div>
</div> </div>
<Grid {/* 2. Highest Qualification */}
item <div className="flex flex-col gap-2">
xs={12} <label className="text-gray-900 text-[15px]">Highest Qualification{requiredMark}</label>
style={{ <FormControl fullWidth error={Boolean(localErrors.education)} id="education" disabled={!data.study_field || educationListQuery.isLoading}>
marginTop: "40px", <InputLabel>Select Qualification</InputLabel>
display: "flex", <Select
gap: 16, value={data.education}
justifyContent: "center", label="Select Qualification"
}} onChange={(e) => handleChange("education", e.target.value)}
> >
<Button variant="outlined" onClick={onSkipStep}> {qualificationOptions.map((opt) => (
Next <MenuItem key={opt.id} value={opt.id}>{opt.education_name || opt.name}</MenuItem>
))}
</Select>
{localErrors.education && <FormHelperText>{localErrors.education}</FormHelperText>}
</FormControl>
</div>
{/* 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
placeholder="Enter College Name"
value={data.college_name}
onChange={(e) => handleChange("college_name", e.target.value)}
/>
</div>
{/* 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
value={data.employee_type}
label="Select Employee Type"
onChange={(e) => handleChange("employee_type", e.target.value)}
>
{employeeTypeOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.employee_type_name}</MenuItem>
))}
</Select>
{localErrors.employee_type && <FormHelperText>{localErrors.employee_type}</FormHelperText>}
</FormControl>
</div>
{!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
value={data.occupation}
label="Select Occupation"
onChange={(e) => handleChange("occupation", e.target.value)}
>
{occupationOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.occupation_name}</MenuItem>
))}
</Select>
{localErrors.occupation && <FormHelperText>{localErrors.occupation}</FormHelperText>}
</FormControl>
</div>
{/* 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
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>
<Box sx={{ mt: 5, display: "flex", gap: 2, justifyContent: "center" }}>
{onSkipStep && (
<Button variant="outlined" size="large" onClick={onSkipStep} sx={{ minWidth: 120 }}>
Skip
</Button> </Button>
<Button variant="contained" color="primary" onClick={handleSubmit}> )}
Submit <Button variant="contained" size="large" onClick={handleSubmit} sx={{ minWidth: 120 }}>
</Button> {onSkipStep ? "Next" : "Update"}
</Grid> </Button>
</form> </Box>
</div> </form>
</> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateFamilyDetails } from "../redux/registrationFormSlice"; import { updateFamilyDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
TextField, TextField,
@ -12,46 +12,55 @@ import {
Box, Box,
} from "@mui/material"; } from "@mui/material";
import { useFamilyMasters } from "../hooks/useMasters"; 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 dispatch = useDispatch();
const data = useSelector((state) => state.registerform.familyDetails); const data = useSelector((state) => state.registerform.familyDetails);
const inputRef = useRef(null); const inputRef = useRef(null);
const brotherSectionRef = useRef(null);
const sisterSectionRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>; const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: familyMasters, isLoading: isFamilyMastersLoading } = const { data: familyMasters, isLoading: isFamilyMastersLoading } = useFamilyMasters();
useFamilyMasters();
// District query for India
const districtQuery = useCityMasters(data.familyState);
const occupationOptions = useMemo(() => { const occupationOptions = useMemo(() => {
const raw = familyMasters; const raw = familyMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.occupation || [];
return raw.occupation || raw.occupations || [];
}, [familyMasters]); }, [familyMasters]);
const maritalStatusOptions = useMemo(() => { const maritalStatusOptions = useMemo(() => {
const raw = familyMasters; const raw = familyMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.maritalStatus || [];
return raw.maritalStatus || raw.marital_status || [];
}, [familyMasters]); }, [familyMasters]);
const familyStatusOptions = useMemo(() => { const familyStatusOptions = useMemo(() => {
const raw = familyMasters; const raw = familyMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return []; return raw.familyStatus || [];
return raw.familyStatus || raw.family_status || [];
}, [familyMasters]); }, [familyMasters]);
const countryOptions = useMemo(() => familyMasters?.country || [], [familyMasters]);
const stateOptions = useMemo(() => familyMasters?.state || [], [familyMasters]);
const districtOptions = useMemo(() => districtQuery.data?.districts || districtQuery.data || [], [districtQuery.data]);
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
const createSibling = () => ({ const createSibling = () => ({
type: "",
name: "", name: "",
occupation: "", occupation: "",
maritalStatus: "", maritalStatus: "",
haveChildrens: "", hasChildren: "",
details: "",
}); });
const syncSiblingArray = (arr, count) => { const syncSiblingArray = (arr, count) => {
@ -72,20 +81,48 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
if (field === "brotherCount") { if (field === "brotherCount") {
const count = Number(value) || 0; const count = Number(value) || 0;
updates.brotherCount = count; updates.brotherCount = value;
updates.brothers = syncSiblingArray(data.brothers, count); updates.brothers = syncSiblingArray(data.brothers, count);
fieldsToClear.push("brothers"); 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") { if (field === "sisterCount") {
const count = Number(value) || 0; const count = Number(value) || 0;
updates.sisterCount = count; updates.sisterCount = value;
updates.sisters = syncSiblingArray(data.sisters, count); updates.sisters = syncSiblingArray(data.sisters, count);
fieldsToClear.push("sisters"); 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)); dispatch(updateFamilyDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear); if (onFieldChange) onFieldChange(fieldsToClear);
if (!isEditMode) {
dispatch(clearAllStepsFrom(4));
}
}; };
const handleSiblingChange = (type, index, field, value) => { const handleSiblingChange = (type, index, field, value) => {
@ -94,14 +131,40 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
list[index] = { ...list[index], [field]: value }; list[index] = { ...list[index], [field]: value };
dispatch(updateFamilyDetails({ [type]: list })); dispatch(updateFamilyDetails({ [type]: list }));
if (onFieldChange) onFieldChange(type); if (onFieldChange) onFieldChange(type);
if (!isEditMode) {
dispatch(clearAllStepsFrom(4));
}
}; };
const handleSubmit = () => { const scrollToError = (errorMap) => {
console.log("Submitting family details:", data); 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(); onSubmitStep();
}; };
const countOptions = Array.from({ length: 11 }, (_, i) => i); const countOptions = Array.from({ length: 11 }, (_, i) => i);
const isIndia = Number(data.familyCountry) === 1;
const renderSiblingCard = (type, index) => { const renderSiblingCard = (type, index) => {
const sibling = (data[type] || [])[index] || createSibling(); const sibling = (data[type] || [])[index] || createSibling();
@ -114,95 +177,88 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
borderRadius: 2, borderRadius: 2,
padding: 2, padding: 2,
backgroundColor: "#fff", backgroundColor: "#fff",
mb: 4
}} }}
> >
<div className="text-gray-900 text-[14px] font-semibold mb-3"> <div className="text-gray-900 text-[14px] font-semibold mb-3">
{labelPrefix} {index + 1} {labelPrefix} {index + 1}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4"> <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 <TextField
fullWidth fullWidth
label="Name" label="Name"
placeholder="Enter Name"
value={sibling.name} value={sibling.name}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "name", e.target.value)}
handleSiblingChange(type, index, "name", e.target.value)
}
variant="outlined" variant="outlined"
inputProps={{ className: index === 0 ? "first-sibling-name" : "" }}
/> />
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-occupation-label`}> <InputLabel>Occupation</InputLabel>
Occupation
</InputLabel>
<Select <Select
labelId={`${type}-${index}-occupation-label`}
label="Occupation" label="Occupation"
value={sibling.occupation} value={sibling.occupation}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "occupation", e.target.value)}
handleSiblingChange(type, index, "occupation", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{occupationOptions.map((opt) => ( {occupationOptions.map((opt) => (
<MenuItem key={opt} value={opt}> <MenuItem key={opt} value={opt}>{opt}</MenuItem>
{opt}
</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-marital-label`}> <InputLabel>Marital Status</InputLabel>
Marital Status
</InputLabel>
<Select <Select
labelId={`${type}-${index}-marital-label`}
label="Marital Status" label="Marital Status"
value={sibling.maritalStatus} value={sibling.maritalStatus}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "maritalStatus", e.target.value)}
handleSiblingChange(type, index, "maritalStatus", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{maritalStatusOptions.map((opt) => ( {maritalStatusOptions.map((opt) => (
<MenuItem key={opt} value={opt}> <MenuItem key={opt} value={opt}>{opt}</MenuItem>
{opt}
</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-children-label`}> <InputLabel>Have Children</InputLabel>
Have Children
</InputLabel>
<Select <Select
labelId={`${type}-${index}-children-label`}
label="Have Children" label="Have Children"
value={sibling.haveChildrens} value={sibling.hasChildren}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "hasChildren", e.target.value)}
handleSiblingChange(
type,
index,
"haveChildrens",
e.target.value
)
}
> >
<MenuItem value={1}>Yes</MenuItem> <MenuItem value=""><em>Select</em></MenuItem>
<MenuItem value={0}>No</MenuItem> <MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
</Select> </Select>
</FormControl> </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> </div>
</Box> </Box>
); );
@ -212,20 +268,18 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4" id="fatherName">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Father Name{requiredMark} Father Name{requiredMark}
</label> </label>
<TextField <TextField
fullWidth fullWidth
inputRef={inputRef} inputRef={inputRef}
name="fatherName" placeholder="Enter Father Name"
label="Father Name"
value={data.fatherName} value={data.fatherName}
onChange={(e) => handleChange("fatherName", e.target.value)} onChange={(e) => handleChange("fatherName", e.target.value)}
error={Boolean(errors.fatherName)} error={Boolean(errors.fatherName)}
helperText={errors.fatherName} helperText={errors.fatherName}
placeholder="Enter Father Name"
variant="outlined" variant="outlined"
/> />
</div> </div>
@ -236,30 +290,24 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</label> </label>
<TextField <TextField
fullWidth fullWidth
name="fatherOccupation" placeholder="Enter Father Occupation"
label="Father Occupation"
value={data.fatherOccupation} value={data.fatherOccupation}
onChange={(e) => handleChange("fatherOccupation", e.target.value)} onChange={(e) => handleChange("fatherOccupation", e.target.value)}
error={Boolean(errors.fatherOccupation)}
helperText={errors.fatherOccupation}
placeholder="Enter Father Occupation"
variant="outlined" variant="outlined"
/> />
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4" id="motherName">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Mother Name{requiredMark} Mother Name{requiredMark}
</label> </label>
<TextField <TextField
fullWidth fullWidth
name="motherName" placeholder="Enter Mother Name"
label="Mother Name"
value={data.motherName} value={data.motherName}
onChange={(e) => handleChange("motherName", e.target.value)} onChange={(e) => handleChange("motherName", e.target.value)}
error={Boolean(errors.motherName)} error={Boolean(errors.motherName)}
helperText={errors.motherName} helperText={errors.motherName}
placeholder="Enter Mother Name"
variant="outlined" variant="outlined"
/> />
</div> </div>
@ -270,13 +318,9 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</label> </label>
<TextField <TextField
fullWidth fullWidth
name="motherOccupation" placeholder="Enter Mother Occupation"
label="Mother Occupation"
value={data.motherOccupation} value={data.motherOccupation}
onChange={(e) => handleChange("motherOccupation", e.target.value)} onChange={(e) => handleChange("motherOccupation", e.target.value)}
error={Boolean(errors.motherOccupation)}
helperText={errors.motherOccupation}
placeholder="Enter Mother Occupation"
variant="outlined" variant="outlined"
/> />
</div> </div>
@ -286,14 +330,13 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
Brother Count Brother Count
</label> </label>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id="brotherCount-label">Select Brother Count</InputLabel> <InputLabel>Select Brother Count</InputLabel>
<Select <Select
labelId="brotherCount-label"
label="Select Brother Count" label="Select Brother Count"
name="brotherCount"
value={data.brotherCount} value={data.brotherCount}
onChange={(e) => handleChange("brotherCount", e.target.value)} onChange={(e) => handleChange("brotherCount", e.target.value)}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{countOptions.map((count) => ( {countOptions.map((count) => (
<MenuItem key={count} value={count}> <MenuItem key={count} value={count}>
{count} {count}
@ -303,19 +346,27 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</FormControl> </FormControl>
</div> </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"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Sister Count Sister Count
</label> </label>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id="sisterCount-label">Select Sister Count</InputLabel> <InputLabel>Select Sister Count</InputLabel>
<Select <Select
labelId="sisterCount-label"
label="Select Sister Count" label="Select Sister Count"
name="sisterCount"
value={data.sisterCount} value={data.sisterCount}
onChange={(e) => handleChange("sisterCount", e.target.value)} onChange={(e) => handleChange("sisterCount", e.target.value)}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{countOptions.map((count) => ( {countOptions.map((count) => (
<MenuItem key={count} value={count}> <MenuItem key={count} value={count}>
{count} {count}
@ -325,103 +376,183 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</FormControl> </FormControl>
</div> </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]"> <label className="text-gray-900 text-[15px]">
Family Status{requiredMark} Family Status{requiredMark}
</label> </label>
<FormControl <FormControl fullWidth variant="outlined" error={Boolean(errors.familyStatus)}>
fullWidth <InputLabel>Select Family Status</InputLabel>
variant="outlined"
error={Boolean(errors.familyStatus)}
>
<InputLabel id="familyStatus-label">Select Family Status</InputLabel>
<Select <Select
labelId="familyStatus-label"
label="Select Family Status" label="Select Family Status"
name="familyStatus"
value={data.familyStatus} value={data.familyStatus}
onChange={(e) => handleChange("familyStatus", e.target.value)} onChange={(e) => handleChange("familyStatus", e.target.value)}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{familyStatusOptions.map((item) => ( {familyStatusOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}> <MenuItem key={item.id} value={item.id}>
{item.family_type_name || item.name || item} {item.family_type_name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{errors.familyStatus && ( {errors.familyStatus && (
<p <p className="text-[#d32f2f] text-[0.75rem] mt-1 ml-3">{errors.familyStatus}</p>
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.familyStatus}
</p>
)} )}
</FormControl> </FormControl>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4" id="nativePlace">
<label className="text-gray-900 text-[15px]">Native Place</label> <label className="text-gray-900 text-[15px]">
Native Place{requiredMark}
</label>
<TextField <TextField
fullWidth fullWidth
name="nativePlace" placeholder="Enter Native Place"
label="Native Place"
value={data.nativePlace} value={data.nativePlace}
onChange={(e) => handleChange("nativePlace", e.target.value)} onChange={(e) => handleChange("nativePlace", e.target.value)}
error={Boolean(errors.nativePlace)} error={Boolean(errors.nativePlace)}
helperText={errors.nativePlace} helperText={errors.nativePlace}
placeholder="Enter Native Place"
variant="outlined" variant="outlined"
/> />
</div> </div>
<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)}
>
{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>
{Number(data.brotherCount) > 0 && ( <div className="mt-10 flex gap-4 justify-center">
<div className="mt-6"> {onSkipStep && (
<div className="text-gray-900 text-[16px] font-semibold mb-3"> <Button variant="outlined" onClick={onSkipStep} sx={{ minWidth: 120 }}>
Brother Details Skip
</div> </Button>
<div className="grid grid-cols-1 gap-4"> )}
{Array.from({ length: Number(data.brotherCount) }).map( <Button variant="contained" color="primary" onClick={handleSubmit} sx={{ minWidth: 120 }}>
(_, index) => renderSiblingCard("brothers", index) {onSkipStep ? "Next" : "Update"}
)}
</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" }}
>
<Button variant="outlined" onClick={onSkipStep}>
Skip
</Button> </Button>
<Button variant="contained" color="primary" onClick={handleSubmit}> </div>
Submit
</Button>
</Grid>
</form> </form>
</div> </div>
); );

View File

@ -1,15 +1,12 @@
import React, { useState } from "react"; import React, { useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
setAge, setAge,
setHeight, setHeight,
setMaritalStatus,
setMotherTongue,
setReligion,
setMatchesWithHoroscope,
setCaste, setCaste,
setSubCaste, setSubCaste,
updateFilter, updateFilter,
resetFilters,
} from "../redux/filterSlice"; } from "../redux/filterSlice";
import { import {
Slider, Slider,
@ -26,8 +23,12 @@ import {
Accordion, Accordion,
AccordionSummary, AccordionSummary,
AccordionDetails, AccordionDetails,
CircularProgress,
} from "@mui/material"; } 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 FilterForm = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -40,34 +41,61 @@ const FilterForm = () => {
location: false, location: false,
lifestyle: false, lifestyle: false,
family: 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) => { const handleAccordionChange = (section) => (event, isExpanded) => {
setExpandedSections((prev) => ({ ...prev, [section]: isExpanded })); setExpandedSections((prev) => ({ ...prev, [section]: isExpanded }));
}; };
const casteOptions = ["Agamudayar", "Pillai", "Vellalar"];
const motherTongueOptions = [
"Tamil",
"Telugu",
"Malayalam",
"Kannada",
"Hindi",
];
const handleSubmit = () => { const handleSubmit = () => {
console.log("Filter Values:", filters); console.log("Filter Values:", filters);
}; };
const handleClear = () => {
dispatch(resetFilters());
toast.success("Filters cleared");
};
const handleSelectionChange = (field, value) => { const handleSelectionChange = (field, value) => {
console.log(`${field} selected:`, 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 ( return (
<div className="max-w-6xl mx-auto p-4 px-0 md:p-6 "> <div className="max-w-6xl mx-auto p-4 px-0 md:p-6 ">
<div className="bg-white rounded-lg shadow-sm"> <div className="bg-white rounded-lg shadow-sm">
{/* Header */} {/* 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"> <Typography variant="h5" className="font-semibold text-center">
Partner Filter preference Partner Filter preference
</Typography> </Typography>
@ -98,7 +126,7 @@ const FilterForm = () => {
<Typography gutterBottom sx={{marginBottom:"30px"}}>Age</Typography> <Typography gutterBottom sx={{marginBottom:"30px"}}>Age</Typography>
<div className="px-2"> <div className="px-2">
<Slider <Slider
value={filters.age} value={[filters.from_age, filters.to_age]}
onChange={(e, newValue) => { onChange={(e, newValue) => {
dispatch(setAge(newValue)); dispatch(setAge(newValue));
handleSelectionChange("Age", newValue); handleSelectionChange("Age", newValue);
@ -119,7 +147,7 @@ const FilterForm = () => {
<Typography gutterBottom sx={{marginBottom:"30px"}}>Height</Typography> <Typography gutterBottom sx={{marginBottom:"30px"}}>Height</Typography>
<div className="px-2"> <div className="px-2">
<Slider <Slider
value={filters.height} value={[filters.from_height, filters.to_height]}
onChange={(e, newValue) => { onChange={(e, newValue) => {
dispatch(setHeight(newValue)); dispatch(setHeight(newValue));
handleSelectionChange("Height", newValue); handleSelectionChange("Height", newValue);
@ -141,46 +169,40 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Marital Status</InputLabel> <InputLabel>Marital Status</InputLabel>
<Select <Select
value={filters.maritalStatus} multiple
value={filters.marital_status || []}
label="Marital Status" label="Marital Status"
onChange={(e) => { onChange={(e) => {
dispatch(setMaritalStatus(e.target.value)); dispatch(updateFilter({ marital_status: e.target.value }));
handleSelectionChange("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) => ( renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}> <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => ( {selected.map((value) => {
<Chip key={value} label={value} size="small" /> 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> </Box>
)} )}
> >
{motherTongueOptions.map((lang) => ( {filterMasters?.marital_statuses?.map((status) => (
<MenuItem key={lang} value={lang}> <MenuItem key={status.id} value={status.id}>
{lang} {status.marital_status_name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -207,61 +229,69 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Religion</InputLabel> <InputLabel>Religion</InputLabel>
<Select <Select
value={filters.religion} multiple
value={filters.religion || []}
label="Religion" label="Religion"
onChange={(e) => { onChange={(e) => {
dispatch(setReligion(e.target.value)); dispatch(updateFilter({ religion: e.target.value }));
handleSelectionChange("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> {filterMasters?.religions?.map((rel) => (
<MenuItem value="Muslim">Muslim</MenuItem> <MenuItem key={rel.id} value={rel.id}>
<MenuItem value="Christian">Christian</MenuItem> {rel.religion_name}
<MenuItem value="Sikh">Sikh</MenuItem> </MenuItem>
))}
</Select> </Select>
</FormControl> </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 */} {/* Caste */}
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Caste (Multi Select)</InputLabel> <InputLabel>Caste</InputLabel>
<Select <Select
multiple multiple
value={filters.caste} value={filters.caste}
label="Caste (Multi Select)" label="Caste"
onChange={(e) => { onChange={(e) => {
dispatch(setCaste(e.target.value)); dispatch(setCaste(e.target.value));
handleSelectionChange("Caste", e.target.value); handleSelectionChange("Caste", e.target.value);
}} }}
renderValue={(selected) => ( renderValue={(selected) => (
<Box <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }} {selected.map((value) => {
> const label = filterMasters?.castes?.find((c) => c.id === value)?.caste_name || value;
{selected.map((value) => ( return (
<Chip key={value} label={value} size="small" /> <Chip
))} key={value}
label={label}
size="small"
onDelete={() => dispatch(setCaste(filters.caste.filter((v) => v !== value)))}
onMouseDown={(e) => e.stopPropagation()}
/>
);
})}
</Box> </Box>
)} )}
> >
{["Agamudayar", "Pillai", "Vellalar"].map((caste) => ( {filterMasters?.castes?.map((caste) => (
<MenuItem key={caste} value={caste}> <MenuItem key={caste.id} value={caste.id}>
{caste} {caste.caste_name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@ -269,21 +299,19 @@ const FilterForm = () => {
{/* Sub-Caste */} {/* Sub-Caste */}
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Sub-Caste (Multi Select)</InputLabel> <InputLabel>Sub-Caste</InputLabel>
<Select <Select
multiple multiple
value={filters.subCaste} value={filters.sub_caste}
label="Sub-Caste (Multi Select)" label="Sub-Caste"
onChange={(e) => { onChange={(e) => {
dispatch(setSubCaste(e.target.value)); dispatch(setSubCaste(e.target.value));
handleSelectionChange("Sub-Caste", e.target.value); handleSelectionChange("Sub-Caste", e.target.value);
}} }}
renderValue={(selected) => ( renderValue={(selected) => (
<Box <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}
>
{selected.map((value) => ( {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> </Box>
)} )}
@ -296,39 +324,7 @@ const FilterForm = () => {
</Select> </Select>
</FormControl> </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> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -356,66 +352,108 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Occupation</InputLabel> <InputLabel>Occupation</InputLabel>
<Select <Select
value={filters.occupation} multiple
value={filters.occupation || []}
label="Occupation" label="Occupation"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ occupation: e.target.value })); dispatch(updateFilter({ occupation: e.target.value }));
handleSelectionChange("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> {filterMasters?.occupations?.map((occ) => (
<MenuItem value="Software Engineer"> <MenuItem key={occ.id} value={occ.id}>
Software Engineer {occ.occupation_name}
</MenuItem> </MenuItem>
<MenuItem value="Doctor">Doctor</MenuItem> ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Annual Income</InputLabel> <InputLabel>Annual Income</InputLabel>
<Select <Select
value={filters.annualIncome} multiple
value={filters.annual_income || []}
label="Annual Income" label="Annual Income"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ annualIncome: e.target.value })); dispatch(updateFilter({ annual_income: e.target.value }));
handleSelectionChange("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> {filterMasters?.annual_incomes?.map((inc) => (
<MenuItem value="0-5 Lakhs">0-5 Lakhs</MenuItem> <MenuItem key={inc.id} value={inc.id}>
<MenuItem value="5-10 Lakhs">5-10 Lakhs</MenuItem> {inc.annual_income_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Employee Type</InputLabel> <InputLabel>Employee Type</InputLabel>
<Select <Select
value={filters.employeeType} multiple
value={filters.employee_type || []}
label="Employee Type" label="Employee Type"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ employeeType: e.target.value })); dispatch(updateFilter({ employee_type: e.target.value }));
handleSelectionChange("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> {filterMasters?.employee_types?.map((emp) => (
<MenuItem value="Government">Government</MenuItem> <MenuItem key={emp.id} value={emp.id}>
<MenuItem value="Private">Private</MenuItem> {emp.employee_type_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Education</InputLabel> <InputLabel>Education</InputLabel>
<Select <Select
value={filters.education} multiple
value={filters.education || []}
label="Education" label="Education"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ education: e.target.value })); dispatch(updateFilter({ education: e.target.value }));
handleSelectionChange("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> {filterMasters?.educations?.map((edu) => (
<MenuItem value="Bachelor">Bachelor</MenuItem> <MenuItem key={edu.id} value={edu.id}>
<MenuItem value="Master">Master</MenuItem> {edu.education_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
@ -443,50 +481,59 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>State</InputLabel> <InputLabel>State</InputLabel>
<Select <Select
value={filters.state} multiple
value={filters.state || []}
label="State" label="State"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ state: e.target.value })); dispatch(updateFilter({ state: e.target.value, district: [] }));
handleSelectionChange("State", e.target.value); 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> {filterMasters?.states?.map((state) => (
<MenuItem value="Tamil Nadu">Tamil Nadu</MenuItem> <MenuItem key={state.id} value={state.id}>
<MenuItem value="Karnataka">Karnataka</MenuItem> {state.state_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Country</InputLabel> <InputLabel>City</InputLabel>
<Select <Select
value={filters.country} multiple
label="Country" value={filters.district || []}
label="City"
disabled={!filters.state || filters.state.length === 0}
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ country: e.target.value })); dispatch(updateFilter({ district: e.target.value }));
handleSelectionChange("Country", 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> {cityOptions.map((city) => (
<MenuItem value="India">India</MenuItem> <MenuItem key={city.id} value={city.id}>
<MenuItem value="USA">USA</MenuItem> {city.district_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </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> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -512,54 +559,22 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Eating Habits</InputLabel> <InputLabel>Eating Habits</InputLabel>
<Select <Select
value={filters.eatingHabits} value={filters.diet}
label="Eating Habits" label="Eating Habits"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ eatingHabits: e.target.value })); dispatch(updateFilter({ diet: e.target.value }));
handleSelectionChange("Eating Habits", e.target.value); handleSelectionChange("Eating Habits", e.target.value);
}} }}
> >
<MenuItem value="Any">Any</MenuItem> {filterMasters?.diets?.map((diet) => (
<MenuItem value="Vegetarian">Vegetarian</MenuItem> <MenuItem key={diet.id} value={diet.id}>
<MenuItem value="Non-Vegetarian">Non-Vegetarian</MenuItem> {diet.diet_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </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> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -585,64 +600,116 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Family Type</InputLabel> <InputLabel>Family Type</InputLabel>
<Select <Select
value={filters.familyType} multiple
value={filters.family_type || []}
label="Family Type" label="Family Type"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ familyType: e.target.value })); dispatch(updateFilter({ family_type: e.target.value }));
handleSelectionChange("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> {filterMasters?.family_types?.map((type) => (
<MenuItem value="Nuclear">Nuclear</MenuItem> <MenuItem key={type.id} value={type.id}>
<MenuItem value="Joint">Joint</MenuItem> {type.family_type_name}
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> </div>
<InputLabel>Family Status</InputLabel> </AccordionDetails>
<Select </Accordion>
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
</MenuItem>
</Select>
</FormControl>
{/* 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",
},
}}
>
<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> <FormControl fullWidth>
<InputLabel>Family Value</InputLabel> <InputLabel>Star</InputLabel>
<Select <Select
value={filters.familyValue} multiple
label="Family Value" value={filters.star || []}
label="Star"
disabled={!filters.isPaidMember}
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ familyValue: e.target.value })); dispatch(updateFilter({ star: e.target.value }));
handleSelectionChange("Family Value", 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>
)}
> >
<MenuItem value="Any">Any</MenuItem> {filterMasters?.stars?.map((star) => (
<MenuItem value="Traditional">Traditional</MenuItem> <MenuItem key={star.id} value={star.id}>
<MenuItem value="Moderate">Moderate</MenuItem> {star.star_name}
<MenuItem value="Liberal">Liberal</MenuItem> </MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
{/* Submit Button */} {/* Sticky Footer Actions */}
<div className="mt-6 flex justify-center"> <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 <Button
variant="contained" variant="contained"
size="large" size="large"
onClick={handleSubmit} onClick={handleSubmit}
className="px-12" className="px-12"
startIcon={<Check size={20} />}
> >
Apply Filters Apply Filters
</Button> </Button>

View File

@ -27,7 +27,7 @@ const FilterModal = () => {
open={open} open={open}
onClose={handleClose} onClose={handleClose}
fullWidth fullWidth
maxWidth="lg" // adjust: "sm" | "md" | "lg" maxWidth="md" // adjust: "sm" | "md" | "lg"
> >
<DialogTitle sx={{background:"#f5fbff"}}> <DialogTitle sx={{background:"#f5fbff"}}>
<Box display="flex" alignItems="center" justifyContent="space-between"> <Box display="flex" alignItems="center" justifyContent="space-between">

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 { useDispatch, useSelector } from "react-redux";
import { updateLifestyleDetails } from "../redux/registrationFormSlice"; import { updateLifestyleDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
FormControl, FormControl,
@ -11,18 +11,26 @@ import {
TextField, TextField,
Checkbox, Checkbox,
ListItemText, ListItemText,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers"; import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { TimePicker } from "@mui/x-date-pickers/TimePicker"; import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { useLifestyleMasters } from "../hooks/useMasters"; import { useLifestyleMasters } from "../hooks/useMasters";
import horoscopeImg from "../assets/images/horoscopeimg.png";
const LifestyleDetailsForm = ({ const LifestyleDetailsForm = ({
onSubmitStep, onSubmitStep,
onSkipStep, onSkipStep,
errors, errors,
onFieldChange, onFieldChange,
isEditMode,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.lifestyleDetails); const data = useSelector((state) => state.registerform.lifestyleDetails);
@ -53,6 +61,15 @@ const LifestyleDetailsForm = ({
return raw.grahas || raw.graha || []; return raw.grahas || raw.graha || [];
}, [lifestyleMasters]); }, [lifestyleMasters]);
const [confirmDialog, setConfirmDialog] = useState({
open: false,
type: "",
house: null,
item: "",
sourceHouse: null,
pendingValue: [],
});
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
@ -60,6 +77,10 @@ const LifestyleDetailsForm = ({
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updateLifestyleDetails({ [field]: value })); dispatch(updateLifestyleDetails({ [field]: value }));
if (onFieldChange) onFieldChange(field); if (onFieldChange) onFieldChange(field);
if (!isEditMode) {
dispatch(clearAllStepsFrom(5));
}
}; };
const handleMultiChange = (field, value) => { const handleMultiChange = (field, value) => {
@ -68,23 +89,74 @@ const LifestyleDetailsForm = ({
if (onFieldChange) onFieldChange(field); if (onFieldChange) onFieldChange(field);
}; };
const handleGrahaChange = (house, value) => { const handleChartChange = (type, house, newValue) => {
const next = { ...(data.graha || {}) }; const currentChart = type === "graha" ? data.graha : data.amsam;
next[house] = value; const currentHouseValue = currentChart?.[house] || [];
dispatch(updateLifestyleDetails({ graha: next }));
if (onFieldChange) onFieldChange("graha"); // 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 handleConfirmMove = () => {
const next = { ...(data.amsam || {}) }; const { type, house, item, sourceHouse, pendingValue } = confirmDialog;
next[house] = value; const currentChart = type === "graha" ? data.graha : data.amsam;
dispatch(updateLifestyleDetails({ amsam: next })); const nextChart = { ...(currentChart || {}) };
if (onFieldChange) onFieldChange("amsam");
// 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 = () => { const handleSubmit = () => {
console.log("Submitting lifestyle details:", data); console.log("Submitting lifestyle details:", data);
onSubmitStep(); onSubmitStep();
}; };
const parseDateValue = (value) => { const parseDateValue = (value) => {
@ -118,6 +190,7 @@ const LifestyleDetailsForm = ({
}; };
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]"> <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"> <span className="text-[10px] font-medium text-gray-700 text-center">
{label} {label}
@ -130,7 +203,17 @@ const LifestyleDetailsForm = ({
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
renderValue={(selected) => { renderValue={(selected) => {
if (!selected || selected.length === 0) return `+ Add ${label}`; 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={{ MenuProps={{
PaperProps: { style: { maxHeight: 280 } }, PaperProps: { style: { maxHeight: 280 } },
@ -145,15 +228,14 @@ const LifestyleDetailsForm = ({
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
</Tooltip>
); );
const renderChartGrid = (type) => { const renderChartGrid = (type) => {
const getValue = (house) => const getValue = (house) =>
(type === "graha" ? data.graha : data.amsam)?.[house] || []; (type === "graha" ? data.graha : data.amsam)?.[house] || [];
const onChange = (house, value) => const onChange = (house, value) =>
type === "graha" handleChartChange(type, house, value);
? handleGrahaChange(house, value)
: handleAmsamChange(house, value);
const label = type === "graha" ? "Rasi" : "Navamsam"; const label = type === "graha" ? "Rasi" : "Navamsam";
return ( return (
@ -165,7 +247,9 @@ const LifestyleDetailsForm = ({
{renderChartCell(label, getValue(5), (value) => onChange(5, value))} {renderChartCell(label, getValue(5), (value) => onChange(5, value))}
<div className="col-span-2 row-span-2 flex items-center justify-center"> <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> </div>
{renderChartCell(label, getValue(6), (value) => onChange(6, value))} {renderChartCell(label, getValue(6), (value) => onChange(6, value))}
@ -186,7 +270,7 @@ const LifestyleDetailsForm = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Diet (Multi-select){requiredMark} Diet{requiredMark}
</label> </label>
<FormControl fullWidth variant="outlined" error={Boolean(errors.diets)}> <FormControl fullWidth variant="outlined" error={Boolean(errors.diets)}>
<InputLabel id="diets-label">Select Diet</InputLabel> <InputLabel id="diets-label">Select Diet</InputLabel>
@ -194,21 +278,10 @@ const LifestyleDetailsForm = ({
labelId="diets-label" labelId="diets-label"
label="Select Diet" label="Select Diet"
name="diets" name="diets"
multiple
value={data.diets} value={data.diets}
onChange={(e) => handleMultiChange("diets", e.target.value)} onChange={(e) => handleChange("diets", e.target.value)}
inputRef={inputRef} inputRef={inputRef}
disabled={isLifestyleMastersLoading} 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={{ sx={{
"& .MuiSelect-select.Mui-disabled": { "& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed", cursor: "not-allowed",
@ -220,8 +293,7 @@ const LifestyleDetailsForm = ({
const label = opt.diet_name || opt.name || String(opt); const label = opt.diet_name || opt.name || String(opt);
return ( return (
<MenuItem key={value} value={value}> <MenuItem key={value} value={value}>
<Checkbox checked={data.diets.indexOf(value) > -1} /> {label}
<ListItemText primary={label} />
</MenuItem> </MenuItem>
); );
})} })}
@ -406,13 +478,33 @@ const LifestyleDetailsForm = ({
justifyContent: "center", justifyContent: "center",
}} }}
> >
<Button variant="outlined" onClick={onSkipStep}> {onSkipStep && (
Skip <Button variant="outlined" onClick={onSkipStep}>
</Button> Skip
</Button>
)}
<Button variant="contained" color="primary" onClick={handleSubmit}> <Button variant="contained" color="primary" onClick={handleSubmit}>
Next {onSkipStep ? "Next" : "Update"}
</Button> </Button>
</Grid> </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> </form>
</div> </div>
); );

View File

@ -417,11 +417,13 @@ const PartnerPreferencesForm = ({
justifyContent: "center", justifyContent: "center",
}} }}
> >
<Button variant="outlined" onClick={onSkipStep}> {onSkipStep && (
Skip <Button variant="outlined" onClick={onSkipStep}>
</Button> Skip
</Button>
)}
<Button variant="contained" color="primary" onClick={handleSubmit}> <Button variant="contained" color="primary" onClick={handleSubmit}>
Next {onSkipStep ? "Next" : "Update"}
</Button> </Button>
</Grid> </Grid>
</form> </form>

File diff suppressed because it is too large Load Diff

View File

View File

@ -1,5 +1,5 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { useSelector } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules"; import { Navigation, Pagination } from "swiper/modules";
import "swiper/css"; import "swiper/css";
@ -17,9 +17,16 @@ import {
Button, Button,
Grid, Grid,
Tooltip, Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from '@mui/material'; } from '@mui/material';
import { usePreviewDetails } from "../hooks/usePreview"; import { usePreviewDetails } from "../hooks/usePreview";
import { isAuthenticated } from "../utills/auth"; import { isAuthenticated } from "../utills/auth";
import horoscopeImg from "../assets/images/horoscopeimg.png";
import { updatePersonalDetails } from "../redux/registrationFormSlice";
const PreviewScreen = ({ onEdit, onSubmit }) => { const PreviewScreen = ({ onEdit, onSubmit }) => {
const formData = useSelector((state) => state.registerform); const formData = useSelector((state) => state.registerform);
@ -28,6 +35,37 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
const isLoading = isAuth && apiLoading; const isLoading = isAuth && apiLoading;
const isError = isAuth && apiError; const isError = isAuth && apiError;
const dispatch = useDispatch();
useEffect(() => {
const pd = previewData?.personal_details;
if (pd) {
const images = pd.images || pd.profile_images;
if (images && images.length > 0) {
// Map images to ensure they have a consistent format for Redux
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 sections = previewData?.personal_details const sections = previewData?.personal_details
? [ ? [
{ {
@ -183,8 +221,8 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0.5, maxWidth: '300px', mx: 'auto', p: 1, bgcolor: '#fff3e0', borderRadius: 2 }}> <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(1)} {renderCell(2)} {renderCell(3)} {renderCell(4)}
{renderCell(5)} {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' }}> <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' }}>
{title.toUpperCase()} <img src={horoscopeImg} alt={title} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
</Box> </Box>
{renderCell(6)} {renderCell(6)}
{renderCell(7)} {renderCell(8)} {renderCell(7)} {renderCell(8)}
@ -224,7 +262,10 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
{!isLoading && {!isLoading &&
sections.map((section) => ( sections.map((section) => (
<Card key={section.title} variant="outlined" sx={{ borderRadius: 2, background:"#fff5ed" }}> <Card key={section.title} variant="outlined"
sx={{ borderRadius: 2,
// background:"#fff5ed"
}}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -243,11 +284,25 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
} }
sx={{padding:"15px 15px", background:"#f5fbff" }} sx={{padding:"15px 15px", background:"#f5fbff" }}
/> />
<Divider /> {/* <Divider /> */}
<CardContent sx={{ pt: 1, }}> <CardContent sx={{ pt: 1, }}>
{Object.entries(section.data || {}).map(([key, value]) => { {Object.entries(section.data || {}).map(([key, value]) => {
// Filter out ID fields // 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') { const hiddenFields = [
"id",
"created_at",
"updated_at",
"phone_number_visibility",
"chat_alert_notification",
"chat_protection",
"profile_photo_protect",
"call_protection",
"match_alert_preference",
"who_can_message",
"who_can_message_categories",
"user_status",
];
if (hiddenFields.includes(key) || key.endsWith('_id') || key.endsWith('Id') || key.endsWith('_ids') || key.endsWith('Ids')) {
return null; return null;
} }
@ -304,14 +359,21 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
color: #d32f2f; color: #d32f2f;
width: 25px; width: 25px;
height: 25px; height: 25px;
overflow: 'hidden';
padding:5px;
display: 'flex';
align-items: 'center';
justify-content: 'center';
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
border-radius: 50%; border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
.custom-swiper-${key} .swiper-button-next::after, .custom-swiper-${key} .swiper-button-next::after,
.custom-swiper-${key} .swiper-button-prev::after { .custom-swiper-${key} .swiper-button-prev::after {
font-size: 12px; font-size: 8px;
font-weight: bold; font-weight: bold;
width:20px;
} }
.custom-swiper-${key} .swiper-pagination-bullet-active { .custom-swiper-${key} .swiper-pagination-bullet-active {
background-color: #d32f2f; background-color: #d32f2f;
@ -329,15 +391,16 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
> >
{value.map((sibling, idx) => ( {value.map((sibling, idx) => (
<SwiperSlide key={idx}> <SwiperSlide key={idx}>
<Card variant="outlined" sx={{ bgcolor: '#fff', height: '100%' }}> <Card variant="outlined"
sx={{ bgcolor: '#fff', height: '100%' }}>
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}> <CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
{Object.entries(sibling).map(([sKey, sVal]) => { {Object.entries(sibling).map(([sKey, sVal]) => {
if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null; if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null;
let displayValue = sVal; let displayValue = sVal;
if (sKey === 'haveChildrens' || sKey === 'have_childrens') { if (sKey === 'haveChildrens' || sKey === 'have_childrens' || sKey === 'hasChildren' || sKey === 'has_children') {
if (sVal === true || sVal === 1 || sVal === '1') { if (sVal === true || sVal === 1 || sVal === '1' || sVal === 'Yes') {
displayValue = 'Yes'; displayValue = 'Yes';
} else if (sVal === false || sVal === 0 || sVal === '0') { } else if (sVal === false || sVal === 0 || sVal === '0' || sVal === 'No') {
displayValue = 'No'; displayValue = 'No';
} }
} }
@ -375,7 +438,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
<Box <Box
key={key} key={key}
py={0.7} py={0.7}
borderBottom="1px solid #e0e0e0" // borderBottom="1px solid #e0e0e0"
sx={{ sx={{
display: "grid", display: "grid",
gridTemplateColumns: { gridTemplateColumns: {
@ -434,12 +497,34 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
color="success" color="success"
size="large" size="large"
onClick={onSubmit} onClick={handleConfirmSubmit}
sx={{ borderRadius: 2 }} sx={{ borderRadius: 2 }}
> >
Submit full Completed Data Submit full Completed Data
</Button> </Button>
</Grid> </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> </Box>
); );

View File

@ -1,9 +1,9 @@
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Autoplay } from "swiper/modules"; import { Navigation, Autoplay } from "swiper/modules";
import { ChevronLeft, ChevronRight, Edit2 } from "lucide-react"; import { ChevronLeft, ChevronRight, Edit2 } from "lucide-react";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import "swiper/css"; import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
import LazyImage from "../components/common/LazyImage"; import LazyImage from "../components/common/LazyImage";
@ -13,9 +13,8 @@ import weddingpromo2 from "../assets/images/weddingpromo2.jpg";
import weddingpromo3 from "../assets/images/weddingpromo3.jpg"; import weddingpromo3 from "../assets/images/weddingpromo3.jpg";
import weddingpromo4 from "../assets/images/weddingpromo4.jpg"; import weddingpromo4 from "../assets/images/weddingpromo4.jpg";
import horoscopeImg from "../assets/images/horoscopeimg.png";
import ProfileCompletion from "../components/profiledashboard/ProfileCompletion"; import ProfileCompletion from "../components/profiledashboard/ProfileCompletion";
import { preloadDummyProfile } from "../redux/registrationFormSlice";
import { useEffect } from "react";
import { import {
Box, Box,
Button, Button,
@ -25,8 +24,12 @@ import {
Divider, Divider,
IconButton, IconButton,
Typography, Typography,
Grid,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { Info } from "@mui/icons-material"; import { Info } from "@mui/icons-material";
import { usePreviewDetails } from "../hooks/usePreview";
import { updatePersonalDetails } from "../redux/registrationFormSlice";
const images = [ const images = [
weddingpromo1, // bride in saree weddingpromo1, // bride in saree
@ -42,20 +45,95 @@ const images = [
]; ];
const ProfilePreviewPage = () => { const ProfilePreviewPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { const dispatch = useDispatch();
personalDetails, const { data, isLoading } = usePreviewDetails();
educationalDetails,
familyDetails,
lifestyleDetails,
partnerPreferences,
} = useSelector((state) => state.registerform);
// For dummy data on first visit
useEffect(() => { useEffect(() => {
dispatch(preloadDummyProfile()); if (data?.personal_details) {
}, [dispatch]); const pd = data.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 }));
}
}
}, [data, dispatch]);
const personalDetails = data?.personal_details
? {
name: data.personal_details.name,
mobileNumber: data.personal_details.mobile,
gender: data.personal_details.gender,
dob: data.personal_details.dob
? data.personal_details.dob.split("T")[0]
: "",
height: data.personal_details.height,
weight: data.personal_details.weight,
maritalStatus: data.personal_details.marital_status,
religion: data.personal_details.religion,
caste: data.personal_details.caste,
email: data.personal_details.email,
state: data.personal_details.state,
city: data.personal_details.district,
pincode: data.personal_details.pincode,
profiles:
data.personal_details.images?.map((url) => ({ preview: url })) || [],
}
: {};
const educationalDetails = data?.educational_details
? {
qualification: data.educational_details.education,
fieldOfStudy: data.educational_details.study_field,
collegeName: data.educational_details.college_name,
occupation: data.educational_details.occupation,
organization: data.educational_details.company_name,
employeeType: data.educational_details.employee_type,
income: data.educational_details.annual_income,
workLocation: data.educational_details.work_location,
}
: {};
const familyDetails = data?.family_details
? {
fatherName: data.family_details.father_name,
fatherOccupation: data.family_details.father_occupation,
motherName: data.family_details.mother_name,
motherOccupation: data.family_details.mother_occupation,
brotherCount: data.family_details.brother_count,
sisterCount: data.family_details.sister_count,
familyStatus: data.family_details.family_status,
nativePlace: data.family_details.native_place,
}
: {};
const lifestyleDetails = data?.lifestyle_details
? {
diets: data.lifestyle_details.diet,
hobbies: data.lifestyle_details.hobbies,
dob: data.lifestyle_details.date_of_birth_formated,
tob: data.lifestyle_details.time_of_birth_formated,
placeOfBirth: data.lifestyle_details.place_of_birth,
horoscope: data.lifestyle_details.horoscope,
}
: {};
const partnerPreferences = data?.partner_preferences
? {
ageRange: data.partner_preferences.preferred_age_range,
castes: data.partner_preferences.preferred_castes,
subCastes: data.partner_preferences.preferred_sub_castes,
occupations: data.partner_preferences.preferred_occupations,
educations: data.partner_preferences.preferred_educations,
hobbies: data.partner_preferences.preferred_hobbies,
annualIncome: data.partner_preferences.preferred_annual_income,
states: data.partner_preferences.preferred_states,
districts: data.partner_preferences.preferred_districts,
}
: {};
const handleEditSection = (stepNum) => { const handleEditSection = (stepNum) => {
navigate("/profile-edit", { state: { step: stepNum } }); navigate("/profile-edit", { state: { step: stepNum } });
@ -64,7 +142,7 @@ const ProfilePreviewPage = () => {
const renderField = (label, value, stepNum = 1) => ( const renderField = (label, value, stepNum = 1) => (
<Box <Box
py={0.7} py={0.7}
borderBottom="1px solid #e0e0e0" // borderBottom="1px solid #e0e0e0"
sx={{ sx={{
display: "grid", display: "grid",
gridTemplateColumns: { gridTemplateColumns: {
@ -102,8 +180,119 @@ const ProfilePreviewPage = () => {
</Box> </Box>
); );
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 }}
>
{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",
}}
>
<Box
component="img"
src={horoscopeImg}
alt={title}
sx={{
width: "70%",
height: "70%",
objectFit: "cover",
animation: "spin 20s linear infinite",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
/>
</Box>
{renderCell(6)}
{renderCell(7)} {renderCell(8)}
{renderCell(9)} {renderCell(10)} {renderCell(11)} {renderCell(12)}
</Box>
</Box>
);
};
const renderPersonalSection = () => ( const renderPersonalSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -120,9 +309,9 @@ const ProfilePreviewPage = () => {
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f5fbff" }} sx={{ padding: "15px 15px", background: "#f2f2f2" }}
/> />
<Divider /> {/* <Divider /> */}
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Name", personalDetails.name, 1)} {renderField("Name", personalDetails.name, 1)}
@ -142,7 +331,7 @@ const ProfilePreviewPage = () => {
{/* Profile Images */} {/* Profile Images */}
<Box <Box
py={0.7} py={0.7}
borderBottom="1px solid #e0e0e0" // borderBottom="1px solid #e0e0e0"
sx={{ sx={{
display: "grid", display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" }, gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
@ -210,7 +399,7 @@ const ProfilePreviewPage = () => {
); );
const renderEducationalSection = () => ( const renderEducationalSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -227,9 +416,9 @@ const ProfilePreviewPage = () => {
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f5fbff" }} sx={{ padding: "15px 15px", background: "#f2f2f2" }}
/> />
<Divider /> {/* <Divider /> */}
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Qualification", educationalDetails.qualification, 2)} {renderField("Qualification", educationalDetails.qualification, 2)}
@ -245,7 +434,7 @@ const ProfilePreviewPage = () => {
); );
const renderFamilySection = () => ( const renderFamilySection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -256,15 +445,15 @@ const ProfilePreviewPage = () => {
<IconButton <IconButton
aria-label="edit" aria-label="edit"
color="primary" color="primary"
onClick={() => handleEditSection(2)} onClick={() => handleEditSection(3)}
size="large" size="large"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f5fbff" }} sx={{ padding: "15px 15px", background: "#f2f2f2" }}
/> />
<Divider /> {/* <Divider /> */}
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Father Name", familyDetails.fatherName)} {renderField("Father Name", familyDetails.fatherName)}
@ -280,7 +469,7 @@ const ProfilePreviewPage = () => {
); );
const renderLifestyleSection = () => ( const renderLifestyleSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -291,15 +480,15 @@ const ProfilePreviewPage = () => {
<IconButton <IconButton
aria-label="edit" aria-label="edit"
color="primary" color="primary"
onClick={() => handleEditSection(2)} onClick={() => handleEditSection(4)}
size="large" size="large"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f5fbff" }} sx={{ padding: "15px 15px", background: "#f2f2f2" }}
/> />
<Divider /> {/* <Divider /> */}
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField( {renderField(
@ -317,32 +506,61 @@ const ProfilePreviewPage = () => {
{renderField("Date of Birth", lifestyleDetails.dob)} {renderField("Date of Birth", lifestyleDetails.dob)}
{renderField("Time of Birth", lifestyleDetails.tob)} {renderField("Time of Birth", lifestyleDetails.tob)}
{renderField("Place of Birth", lifestyleDetails.placeOfBirth)} {renderField("Place of Birth", lifestyleDetails.placeOfBirth)}
{lifestyleDetails.horoscope && (
<Box
sx={{
width: "100%",
mt: 2,
mb: 2,
borderTop: "1px solid #e0e0e0",
pt: 2,
}}
>
<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 = lifestyleDetails.horoscope[`graha_${i}`];
return val ? val.split(",") : [];
}, "Rasi")}
</Grid>
<Grid item xs={12} md={6}>
{renderChartGrid((i) => {
const val = lifestyleDetails.horoscope[`amsam_${i}`];
return val ? val.split(",") : [];
}, "Navamsam")}
</Grid>
</Grid>
</Box>
)}
</CardContent> </CardContent>
</Card> </Card>
); );
const renderPreferenceSection = () => ( const renderPreferenceSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
Lifestyle Details Partner Preferences
</Typography> </Typography>
} }
action={ action={
<IconButton <IconButton
aria-label="edit" aria-label="edit"
color="primary" color="primary"
onClick={() => handleEditSection(2)} onClick={() => handleEditSection(5)}
size="large" size="large"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f5fbff" }} sx={{ padding: "15px 15px", background: "#f2f2f2" }}
/> />
<Divider /> {/* <Divider /> */}
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Age Range", partnerPreferences.ageRange)} {renderField("Age Range", partnerPreferences.ageRange)}
@ -394,6 +612,14 @@ const ProfilePreviewPage = () => {
</Card> </Card>
); );
if (isLoading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="50vh">
<Typography>Loading Profile...</Typography>
</Box>
);
}
return ( return (
<> <>
<div className="custom-swiper-hero flex items-center justify-center p-4"> <div className="custom-swiper-hero flex items-center justify-center p-4">

File diff suppressed because it is too large Load Diff

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
src/hooks/useDebounce.js Normal file
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
src/hooks/useDebounce.jsx Normal file
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;

67
src/hooks/useProfiles.js Normal file
View File

@ -0,0 +1,67 @@
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]) => {
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 }) =>
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
src/hooks/useWebSocket.js Normal file
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 };
};

View File

@ -7,8 +7,9 @@ import { ThemeProvider } from "@mui/material/styles";
import theme from "./theme"; import theme from "./theme";
import { Provider } from "react-redux"; 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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PersistGate } from "redux-persist/integration/react";
// Disable noisy logs in production while keeping warnings/errors. // Disable noisy logs in production while keeping warnings/errors.
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
console.log = () => {}; console.log = () => {};
@ -29,12 +30,14 @@ const queryClient = new QueryClient({
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <PersistGate loading={null} persistor={persistor}>
<App /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <App />
</QueryClientProvider>
</PersistGate>
</Provider> </Provider>
</ThemeProvider> </ThemeProvider>
); );

View File

@ -1,42 +1,57 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Tabs, Tab, Box, Chip } from '@mui/material'; import { Tabs, Tab, Box, CircularProgress } from '@mui/material';
import { CheckCircle, Phone, ExpandMore } from '@mui/icons-material'; import { CheckCircle, Phone } from '@mui/icons-material';
import { ChevronDown } from 'lucide-react'; 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="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="flex items-start gap-4">
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<img <img
src={profile.image} src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name} alt={profile.name}
className="w-32 h-32 rounded-2xl object-cover border-2 border-[#A70710]" 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" /> <Phone className="w-4 h-4" />
</button> </button> */}
</div> </div>
<div className="flex-1"> <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" /> <CheckCircle className="text-green-500 w-5 h-5" />
<span className="text-green-500 font-medium">Verified</span> <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> <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"> <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.age ? `${profile.age} yrs` : ''}{profile.age && profile.height ? ', ' : ''}{profile.height || ''}</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.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>
</div> </div>
<div className="mt-6 flex items-center justify-between border-t border-[#A70710] pt-4"> <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> <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 UnBlock
</button> </button>
</div> </div>
@ -49,7 +64,7 @@ const ReportedProfile = ({ profile, onViewReason }) => {
<div className="flex flex-col sm:flex-row items-start gap-4"> <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'> <div className='overflow-hidden w-[100%] h-[100%] max-w-50 max-h-45 rounded-lg flex-shrink-0'>
<img <img
src={profile.image} src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name} alt={profile.name}
className="w-full h-full object-cover " className="w-full h-full object-cover "
/> />
@ -57,25 +72,39 @@ const ReportedProfile = ({ profile, onViewReason }) => {
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<h3 className="text-lg font-bold text-gray-900 mb-1">{profile.name}</h3> <h3 className="text-lg font-bold text-gray-900 mb-1">{profile.name}</h3>
<p className="text-sm text-gray-500 mb-2"> <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> </p>
<div className="space-y-1.5 text-sm"> <div className="space-y-1.5 text-sm">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">Profile created by Parent</span> <span className="text-gray-600">Profile created by Parent</span>
<span className="text-gray-400"></span> {profile.age && (
<span className="text-gray-600">{profile.age} yrs</span> <>
</div> <span className="text-gray-400"></span>
<div className="flex items-center gap-2"> <span className="text-gray-600">{profile.age} yrs</span>
<span className="text-gray-400"></span> </>
<span className="text-gray-600">{profile.caste}</span> )}
</div> </div>
{profile.caste_name && (
<div className="flex items-center gap-2">
<span className="text-gray-400"></span>
<span className="text-gray-600">{profile.caste_name}</span>
</div>
)}
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-gray-400"></span> {profile.occupation && (
<span className="text-gray-600">{profile.occupation}</span> <>
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">{profile.location}</span> <span className="text-gray-600">{profile.occupation}</span>
</>
)}
{profile.district_name && (
<>
<span className="text-gray-400"></span>
<span className="text-gray-600">{profile.district_name}</span>
</>
)}
</div> </div>
</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="bg-white rounded-lg shadow-2xl max-w-md w-full p-6 animate-slideUp">
<div className="flex items-start gap-4 mb-4"> <div className="flex items-start gap-4 mb-4">
<img <img
src={profile.image} src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name} alt={profile.name}
className="w-16 h-20 rounded-lg object-cover flex-shrink-0" className="w-16 h-20 rounded-lg object-cover flex-shrink-0"
/> />
<div> <div>
<h3 className="text-lg font-bold text-gray-900">{profile.name}</h3> <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> </div>
<div className="border-t pt-4"> <div className="border-t pt-4">
<h4 className="font-bold text-gray-900 mb-3">Reason For Report</h4> <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"> <p className="text-sm text-gray-600 leading-relaxed bg-gray-50 p-3 rounded-lg mb-4">
{profile.reportReason} {profile.reason}
</p> </p>
</div> </div>
<div className='w-full flex justify-center'> <div className='w-full flex justify-center'>
@ -134,117 +163,63 @@ const ReportReasonModal = ({ profile, onClose }) => {
function BlockedProfileListPage() { function BlockedProfileListPage() {
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [selectedReport, setSelectedReport] = useState(null); const [selectedReport, setSelectedReport] = useState(null);
const blockedProfiles = [ const [blockedProfiles, setBlockedProfiles] = useState([]);
{ const [reportedProfiles, setReportedProfiles] = useState([]);
id: 'M6075010', const [loading, setLoading] = useState(true);
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 reportedProfiles = [ useEffect(() => {
{ fetchData();
id: 'TK52586A', }, []);
name: 'Pavilash . P',
age: 23, const fetchData = async () => {
lastSeen: 'Nov 25', setLoading(true);
caste: 'Agamudayar / Arcot / Thuluva vellala', try {
occupation: 'Engineer-non IT', const [blockedRes, reportedRes] = await Promise.all([
location: 'Chennai', getBlockedProfiles(),
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop', getReportedProfiles()
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.'
}, if (blockedRes.status === "success") {
{ setBlockedProfiles(blockedRes.data);
id: 'TK52586A', }
name: 'Pavilash . P', if (reportedRes.status === "success") {
age: 23, setReportedProfiles(reportedRes.data);
lastSeen: 'Nov 25', }
caste: 'Agamudayar / Arcot / Thuluva vellala', } catch (error) {
occupation: 'Engineer-non IT', console.error("Error fetching data:", error);
location: 'Chennai', toast.error("Failed to load profiles");
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop', } finally {
showReason: true, setLoading(false);
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.'
} }
]; };
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) => { const handleTabChange = (event, newValue) => {
setActiveTab(newValue); setActiveTab(newValue);
}; };
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<CircularProgress color="error" />
</Box>
);
}
return ( return (
<div className=" py-4 md:py-8"> <div className=" py-4 md:py-8">
<div className="max-w-[1400px] mx-auto"> <div className="max-w-[1400px] mx-auto">
@ -259,14 +234,14 @@ function BlockedProfileListPage() {
textTransform: 'none', textTransform: 'none',
fontSize: '1rem', fontSize: '1rem',
fontWeight: 600, fontWeight: 600,
minWidth: 120, minWidth: 150,
}, },
'& .Mui-selected': { '& .Mui-selected': {
color: '#fff !important', color: '#fff !important',
background:"#A70710" background:"#A70710"
}, },
'& .MuiTabs-indicator': { '& .MuiTabs-indicator': {
backgroundColor: '#A70710', backgroundColor: 'transparent',
}, },
}} }}
> >
@ -277,28 +252,32 @@ function BlockedProfileListPage() {
<div className="transition-all duration-300"> <div className="transition-all duration-300">
{activeTab === 0 && ( {activeTab === 0 && (
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-2'> <div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-4 px-4'>
{blockedProfiles.map((profile, index) => ( {blockedProfiles.length > 0 ? (
<BlockedProfile key={index} profile={profile} /> 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> </div>
)} )}
{activeTab === 1 && ( {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'> <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.map((profile, index) => ( {reportedProfiles.length > 0 ? (
<ReportedProfile reportedProfiles.map((profile, index) => (
key={index} <ReportedProfile
profile={profile} key={profile.id || index}
onViewReason={setSelectedReport} profile={profile}
/> onViewReason={setSelectedReport}
))} />
))
) : (
<div className="col-span-full text-center py-10 text-gray-500">No reported profiles found.</div>
)}
</div> </div>
)} )}
</div> </div>
{/* Report Reason Modal */} {/* Report Reason Modal */}
<ReportReasonModal <ReportReasonModal
@ -307,7 +286,7 @@ function BlockedProfileListPage() {
/> />
</div> </div>
<style jsx>{` <style>{`
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { getContactUs } from '../api/contact.api'; import { getContactUs } from '../api/contact.api';
import LazyImage from '../components/common/LazyImage'; import LazyImage from '../components/common/LazyImage';
import InstagramIcon from '@mui/icons-material/Instagram'; import InstagramIcon from '@mui/icons-material/Instagram';
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
import FacebookIcon from '@mui/icons-material/Facebook'; import FacebookIcon from '@mui/icons-material/Facebook';
import SvgIcon from '@mui/material/SvgIcon'; import SvgIcon from '@mui/material/SvgIcon';
import { Phone, Mail, ChevronRight } from 'lucide-react'; import { Phone, Mail, ChevronRight } from 'lucide-react';
@ -39,7 +40,13 @@ const ContactUsPage = () => {
icon: <XIcon fontSize="large" />, icon: <XIcon fontSize="large" />,
color: "from-gray-800 to-black", color: "from-gray-800 to-black",
url: contact.x_url 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) { if (isLoading) {

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,58 @@
import React, { useState, useEffect } from 'react';
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import MatrimonyProfile from "../components/profiledetail/MatrimonyProfile" import MatrimonyProfile from "../components/profiledetail/MatrimonyProfile"
import PartnerPreferences from "../components/profiledetail/PartnerPreferences" import PartnerPreferences from "../components/profiledetail/PartnerPreferences"
import MatchingList from "../components/profiledashboard/MatchingList"; import MatchingList from "../components/profiledashboard/MatchingList";
import { getProfileDetail } from "../services/profileActionApi";
import { CircularProgress, Box } from "@mui/material";
const ProfileDetailPage = () => { const ProfileDetailPage = () => {
const { id } = useParams(); const { id } = useParams();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDetail = async () => {
setLoading(true);
try {
const res = await getProfileDetail(id);
setData(res);
} catch (error) {
console.error("Failed to fetch profile details:", error);
} finally {
setLoading(false);
}
};
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 ( return (
<> <>
<div className="w-[100%] max-w-[1400px] mx-auto my-10"> <div className="w-[100%] max-w-[1400px] mx-auto my-10">
<MatrimonyProfile/> <MatrimonyProfile data={data} />
<PartnerPreferences/> <PartnerPreferences data={data} />
<MatchingList/> <MatchingList matches={data.all_matches} />
</div> </div>
</> </>
) );
} };
export default ProfileDetailPage export default ProfileDetailPage;

View File

@ -11,7 +11,8 @@ import weddingpromo2 from "../assets/images/weddingpromo2.jpg";
import weddingpromo3 from "../assets/images/weddingpromo3.jpg"; import weddingpromo3 from "../assets/images/weddingpromo3.jpg";
import weddingpromo4 from "../assets/images/weddingpromo4.jpg"; import weddingpromo4 from "../assets/images/weddingpromo4.jpg";
import { useDashboardQuery } from "../hooks/useDashboardQuery";
const NewJoinedProfile = lazy(() => import("../components/profiledashboard/NewJoinedProfile"));
const ProfileCompletion = lazy(() => import("../components/profiledashboard/ProfileCompletion")); const ProfileCompletion = lazy(() => import("../components/profiledashboard/ProfileCompletion"));
const MatrimonyArticles = lazy(() => import("../components/profiledashboard/MatrimonyArticles")); const MatrimonyArticles = lazy(() => import("../components/profiledashboard/MatrimonyArticles"));
const MatchingList = lazy(() => import("../components/profiledashboard/MatchingList")); const MatchingList = lazy(() => import("../components/profiledashboard/MatchingList"));
@ -48,6 +49,15 @@ const SectionFallback = ({ height = 280 }) => (
const UserDashboardHome = () => { 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 ( return (
<> <>
<div className="custom-swiper-hero flex items-center justify-center p-4"> <div className="custom-swiper-hero flex items-center justify-center p-4">
@ -92,11 +102,11 @@ const UserDashboardHome = () => {
}, },
}} }}
> >
{images.map((img, idx) => ( {sliderImages.map((slide, idx) => (
<SwiperSlide key={idx}> <SwiperSlide key={slide.id || idx}>
<div className="relative overflow-hidden rounded-3xl w-[100%]"> <div className="relative overflow-hidden rounded-3xl w-[100%]">
<img <img
src={img} src={slide.image}
alt={`Slide ${idx + 1}`} alt={`Slide ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
loading={idx === 0 ? "eager" : "lazy"} loading={idx === 0 ? "eager" : "lazy"}
@ -206,28 +216,32 @@ const UserDashboardHome = () => {
`}</style> `}</style>
</div> </div>
<Suspense fallback={<SectionFallback height={320} />}> <Suspense fallback={<SectionFallback height={320} />}>
<Profilecardemo /> <Profilecardemo profiles={dashboardData?.daily_recommended} />
</Suspense> </Suspense>
{/* <DailyRecommendedCard/> */} {/* <DailyRecommendedCard/> */}
<Suspense fallback={<SectionFallback height={220} />}> <Suspense fallback={<SectionFallback height={220} />}>
<ProfileCompletion /> <ProfileCompletion
percentage={dashboardData?.profile_complete_percentage}
missingDetails={dashboardData?.non_filled_sections}
becomePaidMember={dashboardData?.become_paid_member}
/>
</Suspense> </Suspense>
{/* <DailyRecommendedCard/> */} {/* <DailyRecommendedCard/> */}
<Suspense fallback={<SectionFallback height={280} />}> <Suspense fallback={<SectionFallback height={280} />}>
<MatrimonyArticles /> <MatrimonyArticles articles={dashboardData?.blogs} />
</Suspense> </Suspense>
<Suspense fallback={<SectionFallback height={320} />}> <Suspense fallback={<SectionFallback height={320} />}>
<MatchingList /> <MatchingList matches={dashboardData?.all_matches} />
</Suspense>
<Suspense fallback={<SectionFallback height={320} />}>
<NewJoinedProfile profiles={dashboardData?.new_joined} />
</Suspense> </Suspense>
{/* <PaidMemberCard/> */}
{/* <NewJoinedProfile/> */}
{/* <CustomerSupportCard/> */} {/* <CustomerSupportCard/> */}
<Suspense fallback={<SectionFallback height={240} />}> <Suspense fallback={<SectionFallback height={240} />}>
<VideoSwiperGallery /> <VideoSwiperGallery videos={dashboardData?.youtube_videos} />
</Suspense> </Suspense>
</> </>
) )

View File

@ -74,6 +74,13 @@ const LoginPage = () => {
localStorage.setItem("access_token", token); localStorage.setItem("access_token", token);
setAccessToken(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!"); toast.success("Login Successful!");
navigate("/dashboard-home"); navigate("/dashboard-home");
} else { } else {

View File

@ -1,29 +1,27 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
const initialState = { const initialState = {
age: [18, 70], from_age: 18,
height: [4.0, 7.11], to_age: 70,
maritalStatus: "All Profile", from_height: 4.0,
motherTongue: [], to_height: 7.11,
religion: "Hindu", marital_status: [],
matchesWithHoroscope: false, religion: [],
caste: [], caste: [],
subCaste: [], sub_caste: [],
star: "Any", star: [],
dasham: "Doesn't matter", occupation: [],
occupation: "Any", annual_income: [],
annualIncome: "Any", employee_type: [],
employeeType: "Any", education: [],
education: "Any", state: [],
state: "Any", district: [],
country: "Any", diet: "",
citizenship: "Any", family_type: [],
eatingHabits: "Any", filter_type: "all_matches",
smokingHabits: "Doesn't matter", page: 1,
drinkingHabits: "Doesn't matter", isPaidMember: false,
familyType: "Any", search: "",
familyStatus: "Any",
familyValue: "Any",
}; };
const filterSlice = createSlice({ const filterSlice = createSlice({
@ -31,73 +29,24 @@ const filterSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setAge: (state, action) => { setAge: (state, action) => {
state.age = action.payload; state.from_age = action.payload[0];
state.to_age = action.payload[1];
}, },
setHeight: (state, action) => { setHeight: (state, action) => {
state.height = action.payload; state.from_height = action.payload[0];
state.to_height = action.payload[1];
}, },
setMaritalStatus: (state, action) => { setMaritalStatus: (state, action) => {
state.maritalStatus = action.payload; state.marital_status = Array.isArray(action.payload) ? action.payload : [action.payload];
},
setMotherTongue: (state, action) => {
state.motherTongue = action.payload;
}, },
setReligion: (state, action) => { setReligion: (state, action) => {
state.religion = action.payload; state.religion = Array.isArray(action.payload) ? action.payload : [action.payload];
},
setMatchesWithHoroscope: (state, action) => {
state.matchesWithHoroscope = action.payload;
}, },
setCaste: (state, action) => { setCaste: (state, action) => {
state.caste = action.payload; state.caste = action.payload;
}, },
setSubCaste: (state, action) => { setSubCaste: (state, action) => {
state.subCaste = action.payload; state.sub_caste = 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;
}, },
// universal update // universal update
@ -105,7 +54,16 @@ const filterSlice = createSlice({
return { ...state, ...action.payload }; 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 +71,13 @@ export const {
setAge, setAge,
setHeight, setHeight,
setMaritalStatus, setMaritalStatus,
setMotherTongue,
setReligion, setReligion,
setMatchesWithHoroscope,
setCaste, setCaste,
setSubCaste, setSubCaste,
setStar,
setDasham,
setOccupation,
setAnnualIncome,
setEmployeeType,
setEducation,
setState,
setCountry,
setCitizenship,
setEatingHabits,
setSmokingHabits,
setDrinkingHabits,
setFamilyType,
setFamilyStatus,
setFamilyValue,
updateFilter, updateFilter,
resetFilters, resetFilters,
setPage,
setPaidMemberStatus,
} = filterSlice.actions; } = filterSlice.actions;
export default filterSlice.reducer; export default filterSlice.reducer;

View File

@ -5,49 +5,72 @@ const registrationformSlice = createSlice({
initialState: { initialState: {
personalDetails: { personalDetails: {
name: "", name: "",
mobileNumber: "", mobile: "",
email: "",
gender: "", gender: "",
dob: "",
height: "", height: "",
weight: "", weight: "",
maritalStatus: "", marital_status: "",
religion: "", religion: 1, // Default Hindu
profileFor: "", profile_for: "",
caste: "", caste: 1, // Default Naidu
subCaste: "", sub_caste: "",
willing_to_marry: "",
inter_caste_parents: "",
inter_caste_parents_details: "",
gothram: "", gothram: "",
raasi: "", do_you_speak_telugu: "",
star: "", about_us: "",
bloodGroup: "", known_languages: [],
email: "", mother_language: "",
complexion: "",
physical_status: "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
dob: "",
raasi: "",
star: "",
state: "", state: "",
city: "", city: "",
pincode: "", pincode: "",
profiles: [], profiles: [],
verifiedMobileNumber: "",
}, },
educationalDetails: { educationalDetails: {
collegeName: "", study_field: "",
employeeType: "", education: "",
qualification: "", education_detail: "",
fieldOfStudy: "", college_name: "",
employee_type: "",
occupation: "", occupation: "",
organization: "", occupation_detail: "",
income: "", company_name: "",
workLocation: "", income_currency: "INR",
annual_income: "",
work_country: 1,
work_city: "",
work_state: "",
work_district: "",
address: "",
}, },
familyDetails: { familyDetails: {
fatherName: "", fatherName: "",
fatherOccupation: "", fatherOccupation: "",
motherName: "", motherName: "",
motherOccupation: "", motherOccupation: "",
brotherCount: 0, brotherCount: "",
sisterCount: 0, sisterCount: "",
brothers: [], brothers: [],
sisters: [], sisters: [],
familyStatus: "", familyStatus: "",
nativePlace: "", nativePlace: "",
familyCountry: "",
familyState: "",
familyDistrict: "",
familyCity: "",
address: "",
expectationDetails: "",
willingToGoAbroad: "",
}, },
lifestyleDetails: { lifestyleDetails: {
diets: [], diets: [],
@ -114,14 +137,38 @@ const registrationformSlice = createSlice({
state.partnerPreferences = { ...state.partnerPreferences, ...action.payload }; state.partnerPreferences = { ...state.partnerPreferences, ...action.payload };
}, },
submitForm: (state) => { clearAllStepsFrom: (state, action) => {
console.log("Form Submitted:", { const step = action.payload;
personalDetails: state.personalDetails, if (step <= 2) {
educationalDetails: state.educationalDetails, state.educationalDetails = {
familyDetails: state.familyDetails, study_field: "", education: "", education_detail: "", college_name: "",
lifestyleDetails: state.lifestyleDetails, employee_type: "", occupation: "", occupation_detail: "", company_name: "",
partnerPreferences: state.partnerPreferences, 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 = {
diets: "", hobbies: [], 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 = {
ageRange: "", castes: [], subCastes: [], occupations: [], educations: [],
hobbies: [], annualIncome: "", states: [], districts: [],
};
}
}, },
@ -279,6 +326,7 @@ export const {
updateFamilyDetails, updateFamilyDetails,
updateLifestyleDetails, updateLifestyleDetails,
updatePartnerPreferences, updatePartnerPreferences,
clearAllStepsFrom,
submitForm, submitForm,
preloadDummyProfile, preloadDummyProfile,
} = registrationformSlice.actions; } = registrationformSlice.actions;

View File

@ -1,9 +1,36 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import registerformReducer from "./registrationFormSlice"; import registerformReducer from "./registrationFormSlice";
import filtersReducer from "./filterSlice"; 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({ export const store = configureStore({
reducer: { reducer: {
registerform:registerformReducer, registerform: registerformReducer,
filters:filtersReducer, filters: persistedFiltersReducer,
}, },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
}); });
export const persistor = persistStore(store);

View File

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

View File

@ -19,9 +19,10 @@ const PublicRoutes = () => {
<> <>
<Route element={<HomeLayout />}> <Route element={<HomeLayout />}>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route element={<PublicGuard />}> {/* <Route element={<PublicGuard />}>
<Route path="/registration" element={<StepperForm />} /> </Route> */}
</Route> <Route path="/registration" element={<StepperForm />} />
</Route> </Route>
<Route element={<PublicGuard />}> <Route element={<PublicGuard />}>

34
src/services/chatApi.js Normal file
View File

@ -0,0 +1,34 @@
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;
}
};

View File

@ -0,0 +1,65 @@
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(`unblock_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 (profile_id, status) => {
try {
const response = await axiosInstance.post(API_ENDPOINTS.UPDATE_INTEREST_STATUS, {
profile_id,
status
});
return response.data;
} catch (error) {
console.error("Error updating interest status:", error);
throw error;
}
};

View File

@ -0,0 +1,28 @@
import axiosInstance from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
export const shortlistProfile = async (profileId) => {
const response = await axiosInstance.post(`${API_ENDPOINTS.SHORTLIST_API}?profile_id=${profileId}`);
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to shortlist");
}
return response.data;
};
export const sendInterest = async (profileId) => {
const response = await axiosInstance.post(`interest_send`, {
profile_id: profileId // ✅ sent in request body
});
if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to send interest");
}
return response.data;
};
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;
};

View File

@ -53,7 +53,7 @@ class ErrorBoundary extends Component {
</p> </p>
{/* Show error details in development */} {/* 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"> <details className="mb-4 text-sm text-gray-500">
<summary className="cursor-pointer font-medium"> <summary className="cursor-pointer font-medium">
Error Details (Development) Error Details (Development)

View File

@ -1,7 +1,12 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import path from "path" import path from "path"
import { fileURLToPath } from "url"
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss(),], plugins: [react(), tailwindcss(),],
@ -11,3 +16,4 @@ export default defineConfig({
}, },
}, },
}) })