Compare commits

..

No commits in common. "4ba4ce1e1b58861671f6f00b3c0ccda0c8e9c9d9" and "65bd6c646b5a430f507de160d913e605dad8cff7" have entirely different histories.

64 changed files with 5942 additions and 6103 deletions

9
.gitignore vendored
View File

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

View File

@ -1,60 +1,16 @@
# Thirukalyanam Web Application # React + Vite
A modern matrimonial web application built with React, Vite, and Tailwind CSS. This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Live Application Currently, two official plugins are available:
- [Live Site](https://www.thirukalyanam.amrithaa.net/)
## Features - [@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
- Multi-step Registration Form (Personal, Educational, Family, Lifestyle, Partner Preferences) - [@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
- User Dashboard
- Real-time Chat (WebSocket)
- Profile Filtering and Matching
- Notification System
## Tech Stack ## React Compiler
- **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
## Getting Started 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).
### Prerequisites ## Expanding the ESLint configuration
- Node.js (v18 or higher)
- npm or yarn
### Installation 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.
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,9 +31,6 @@
"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"
}, },
@ -1722,31 +1719,6 @@
"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",
@ -7167,12 +7139,6 @@
"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",
@ -8087,27 +8053,6 @@
"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",
@ -8171,16 +8116,6 @@
} }
} }
}, },
"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",
@ -8270,15 +8205,6 @@
"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",
@ -9143,20 +9069,6 @@
"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,9 +33,6 @@
"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

@ -53,19 +53,5 @@ export const API_ENDPOINTS = {
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",
}; };

View File

@ -1,7 +0,0 @@
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,17 +63,3 @@ 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,9 +5,3 @@ 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.

Before

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

Before

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

@ -1,20 +0,0 @@
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

@ -1,142 +0,0 @@
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,7 +4,6 @@ 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";
@ -20,7 +19,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, useMemo } from "react"; import { useState, useRef, useEffect } 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";
@ -28,15 +27,13 @@ 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, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery } 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" }
@ -161,15 +158,6 @@ 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"));
@ -191,105 +179,11 @@ 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") {
@ -306,7 +200,7 @@ const ProfileHeader = () => {
const deleteAccountMutation = useMutation({ const deleteAccountMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
return await axiosInstance.post(API_ENDPOINTS.DELETE_ACCOUNT); return await axiosInstance.delete(API_ENDPOINTS.DELETE_ACCOUNT);
}, },
onSuccess: (response) => { onSuccess: (response) => {
toast.success(response?.data?.message || "Account deleted successfully"); toast.success(response?.data?.message || "Account deleted successfully");
@ -416,17 +310,10 @@ const ProfileHeader = () => {
{getNavIcon(index)} {getNavIcon(index)}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={ <ListItemText primary={
<div className="flex items-center justify-between w-full pr-4"> <div className="flex items-center justify-between">
<span>{label}</span> {label}
{label === "Notifications" && notificationCount > 0 && ( {label === "Notifications" && notificationCount > 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"> <span className="bg-red-600 text-white text-xs px-2 py-0.5 rounded-full">{notificationCount}</span>
{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>
} /> } />
@ -472,10 +359,7 @@ 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={{ badges={{ "Notifications": notificationCount }}
"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);
@ -485,16 +369,16 @@ const ProfileHeader = () => {
</Box> </Box>
{(auth ? ( {(auth ? (
<Box key="user-menu-box" sx={{ flexGrow: 0 }}> <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={apiProfileImage || profileImage || userimg || "/static/images/avatar/2.jpg" }/> <Avatar sx={{width:"50px", height:"50px"}} src={userimg || "/static/images/avatar/2.jpg" }/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
):( <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" ):( <button 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,6 +52,10 @@ 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) => (
@ -65,44 +69,4 @@ 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 key={index} className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden"> <div 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,10 +1,15 @@
import React, { useEffect } from "react"; import React, { useState } from "react";
import { useInView } from "react-intersection-observer"; import { Crown, Bookmark, CurrencyIcon, Currency, Wallet, Receipt, Sparkles, MoonStar, IdCard, RockingChair, LocateFixed, School, WorkflowIcon } from "lucide-react";
import { RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react"; import CakeIcon from "@mui/icons-material/Cake";
import GroupsIcon from "@mui/icons-material/Groups";
import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import 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";
@ -18,135 +23,260 @@ 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 toast from "react-hot-toast"; import { Button, Fab } from "@mui/material";
import { useSelector, useDispatch } from "react-redux"; import MessageIcon from "@mui/icons-material/Message";
import { updateFilter } from "../../redux/filterSlice"; import PhoneIcon from "@mui/icons-material/Phone";
import { useProfiles } from "../../hooks/useProfiles"; // Profile Card Component
import ProfileCardUI from "../common/ProfileCardUI"; function ProfileCard({ profile }) {
import ProfileCardSkeleton from "../common/ProfileCardSkeleton"; const [isLiked, setIsLiked] = useState(false);
const navigate = useNavigate();
return (
<div
onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200"
>
<div className="relative">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute top-4 left-4 z-10 bg-red-900 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
<motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
>
<Bookmark className="w-4 h-4" />
<span className="text-[12px] font-medium">Shortlist</span>
</motion.button>
<div
classname=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
style={{ height: "300px" }}
>
<img
src={profile.image}
alt="Profile"
className="w-full h-full object-cover bg-gray-200"
style={{
// objectFit:"inherit",
objectPosition: "top",
}}
/>
</div>
<div
className="absolute bottom-0 left-0 right-0 h-25 pointer-events-none"
style={{
background:
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
}}
></div>
<div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-2">
{profile.name}
</h1>
<p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: {profile.id}
</p>
</div>
</div>
<div className="px-4 pt-2 pb-4 flex flex-col gap-2 bg-white">
<div className="flex items-center gap-2">
<VisibilityIcon />
<span className="text-[14px] text-gray-900">{profile.lastseen}</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<CakeIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
{profile.age} yr
</span>
</div>
<div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
{profile.height} cm
</span>
</div>
<div className="flex items-center gap-2">
<Receipt className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
5 - 10 LPA
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<MoonStar className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
Aries
</span>
</div>
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
Scorpio
</span>
</div>
<div className="flex items-center gap-2">
<IdCard className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
Bramin
</span>
</div>
</div>
<div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900">
{profile.location}
</span>
</div>
<div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<button
onClick={(e) => {
e.stopPropagation();
// your decline logic
}} className="gap-2 px-3 w-[fit-content] bg-red-50 border-1 border-red-200
font-400 text-base py-1.5 rounded-[20px] shadow-md text-[14px]
hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95">
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Decline
</button>
<button
className="w-[fit-content] bg-green-50 border-1 border-green-200 font-400 text-base text-[14px]
rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300
transform hover:scale-105 flex items-center justify-center"
onClick={(e) =>{
e.stopPropagation();
setIsLiked(!isLiked);
} }
>
{isLiked ? (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#EF4444"/>
</svg>
) : (
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
Interest
</button>
</div>
{/* <div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<div className="flex gap-2">
<Fab size="medium" color="primary" aria-label="add">
<MessageIcon />
</Fab>
<Fab size="medium" color="secondary" aria-label="add">
<PhoneIcon />
</Fab>
</div>
<Button
variant="contained"
color="#f5fbff"
onClick={(e) => {
e.stopPropagation();
navigate(`/profile-details/${profile.id}`);
}}
sx={{
color: "#000000",
background: "#f5fbff",
fontWeight: "600",
borderRadius: "30px",
}}
>
View Details
</Button>
</div> */}
</div>
</div>
);
}
// 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 dispatch = useDispatch(); const [selectedTab, setSelectedTab] = useState("your-matches");
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: "all_matches", id: "your-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: "shorlisted_by_you", id: "shortlisted-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: "shorlisted_you", id: "shortlisted-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_matches", id: "location",
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_matches", id: "education",
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_matches", id: "job",
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",
@ -154,6 +284,89 @@ useEffect(() => {
}, },
]; ];
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 (
@ -168,7 +381,7 @@ useEffect(() => {
<div className="w-full md:w-80"> <div className="w-full md:w-80">
<div <div
className="relative rounded-[10px] border border-gray-200 bg-white my-6 className="rounded-[10px] border border-gray-200 bg-white my-6
shadow-lg h-[400px] md:h-[600px] overflow-y-auto md:sticky md:top-[150px]" shadow-lg h-[400px] md:h-[600px] overflow-y-auto md:sticky md:top-[150px]"
> >
@ -193,9 +406,7 @@ useEffect(() => {
</h2> </h2>
)} )}
<div <div
onClick={() => { onClick={() => setSelectedTab(tab.id)}
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"
@ -261,58 +472,19 @@ useEffect(() => {
{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">
<div className="relative cursor-pointer" onClick={() => {
if (isPaidMember) {
navigate("/horoscoper-generate");
} else {
toast.error("Star Matching is locked for free members");
}
}}>
<img <img
src={horoscope} src={horoscope}
className={!isPaidMember ? "opacity-50 blur-[1px]" : ""} onClick={() => {
navigate("/horoscoper-generate");
}}
/> />
{!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">
{isLoading && !isFetchingNextPage ? ( {profiles.map((profile) => (
[...Array(6)].map((_, i) => <ProfileCardSkeleton key={i} />) <ProfileCard key={profile.id} profile={profile} />
) : 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,23 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState } 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>{` <style jsx global>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -1,4 +1,4 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState } 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,84 +10,141 @@ 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"; const MatchingList = () => {
import { useMutation, useQueryClient } from "@tanstack/react-query"; const swiperRef = useRef(null);
import toast from "react-hot-toast"; const navigate = useNavigate();
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
// 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 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
@ -95,110 +152,140 @@ 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/${id}`)} onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer" className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-2 border-gray-200"
> >
{/* IMAGE SECTION */} {/* Profile Image Section */}
<div className="relative"> <div className="relative">
<img {/* Premium Badge */}
src={image} {profile.isPremium && (
alt="profile" <motion.div
className="w-full h-[320px] object-cover" initial={{ scale: 0 }}
onError={(e) => { animate={{ scale: 1 }}
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"; 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" />
{isPremium && ( </motion.div>
<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 {/* Shortlist 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"}`} <motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!shortlistMutation.isPending) { // shortlist logic
shortlistMutation.mutate(id);
}
}} }}
> >
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} /> <Bookmark className="w-4 h-4" />
{shortlistMutation.isPending ? "..." : "Shortlist"} <span className="text-[12px] font-medium">Shortlist</span>
</div> </motion.button>
</div>
{/* 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=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
<h2 className="text-center text-[22px] font-semibold mb-1"> style={{ height: "300px" }}
{name} >
</h2> <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"
/> */}
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8"> {/* White Gradient Overlay at bottom of image */}
<p>ID: {idNumber}</p> <div
<p className="flex items-center gap-0.5"> className="absolute bottom-0 left-0 right-0 h-25 pointer-events-none"
<Eye size={12} /> {lastSeen} 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> </p>
</div> </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>
<div className="flex gap-4 mt-[15px] justify-center"> {/* Stats and Follow Section */}
<button <div
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" : ""}`} className="px-4 pt-[-2px] pb-4 flex flex-col gap-2 "
onClick={(e) => { style={{
e.stopPropagation(); background: "rgb(255, 255, 255)",
if (!declineMutation.isPending) declineMutation.mutate(id);
}} }}
disabled={declineMutation.isPending}
> >
<X size={18} /> {declineMutation.isPending ? "..." : "Decline"} <div className="flex items-center gap-4">
</button> <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>
<button <div className="flex items-center gap-2">
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" : ""}`} <AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
onClick={(e) => { <span className="text-[14px] font-600 text-gray-900">
e.stopPropagation(); Height: {profile.height}
if (!interestMutation.isPending) interestMutation.mutate(id); </span>
}} </div>
disabled={interestMutation.isPending}
>
<Heart size={18} /> {interestMutation.isPending ? "..." : "Interest"} </div>
</button>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<AccountBalanceWalletIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.salary}
</span>
</div>
<div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.location}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<TempleHinduIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.caste}
</span>
</div>
<div className="flex items-center gap-2">
<img src={Image} alt="" className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.zodiac1}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<img src={Image1} alt="" className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.zodiac2}
</span>
</div>
</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
@ -254,8 +341,8 @@ const MatchingList = ({ matches }) => {
}} }}
className="pb-16" className="pb-16"
> >
{displayProfiles.map((profile, index) => ( {profiles.map((profile) => (
<SwiperSlide key={profile.id || index}> <SwiperSlide key={profile.id}>
<ProfileCard profile={profile} /> <ProfileCard profile={profile} />
</SwiperSlide> </SwiperSlide>
))} ))}
@ -296,10 +383,7 @@ const MatchingList = ({ matches }) => {
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={() => { onClick={() => navigate("/matches")}
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>
@ -307,7 +391,7 @@ const MatchingList = ({ matches }) => {
</div> </div>
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style>{` <style jsx global>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -5,13 +5,48 @@ 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">
@ -35,18 +70,50 @@ const MatrimonyArticles = ({ articles }) => {
loop={true} loop={true}
className="mySwiper" className="mySwiper"
> >
{displayArticles.map((item) => ( {articleData.map((item, index) => (
<SwiperSlide key={item.id}> <SwiperSlide key={index}>
<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.image} src={item.img}
className="w-full h-full object-cover" className="w-full h-88 object-cover"
alt={`Article ${item.id}`} alt={item.title}
/> />
</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>

View File

@ -1,19 +1,7 @@
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"
import { useNavigate } from 'react-router-dom'; const MembershipCard = ()=>{
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 (
<> <>
@ -31,31 +19,50 @@ 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">{offerPercentage}% OFF</span> on paid membership! Get up to <span className="text-red-600 font-bold">58% 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">
{points.map((point, index) => { <div className="flex items-start gap-4">
const Icon = icons[index % icons.length];
return (
<div key={index} className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1"> <div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<Icon className="w-5 h-5 text-gray-700" /> <Phone className="w-5 h-5 text-gray-700" />
</div> </div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium"> <p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
{point} Call/WhatsApp matches
</p>
</div>
<div className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<MessageCircle className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
Unlimited messages
</p>
</div>
<div className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<ThumbsUp className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
Higher chances of response
</p>
</div>
<div className="flex items-start gap-4">
<div className="w-4 h-4 flex-shrink-0 mt-0 py-1">
<Eye className="w-5 h-5 text-gray-700" />
</div>
<p className="text-[16px] lg:text-[16px] text-gray-900 font-medium">
View and match horoscopes
</p> </p>
</div> </div>
);
})}
</div> </div>
{/* CTA Button */} {/* CTA Button */}
<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">
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>
@ -79,3 +86,4 @@ return (
</>); </>);
}; };
export default MembershipCard export default MembershipCard

View File

@ -1,88 +1,116 @@
import { useRef, useState, useEffect } from 'react'; import { useRef, useState } 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 { import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react';
Crown, import CakeIcon from "@mui/icons-material/Cake";
Bookmark, import HeightIcon from "@mui/icons-material/Height";
X, import GroupsIcon from "@mui/icons-material/Groups";
ChevronLeft, import TempleHinduIcon from "@mui/icons-material/TempleHindu";
ChevronRight, import SchoolIcon from "@mui/icons-material/School";
Heart, import LocationOnIcon from "@mui/icons-material/LocationOn";
Eye, import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
} from 'lucide-react'; import profilebg from "../../assets/images/profilebg.jpg";
// 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"; const NewJoinedProfile = () => {
import { useMutation, useQueryClient } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi"; const swiperRef = useRef(null);
const navigate = useNavigate();
// Sample profile data
const profiles = [
{
id: 1,
name: 'Selva Kumar . R',
userId: 'TK52586A',
lastSeen: '14 Nov 25',
age: 23,
height: '5\'2"',
religion: 'Hindu / Agamudayar / Thuluva Vellal',
education: 'BCA, Data Analyst',
location: 'Vellore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 2,
name: 'Priya Sharma',
userId: 'TK52587B',
lastSeen: '15 Nov 25',
age: 25,
height: '5\'4"',
religion: 'Hindu / Brahmin / Iyer',
education: 'MBA, Marketing Manager',
location: 'Chennai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 3,
name: 'Rahul Venkat',
userId: 'TK52588C',
lastSeen: '16 Nov 25',
age: 28,
height: '5\'10"',
religion: 'Hindu / Mudaliar / Arcot',
education: 'B.Tech, Software Engineer',
location: 'Bangalore, Karnataka',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 4,
name: 'Aishwarya Reddy',
userId: 'TK52589D',
lastSeen: '17 Nov 25',
age: 26,
height: '5\'5"',
religion: 'Hindu / Reddy / Telangana',
education: 'CA, Chartered Accountant',
location: 'Hyderabad, Telangana',
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 5,
name: 'Karthik Mohan',
userId: 'TK52590E',
lastSeen: '18 Nov 25',
age: 27,
height: '5\'8"',
religion: 'Hindu / Nadar / Tamil',
education: 'M.Tech, Civil Engineer',
location: 'Madurai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 6,
name: 'Divya Lakshmi',
userId: 'TK52591F',
lastSeen: '19 Nov 25',
age: 24,
height: '5\'3"',
religion: 'Hindu / Pillai / Tamil',
education: 'B.Com, HR Executive',
location: 'Coimbatore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&h=500&fit=crop',
isPremium: true
}
];
// Profile Card Component
const ProfileCard = ({ profile }) => { const ProfileCard = ({ profile }) => {
const navigate = useNavigate(); const [isLiked, setIsLiked] = useState(false);
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
@ -90,110 +118,173 @@ 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/${id}`)} onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full rounded-[28px] overflow-hidden bg-white shadow-md cursor-pointer" className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200"
> >
{/* IMAGE SECTION */}
<div className="relative"> <div className="relative">
<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-orange-500 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>
)} )}
<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();
if (!shortlistMutation.isPending) {
shortlistMutation.mutate(id);
}
}} }}
> >
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} /> <Bookmark className="w-4 h-4" />
{shortlistMutation.isPending ? "..." : "Shortlist"} <span className="text-[12px] font-medium">Shortlist</span>
</div> </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>
{/* 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="absolute bottom-0 left-0 right-0 h-35 pointer-events-none"
<h2 className="text-center text-[22px] font-semibold mb-1"> style={{
{name} background:
</h2> "linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
}}
></div>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8"> <div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900">
<p>ID: {idNumber}</p> <h1 className="text-[18px] text-green-900 font-bold mb-2">
<p className="flex items-center gap-0.5"> {profile.name}
<Eye size={12} /> {lastSeen} </h1>
<p className="text-[14px] text-gray-700 leading-relaxed">
Matrimony ID: {profile.userId}
</p> </p>
</div> </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>
<div className="flex gap-4 mt-[15px] justify-center"> <div
{/* <button className="px-4 pt-[-2px] pb-4 flex flex-col gap-2"
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" : ""}`} style={{ background: "rgb(255, 255, 255)" }}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<CakeIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
Age : {profile.age}
</span>
</div>
<div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
Height: {profile.height}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<GroupsIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.religion}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<SchoolIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.education}
</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-600 text-gray-900">
{profile.location}
</span>
</div>
</div>
<div className="flex gap-3 my-2 justify-between w-full px-[0px]">
<button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!declineMutation.isPending) declineMutation.mutate(id);
}} }}
disabled={declineMutation.isPending} 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"
> >
<X size={18} /> {declineMutation.isPending ? "..." : "Decline"} <svg
</button> */} 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 <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" : ""}`} 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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!interestMutation.isPending) interestMutation.mutate(id); setIsLiked(!isLiked);
}} }}
disabled={interestMutation.isPending}
> >
<Heart size={18} /> {interestMutation.isPending ? "..." : "Interest"} {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> </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 (
<> <>
@ -210,10 +301,10 @@ const NewJoinedProfile = ({ profiles }) => {
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-[#000000] sm:text-[22px] lg:text-[24px] font-semibold mb-3"> <h1 className="text-[20px] text-[#034E08] sm:text-[22px] lg:text-[24px] font-bold mb-3">
New Joined New Joined
</h1> </h1>
<p className="text-gray-900 text-[12px]">Find your perfect match today</p> <p className="text-gray-600 text-[12px]">Find your perfect match today</p>
</motion.div> </motion.div>
{/* Swiper Container */} {/* Swiper Container */}
@ -228,6 +319,10 @@ const NewJoinedProfile = ({ profiles }) => {
disableOnInteraction: false, disableOnInteraction: false,
pauseOnMouseEnter: true pauseOnMouseEnter: true
}} }}
pagination={{
clickable: true,
dynamicBullets: true
}}
loop={true} loop={true}
speed={800} speed={800}
breakpoints={{ breakpoints={{
@ -246,8 +341,8 @@ const NewJoinedProfile = ({ profiles }) => {
}} }}
className="pb-16" className="pb-16"
> >
{displayProfiles.map((profile, index) => ( {profiles.map((profile) => (
<SwiperSlide key={profile.id || index}> <SwiperSlide key={profile.id}>
<ProfileCard profile={profile} /> <ProfileCard profile={profile} />
</SwiperSlide> </SwiperSlide>
))} ))}
@ -288,18 +383,14 @@ const NewJoinedProfile = ({ profiles }) => {
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 Matches View All
</motion.button> </motion.button>
</motion.div> </motion.div>
</div> </div>
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style>{` <style jsx global>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -9,7 +9,8 @@ import { useNavigate } from "react-router-dom";
import AstroChatUI from "./AstroChatUI"; import AstroChatUI from "./AstroChatUI";
import MembershipCard from "./MembershipCard"; import MembershipCard from "./MembershipCard";
const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember }) => { const ProfileCompletion = () => {
const [completeness, setCompleteness] = useState(30);
const navigate = useNavigate(); const navigate = useNavigate();
const cards = [ const cards = [
@ -94,7 +95,7 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
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"
> >
{percentage}% {completeness}%
</motion.span> </motion.span>
</div> </div>
@ -102,7 +103,7 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
<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: `${percentage}%` }} animate={{ width: `${completeness}%` }}
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"
/> />
@ -122,7 +123,6 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
</Box> */} </Box> */}
<AstroChatUI/> <AstroChatUI/>
<div className="my-4 rounded-2xl bg-green-50 border border-1 border-green-200 p-4 "> <div className="my-4 rounded-2xl bg-green-50 border border-1 border-green-200 p-4 ">
<motion.div <motion.div
variants={container} variants={container}
@ -130,16 +130,15 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
animate="show" animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"
> >
{/* NOTE: The cards are static for now. To make them dynamic,
the `missingDetails` prop should be an array of objects to map over. */}
{cards.map((card, index) => ( {cards.map((card, index) => (
<div <div
key={card.id}
onClick={() => navigate(card.url)} onClick={() => navigate(card.url)}
className=" border border-1 border-red-50 bg-white rounded-3xl hover:bg-red-50 hover:border-2 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 }}
@ -164,12 +163,15 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
{card.title} {card.title}
</motion.h3> </motion.h3>
</div> </div>
))} ))}
</motion.div> </motion.div>
<MembershipCard becomePaidMember={becomePaidMember} /> <MembershipCard/>
</div>
</div>
</div> </div>
{/* Additional Info Section */} {/* Additional Info Section */}
{/* <motion.div {/* <motion.div

View File

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

View File

@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef } from "react";
import { import {
Heart, Heart,
X, X,
ChevronRight, ChevronRight,
SkipForward,
Bookmark, Bookmark,
MessageCircle, MessageCircle,
Ban, Ban,
@ -18,10 +19,8 @@ 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 = ({ data }) => { const MatrimonyProfile = () => {
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);
@ -29,17 +28,26 @@ const MatrimonyProfile = ({ data }) => {
const mainSwiperRef = useRef(null); const mainSwiperRef = useRef(null);
const modalSwiperRef = useRef(null); const modalSwiperRef = useRef(null);
if (!data) return null; const profile = {
name: "Sudharshan M",
const profile = data.profile; id: "M8355880",
const personal = data.personalDetails; verified: true,
const family = data.familyDetails; lastSeen: "Last seen few hour ago",
const education = data.educationalDetails; age: "30 yrs",
const lifestyle = data.lifestyleDetails; height: "5'5\"",
caste: "Brahmin",
const profileImages = personal.images && personal.images.length > 0 education: "Engineer - Non IT",
? personal.images location: "Chennai",
: [profile.profile_picture || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"]; maritalStatus: "Never Married",
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);
@ -50,31 +58,6 @@ const MatrimonyProfile = ({ data }) => {
}, 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
@ -93,12 +76,16 @@ const MatrimonyProfile = ({ data }) => {
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"
> >
{profileImages.map((img, idx) => ( {profile.images.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"
@ -108,21 +95,35 @@ const MatrimonyProfile = ({ data }) => {
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>
@ -143,7 +144,7 @@ const MatrimonyProfile = ({ data }) => {
/> />
</svg> </svg>
</div> </div>
<span className="text-[#034E08] font-semibold">{profile.approved ? "ID verified" : "Pending Verification"}</span> <span className="text-[#034E08] font-semibold">ID verified</span>
</div> </div>
<div className="relative"> <div className="relative">
<button <button
@ -160,16 +161,10 @@ const MatrimonyProfile = ({ data }) => {
</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 <button className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3">
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 <button className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3">
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">
@ -187,47 +182,44 @@ const MatrimonyProfile = ({ data }) => {
{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.member_id} | {profile.last_seen_at} {profile.id} | {profile.lastSeen}
</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 items-center"> <p className="flex flex-wrap gap-2">
<span className="font-semibold">{profile.maritalStatus}</span>
<span></span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
Profile created by {personal.profile_for || "N/A"} {profile.createdBy}
</span> </span>
{(personal.age || profile.age) && (
<>
<span></span> <span></span>
<span className="text-sm">{personal.age || profile.age} yrs</span> <span>{profile.age}</span>
</>
)}
{(profile.religion || profile.caste || profile.sub_caste || profile.college_name) && (
<>
<span></span> <span></span>
<span className="text-sm"> <span>{profile.height}</span>
{[ <span></span>
safeVal(profile.religion, 'religion_name'), <span>{profile.caste}</span>
safeVal(profile.caste, 'caste_name'), </p>
safeVal(profile.sub_caste, 'sub_caste_name'), <p>
profile.college_name <span className="font-semibold">{profile.education}</span>
].filter(v => v !== "N/A" && v !== undefined).join(" / ")} <span> </span>
</span> <span>{profile.location}</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
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" // onClick={()=>{
> // 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 {/* <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">
onClick={handleSendInterest} <SkipForward className="w-5 h-5" /> Skip
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> */}
> <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>
@ -235,7 +227,7 @@ const MatrimonyProfile = ({ data }) => {
</div> </div>
</div> </div>
{/* Image Modal */} {/* Image Modal with Swiper */}
{isModalOpen && ( {isModalOpen && (
<div <div
style={{ backdropFilter: "blur(5px)" }} style={{ backdropFilter: "blur(5px)" }}
@ -251,6 +243,7 @@ const MatrimonyProfile = ({ data }) => {
<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" }}
@ -278,22 +271,20 @@ const MatrimonyProfile = ({ data }) => {
}} }}
className="h-full w-full" className="h-full w-full"
> >
{profileImages.map((img, idx) => ( {profile.images.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>
@ -304,13 +295,24 @@ const MatrimonyProfile = ({ data }) => {
</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">
<div className="swiper-pagination-modal text-lg font-semibold"></div>
<div className="text-right">
<h3 className="font-bold text-lg">{profile.name}</h3> <h3 className="font-bold text-lg">{profile.name}</h3>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{profile.member_id} | Profile created by {personal.profile_for} {profile.id} | {profile.createdBy}
</p>
<p className="text-sm">
{profile.age} {profile.height} {profile.caste} BE
{profile.education} {profile.location}
</p> </p>
</div> </div>
</div>
</div>
{/* Thumbnail Swiper */}
<Swiper <Swiper
modules={[Thumbs]} modules={[Thumbs]}
watchSlidesProgress watchSlidesProgress
@ -319,28 +321,25 @@ const MatrimonyProfile = ({ data }) => {
slidesPerView={5} slidesPerView={5}
className="mb-2" className="mb-2"
> >
{profileImages.map((img, idx) => ( {profile.images.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">Like this member?</p> <p className="text-sm text-gray-600 mb-2">
<button Like this member?
onClick={handleSendInterest} </p>
className="bg-[#034E08] text-white px-8 py-2 rounded-full hover:bg-orange-700 transition-colors font-semibold" <button 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>
@ -355,8 +354,16 @@ const MatrimonyProfile = ({ data }) => {
<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 className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20"> <svg
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" /> className="w-5 h-5 text-[#A70710]"
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>
@ -364,94 +371,175 @@ const MatrimonyProfile = ({ data }) => {
<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">Name</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">{profile.name}</span> <span className="ml-3 text-gray-900">30 Years and 8 months</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">{profile.height ? `${profile.height} ft` : "N/A"}</span> <span className="ml-3 text-gray-900">5'5"</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">{profile.weight ? `${profile.weight} Kg` : "N/A"}</span> <span className="ml-3 text-gray-900">97 Kg</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">Body Type</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.religion || safeVal(profile.religion, 'religion_name')}</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>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Profile Created By</span> <span className="text-gray-600 w-40">Profile Created By</span>
<span className="text-gray-400">:</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> <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 className="flex">
<span className="text-gray-600 w-40">Religion</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Hindu</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">{personal.caste || safeVal(profile.caste, 'caste_name')}</span> <span className="ml-3 text-gray-900">Brahmin - Iyer</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Sub Caste</span> <span className="text-gray-600 w-40">Subcaste</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.sub_caste || safeVal(profile.sub_caste, 'sub_caste_name')}</span> <span className="ml-3 text-gray-900">Brahacharmam</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Gothram</span> <span className="text-gray-600 w-40">Gothra(m)</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.gothram || safeVal(profile.gothram, 'gothram_name')}</span> <span className="ml-3 text-gray-900">Kashyapa / Kaashyapa</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Rasi</span> <span className="text-gray-600 w-40">Dosha(m)</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.raasi || safeVal(profile.raasi, 'raasi_name')}</span> <span className="ml-3 text-gray-900">Don't know</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">Birth Star</span> <span className="text-gray-600 w-40">Income</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.star || safeVal(profile.star, 'star_name')}</span> <span className="ml-3 text-gray-900"> 4 - 5 Lakhs</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Known Languages</span> <span className="text-gray-600 w-40">Education</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{personal.known_languages || "N/A"}</span> <span className="ml-3 text-gray-900">BE</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Speaks Telugu</span> <span className="text-gray-600 w-40">Occupation</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</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> <span className="ml-3 text-gray-900">Engineer - Non IT</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>
@ -460,7 +548,11 @@ const MatrimonyProfile = ({ data }) => {
<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 className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20"> <svg
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>
@ -469,44 +561,104 @@ const MatrimonyProfile = ({ data }) => {
<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">Father Name</span> <span className="text-gray-600 w-40">Parents</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.father_name || profile.father_name || "N/A"}</span> <span className="ml-3 text-gray-900">
Father Passed Away, Mother is a Home Maker
</span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Father Occupation</span> <span className="text-gray-600 w-40">Ancestral Origin</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.father_occupation || profile.father_occupation || "N/A"}</span> <span className="ml-3 text-gray-900">Rameshwaram</span>
</div> </div>
<div className="flex">
<span className="text-gray-600 w-40">Mother Name</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.mother_name || profile.mother_name || "N/A"}</span>
</div> </div>
<div className="flex"> {/* Contact Information Section */}
<span className="text-gray-600 w-40">Mother Occupation</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_occupation || profile.mother_occupation || "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> </div>
<div className="flex"> <h3 className="font-semibold text-lg">Contact Information</h3>
<span className="text-gray-600 w-40">Siblings</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.brother_count || profile.brother_count} Brothers, {family.sister_count || profile.sister_count} Sisters</span>
</div> </div>
<div className="flex">
<span className="text-gray-600 w-40">Family Type</span> <div className="p-5 space-y-3 text-sm">
<div className="flex items-center">
<span className="text-gray-600 w-40">Mobile Number</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.family_status || safeVal(profile.family_type, 'family_type_name') || "N/A"}</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 className="flex">
<span className="text-gray-600 w-40">Settled</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.settled || "N/A"}</span>
</div> </div>
<div className="flex"> </div>
<span className="text-gray-600 w-40">Native Place</span> </div>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{family.native_place || profile.native_place || "N/A"}</span> {/* About Myself Section */}
<div className="my-8">
<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 className="p-5 space-y-4 text-sm">
<div>
<h4 className="font-semibold text-gray-900 mb-2">
About Sudharshan M
</h4>
<p className="text-gray-700 leading-relaxed">
I am making this profile for my brother. He completed his
bachelor's degree and is now working as a project engineer -
non IT. We belong to a middle class, nuclear family with
traditional values, currently settled in Chennai.
</p>
</div>
<div>
<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>
@ -514,7 +666,11 @@ const MatrimonyProfile = ({ data }) => {
<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 className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20"> <svg
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>
@ -523,108 +679,55 @@ const MatrimonyProfile = ({ data }) => {
<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">Diet</span> <span className="text-gray-600 w-40">Cuisine</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">
{lifestyle.dasa_years || "0"} Years, {lifestyle.dasa_months || "0"} Months, {lifestyle.dasa_days || "0"} Days Chinese, North Indian, South Indian
</span> </span>
</div> </div>
<div className="flex"> <div className="flex">
<span className="text-gray-600 w-40">Age</span> <span className="text-gray-600 w-40">Books</span>
<span className="text-gray-400">:</span> <span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">{(personal.age || profile.age || lifestyle.age) ? `${personal.age || profile.age || lifestyle.age} Years` : "N/A"}</span> <span className="ml-3 text-gray-900">
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">
{lifestyle.hobbies && lifestyle.hobbies.length > 0 ? lifestyle.hobbies.join(", ") : "N/A"} Anime, Comedy, Sci-Fi
</span> </span>
</div> </div>
<div className="flex">
<span className="text-gray-600 w-40">Sports</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Yoga / Meditation</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Smoking Habits</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Doesn't Smoke</span>
</div>
<div className="flex">
<span className="text-gray-600 w-40">Drinking Habits</span>
<span className="text-gray-400">:</span>
<span className="ml-3 text-gray-900">Doesn't Drink</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{/* Educational Details Section */}
<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="bg-white p-2 rounded-full">
<svg 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" />
</svg>
</div>
<h3 className="font-semibold text-lg">Educational Details</h3>
</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>
); );
}; };

View File

@ -1,29 +1,24 @@
import React from 'react'; import React from 'react';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
const PartnerPreferences = ({ data }) => { const PartnerPreferences = () => {
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 Groom's Age", value: pref.preferred_age_range || "Any", match: matchDetails.age }, { label: "Preferred Bride's Age", value: "22-29 yrs", match: true },
{ 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 Height", value: "5'0\" - 5'5\"", match: false },
{ label: "Preferred Marital Status", value: pref.preferred_marital_statuses?.join(", ") || "Any", match: matchDetails.marital_status }, { label: "Preferred Marital Status", value: "Never Married", match: true },
{ label: "Preferred Mother Tongue", value: pref.preferred_mother_tongues?.join(", ") || "Any", match: matchDetails.mother_tongue }, { label: "Preferred Mother Tongue", value: "Tamil", match: true },
{ label: "Preferred Education", value: pref.preferred_educations?.join(", ") || "Any", match: matchDetails.education }, { label: "Preferred Physical Status", value: "Normal", match: true },
{ label: "Preferred Employee Type", value: pref.preferred_employee_types?.join(", ") || "Any", match: true }, // Not in matchDetails? { label: "Preferred Eating Habits", value: "Vegetarian", match: false },
{ label: "Preferred Smoking Habits", value: "Doesn't Matter", match: true },
{ label: "Preferred Drinking Habits", value: "Doesn't Matter", match: true },
]; ];
const religiousPreferences = [ const religiousPreferences = [
{ label: "Preferred Caste", value: pref.preferred_castes?.join(", ") || "Any", match: matchDetails.caste }, { label: "Preferred Religion", value: "Hindu", match: true },
{ label: "Preferred Sub-caste", value: pref.preferred_sub_castes?.join(", ") || "Any", match: matchDetails.sub_caste }, { label: "Preferred Caste", value: "Brahmin - Iyer", match: false },
{ label: "Preferred State", value: pref.preferred_states?.join(", ") || "Any", match: true }, { label: "Preferred Subcaste", value: "Any", match: false },
{ label: "Preferred City", value: pref.preferred_districts?.join(", ") || "Any", match: true }, { label: "Preferred Star", value: "Any", match: true },
{ label: "Preferred Occupation", value: pref.preferred_occupations?.join(", ") || "Any", match: matchDetails.occupation }, { label: "Preferred Dosham", value: "No Dosham", match: true },
{ label: "Preferred Annual Income", value: pref.preferred_annual_income || "Any", match: matchDetails.annual_income },
]; ];
const PreferenceItem = ({ label, value, match }) => ( const PreferenceItem = ({ label, value, match }) => (
@ -51,7 +46,7 @@ const PartnerPreferences = ({ data }) => {
<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>
Partner Preferences His Partner Preferences
<span className="text-pink-400"></span> <span className="text-pink-400"></span>
</h1> </h1>
</div> </div>
@ -61,31 +56,38 @@ const PartnerPreferences = ({ data }) => {
<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={data.my_profile || "https://api.dicebear.com/7.x/avataaars/svg?seed=male1"} src="https://api.dicebear.com/7.x/avataaars/svg?seed=male1"
alt="Your Profile" alt="Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100 object-cover" className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100"
/> />
<div> <div>
<p className="text-gray-600 text-sm mb-1">Overall Match Score</p> <p className="text-gray-600 text-sm mb-1">You match</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">
{overallMatch}<span className="text-[#034E08]">%</span> 14<span className="text-[#034E08]">/20</span>
</p> </p>
<p className="text-xs text-gray-500">of preferences match</p> <p className="text-xs text-gray-500">of his preferences</p>
</div> </div>
</div> </div>
<img <img
src={data.profile.profile_picture || "https://api.dicebear.com/7.x/avataaars/svg?seed=female1"} src="https://api.dicebear.com/7.x/avataaars/svg?seed=female1"
alt="Partner Profile" alt="Your Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100 object-cover" className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100"
/> />
</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 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>
<div className="space-y-1 p-6"> <div className="space-y-1 p-6">
{basicPreferences.map((pref, index) => ( {basicPreferences.map((pref, index) => (
@ -94,10 +96,17 @@ const PartnerPreferences = ({ data }) => {
</div> </div>
</div> </div>
{/* Other Preferences Section */} {/* Religious 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">Professional & Location</h2> <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>
<div className="space-y-1 p-6"> <div className="space-y-1 p-6">
{religiousPreferences.map((pref, index) => ( {religiousPreferences.map((pref, index) => (
@ -106,7 +115,6 @@ const PartnerPreferences = ({ data }) => {
</div> </div>
</div> </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,222 +1,73 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { Heart, X, Crown, Bookmark, Eye, Clock, ChevronLeft, ChevronRight } from "lucide-react"; import { Heart, X, Crown, Bookmark, Eye } from "lucide-react";
import { Swiper, SwiperSlide } from "swiper/react"; // Import your images
import { Navigation, Pagination } from "swiper/modules"; import Profile1 from "../../assets/images/bride1.jpg";
import "swiper/css"; import Profile2 from "../../assets/images/bride2.jpg";
import "swiper/css/navigation"; import Profile3 from "../../assets/images/bride3.jpg";
import "swiper/css/pagination"; import Profile4 from "../../assets/images/bride4.jpg";
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);
useEffect(() => { export default function ProfileCard() {
setIsShortlisted(profile?.is_shortlisted === 1); // Sample data for multiple cards with image paths
}, [profile?.is_shortlisted]); const profiles = [
{
const shortlistMutation = useMutation({ id: 1,
mutationFn: shortlistProfile, name: "Jerome Bell",
onMutate: () => { idNumber: "KI2847596",
setIsShortlisted((prev) => !prev); lastSeen: "4 Nov 2025",
salary: "5-10",
age: "22 yrs",
height: "5'2\"",
location: "Chennai",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile1,
}, },
onSuccess: (data) => { {
toast.success(data.message || "Profile shortlisted successfully."); id: 2,
queryClient.invalidateQueries(); name: "Neha Singh",
idNumber: "KI2847597",
lastSeen: "5 Nov 2025",
salary: "8-12",
age: "26 yrs",
height: "5'6\"",
location: "hyderabad",
caste: "Brahmin",
zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile2,
}, },
onError: (error) => { {
setIsShortlisted(profile?.is_shortlisted === 1); id: 3,
toast.error(error.message || "Failed to update shortlist status."); name: "Priya Sharma",
} idNumber: "KI2847598",
}); lastSeen: "3 Nov 2025",
salary: "6-11",
const interestMutation = useMutation({ age: "24 yrs",
mutationFn: sendInterest, height: "5'4\"",
onSuccess: (data) => { location: "Mumbai",
toast.success(data.message || "Interest sent successfully."); caste: "Brahmin",
queryClient.invalidateQueries(); zodiac1: "Aries",
zodiac2: "Scorpio",
image: Profile3,
}, },
onError: (error) => { {
toast.error(error.message || "Failed to send interest."); id: 4,
} name: "Kavya Iyer",
}); idNumber: "KI2847599",
lastSeen: "2 Nov 2025",
const declineMutation = useMutation({ salary: "7-10",
mutationFn: declineProfile, age: "23 yrs",
onSuccess: (data) => { height: "5'3\"",
toast.success(data.message || "Profile declined."); location: "Bangalore",
queryClient.invalidateQueries(); caste: "Brahmin",
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">
@ -233,79 +84,78 @@ export default function ProfileCard({ profiles }) {
</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] relative px-2 md:px-12"> <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">
{!showTimer ? ( {profiles.map((profile) => (
<> <div
{/* Custom Navigation Arrows */} key={profile.id}
<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"> className="w-full rounded-[28px] overflow-hidden bg-white shadow-md"
<ChevronLeft size={24} /> >
</button> {/* IMAGE SECTION */}
<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"> <div className="relative">
<ChevronRight size={24} /> <img
src={profile.image}
alt="profile"
className="w-full h-[320px] object-cover"
/>
<div className="absolute top-4 left-4 w-9 h-9 rounded-full bg-[#8b0000] flex items-center justify-center">
<Crown size={18} color="#fff" />
</div>
<div className="absolute top-4 right-4 px-3 py-1.5 rounded-[20px] bg-white flex items-center gap-1.5 text-[13px] font-medium shadow-lg">
<Bookmark size={14} /> Shortlist
</div>
</div>
{/* CONTENT */}
<div className="px-4 py-4 -mt-[60px] bg-white/65 backdrop-blur-[25px] rounded-t-[15px] shadow-[0_-10px_30px_rgba(0,0,0,0.15)] relative z-[2]">
<h2 className="text-center text-[22px] font-semibold mb-1">
{profile.name}
</h2>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8">
<p>ID: {profile.idNumber}</p>
<p className="flex items-center gap-0.5">
<Eye size={12} /> {profile.lastSeen}
</p>
</div>
<div className="flex flex-wrap justify-center gap-2">
{[
profile.age,
profile.height,
profile.salary + " LPA",
profile.location,
profile.caste,
profile.zodiac1,
profile.zodiac2,
].map((v, i) => (
<span
key={i}
className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
>
{v}
</span>
))}
</div>
<div className="flex gap-4 mt-[15px] justify-center">
<button className="px-2 py-1 rounded-[20px] border border-red-200 bg-red-50 flex items-center gap-1.5 font-semibold text-red-900">
<X size={18} /> Decline
</button> </button>
<Swiper <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">
modules={[Navigation, Pagination]} <Heart size={18} /> Interest
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"
>
{displayProfiles.map((profile, index) => (
<SwiperSlide key={profile.id || index}>
<ProfileCardItem profile={profile} />
</SwiperSlide>
))}
{/* END SLIDE (BUFFER) */}
<SwiperSlide>
<div className="w-full h-full min-h-[400px] flex flex-col items-center justify-center bg-white rounded-[28px] shadow-md border border-gray-100 p-6">
<h3 className="text-xl font-bold mb-4 text-gray-800 text-center">You've seen all matches!</h3>
<button
onClick={handleShowTimer}
className="px-6 py-2 bg-[#8b0000] text-white rounded-full font-semibold shadow-lg hover:bg-red-800 transition"
>
View Next Batch Timer
</button> </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>
<p className="text-gray-500 text-center mb-6"> </div>
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="200px" minHeight="195px"
value={extFiles} value={extFiles}
accept="image/*" accept="image/*"
maxFiles={3} maxFiles={3}
maxFileSize={10 * 1024 * 1024} maxFileSize={10 * 1024 * 1024}
label={<span style={{ fontSize: "16px" }}>Drag'n drop up to 3 images (max 10 MB each)</span>} label="Drag'n drop up to 3 images (max 10 MB each)"
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" sx={{fontSize:"30px"}} /> <ArrowLeftIcon fontSize="small" />
</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" sx={{fontSize:"30px"}} /> <ArrowRightIcon fontSize="small" />
</button> </button>
</div> </div>
)} )}

View File

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateEducationalDetails, clearAllStepsFrom } from "../redux/registrationFormSlice"; import { updateEducationalDetails } from "../redux/registrationFormSlice";
import { import {
TextField, TextField,
Button, Button,
@ -9,412 +9,454 @@ 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: externalErrors, errors,
isEditMode, onFieldChange,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.educationalDetails); const data = useSelector((state) => state.registerform.educationalDetails);
const [localErrors, setLocalErrors] = useState({}); const inputRef = useRef(null);
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.study_field); const educationListQuery = useEducationList(data.fieldOfStudy);
const districtQuery = useCityMasters(data.work_state);
const studyFieldOptions = educationMasters?.studyFields || []; const studyFieldOptions = useMemo(() => {
const qualificationOptions = educationListQuery.data?.education || educationListQuery.data?.data || []; const raw = educationMasters;
const occupationOptions = educationMasters?.occupation || []; if (!raw) return [];
const employeeTypeOptions = educationMasters?.employeeType || []; if (Array.isArray(raw)) return raw;
const countryOptions = educationMasters?.country || []; return raw.studyFields || raw.study_fields || raw.fieldOfStudy || [];
const stateOptions = educationMasters?.state || []; }, [educationMasters]);
const districtOptions = districtQuery.data?.districts || districtQuery.data || [];
const isUnemployed = data.employee_type === 11; const qualificationOptions = useMemo(() => {
const isIndia = Number(data.work_country) === 1; const raw = educationListQuery.data;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.education || raw.data || [];
}, [educationListQuery.data]);
const occupationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [educationMasters]);
const employeeTypeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.employeeType || raw.employee_type || [];
}, [educationMasters]);
const annualIncomeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.annualIncome || raw.annual_income || [];
}, [educationMasters]);
const workLocationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.workLocation || raw.work_location || raw.workLocations || [];
}, [educationMasters]);
const getOptionLabel = (item, fallback = "") => {
if (!item) return fallback;
if (typeof item === "string") return item;
return (
item.study_field_name ||
item.education_name ||
item.occupation_name ||
item.employee_type_name ||
item.annual_income_name ||
item.work_location_name ||
item.name ||
fallback
);
};
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleChange = (field, value) => { const handleChange = (field, value) => {
const updates = { [field]: value }; const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "study_field") { if (field === "fieldOfStudy") {
updates.education = ""; updates.qualification = "";
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));
setLocalErrors((prev) => ({ ...prev, [field]: "" })); if (onFieldChange) onFieldChange(fieldsToClear);
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 = () => {
const freshErrors = validateForm(); console.log("Submitting educational details:", data);
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>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.fieldOfStudy)}
>
<InputLabel id="fieldOfStudy-label">
Select Field of Study
</InputLabel>
<Select <Select
value={data.study_field} labelId="fieldOfStudy-label"
label="Select Field of Study" label="Select Field of Study"
onChange={(e) => handleChange("study_field", e.target.value)} name="fieldOfStudy"
> value={data.fieldOfStudy}
{studyFieldOptions.map((opt) => ( onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
<MenuItem key={opt.id} value={opt.id}>{opt.study_field_name}</MenuItem> inputRef={inputRef}
))} disabled={isEducationMastersLoading}
</Select> sx={{
{localErrors.study_field && <FormHelperText>{localErrors.study_field}</FormHelperText>} "& .MuiSelect-select.Mui-disabled": {
</FormControl> cursor: "not-allowed",
</div> },
{/* 2. Highest Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Highest Qualification{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.education)} id="education" disabled={!data.study_field || educationListQuery.isLoading}>
<InputLabel>Select Qualification</InputLabel>
<Select
value={data.education}
label="Select Qualification"
onChange={(e) => handleChange("education", e.target.value)}
>
{qualificationOptions.map((opt) => (
<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) => ( {studyFieldOptions.map((field) => (
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem> <MenuItem key={field.id ?? field} value={field.id ?? field}>
{getOptionLabel(field, "Field of Study")}
</MenuItem>
))} ))}
</Select> </Select>
{localErrors.work_country && <FormHelperText>{localErrors.work_country}</FormHelperText>} {errors.fieldOfStudy && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.fieldOfStudy}
</p>
)}
</FormControl> </FormControl>
</div> </div>
{/* 12. City/Town (Only if NOT India) */} {/* Highest Qualification */}
{!isIndia && ( <div className="flex flex-col gap-4">
<div className="flex flex-col gap-2"> <label className="text-gray-900 text-[15px]">
<label className="text-gray-900 text-[15px]">City / Town{requiredMark}</label> Highest Educational Qualification{requiredMark}
<TextField </label>
id="work_city" <FormControl
fullWidth fullWidth
placeholder="Enter City / Town" variant="outlined"
value={data.work_city} error={Boolean(errors.qualification)}
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) => ( <InputLabel id="qualification-label">
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem> 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> </Select>
{localErrors.work_state && <FormHelperText>{localErrors.work_state}</FormHelperText>} {errors.qualification && (
</FormControl> <p
</div> style={{
)} color: "#d32f2f",
margin: "3px 14px 0 14px",
{/* 14. City (District) (Only if India) */} fontSize: "0.75rem",
{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) => ( {errors.qualification}
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem> </p>
))} )}
</Select>
{localErrors.work_district && <FormHelperText>{localErrors.work_district}</FormHelperText>}
</FormControl> </FormControl>
</div> </div>
)}
</>
)}
{/* 15. Address */} {/* College Name */}
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">Address{requiredMark}</label> <label className="text-gray-900 text-[15px]">
College Name
</label>
<TextField <TextField
id="address"
fullWidth fullWidth
multiline name="collegeName"
rows={2} label="College Name"
placeholder="Enter your address" value={data.collegeName}
value={data.address} onChange={(e) => handleChange("collegeName", e.target.value)}
onChange={(e) => handleChange("address", e.target.value)} error={Boolean(errors.collegeName)}
error={Boolean(localErrors.address)} helperText={errors.collegeName}
helperText={localErrors.address} placeholder="Enter College Name"
variant="outlined"
/> />
</div> </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>
<Box sx={{ mt: 5, display: "flex", gap: 2, justifyContent: "center" }}> <Grid
{onSkipStep && ( item
<Button variant="outlined" size="large" onClick={onSkipStep} sx={{ minWidth: 120 }}> xs={12}
Skip style={{
marginTop: "40px",
display: "flex",
gap: 16,
justifyContent: "center",
}}
>
<Button variant="outlined" onClick={onSkipStep}>
Next
</Button> </Button>
)} <Button variant="contained" color="primary" onClick={handleSubmit}>
<Button variant="contained" size="large" onClick={handleSubmit} sx={{ minWidth: 120 }}> Submit
{onSkipStep ? "Next" : "Update"}
</Button> </Button>
</Box> </Grid>
</form> </form>
</div> </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, clearAllStepsFrom } from "../redux/registrationFormSlice"; import { updateFamilyDetails } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
TextField, TextField,
@ -12,55 +12,46 @@ 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, isEditMode }) => { const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.familyDetails); const data = useSelector((state) => state.registerform.familyDetails);
const inputRef = useRef(null); const inputRef = useRef(null);
const 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 } = useFamilyMasters(); const { data: familyMasters, isLoading: isFamilyMastersLoading } =
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 [];
return raw.occupation || []; if (Array.isArray(raw)) return raw;
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 [];
return raw.maritalStatus || []; if (Array.isArray(raw)) return raw;
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 [];
return raw.familyStatus || []; if (Array.isArray(raw)) return [];
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: "",
hasChildren: "", haveChildrens: "",
details: "",
}); });
const syncSiblingArray = (arr, count) => { const syncSiblingArray = (arr, count) => {
@ -81,48 +72,20 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
if (field === "brotherCount") { if (field === "brotherCount") {
const count = Number(value) || 0; const count = Number(value) || 0;
updates.brotherCount = value; updates.brotherCount = count;
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 = value; updates.sisterCount = count;
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) => {
@ -131,40 +94,14 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
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 scrollToError = (errorMap) => { const handleSubmit = () => {
const errorFields = Object.keys(errorMap); console.log("Submitting family details:", data);
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();
@ -177,88 +114,95 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
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) => handleSiblingChange(type, index, "name", e.target.value)} onChange={(e) =>
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>Occupation</InputLabel> <InputLabel id={`${type}-${index}-occupation-label`}>
Occupation
</InputLabel>
<Select <Select
labelId={`${type}-${index}-occupation-label`}
label="Occupation" label="Occupation"
value={sibling.occupation} value={sibling.occupation}
onChange={(e) => handleSiblingChange(type, index, "occupation", e.target.value)} onChange={(e) =>
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}>{opt}</MenuItem> <MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel>Marital Status</InputLabel> <InputLabel id={`${type}-${index}-marital-label`}>
Marital Status
</InputLabel>
<Select <Select
labelId={`${type}-${index}-marital-label`}
label="Marital Status" label="Marital Status"
value={sibling.maritalStatus} value={sibling.maritalStatus}
onChange={(e) => handleSiblingChange(type, index, "maritalStatus", e.target.value)} onChange={(e) =>
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}>{opt}</MenuItem> <MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel>Have Children</InputLabel> <InputLabel id={`${type}-${index}-children-label`}>
Have Children
</InputLabel>
<Select <Select
labelId={`${type}-${index}-children-label`}
label="Have Children" label="Have Children"
value={sibling.hasChildren} value={sibling.haveChildrens}
onChange={(e) => handleSiblingChange(type, index, "hasChildren", e.target.value)} onChange={(e) =>
handleSiblingChange(
type,
index,
"haveChildrens",
e.target.value
)
}
> >
<MenuItem value=""><em>Select</em></MenuItem> <MenuItem value={1}>Yes</MenuItem>
<MenuItem value="Yes">Yes</MenuItem> <MenuItem value={0}>No</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>
); );
@ -268,18 +212,20 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
<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" id="fatherName"> <div className="flex flex-col gap-4">
<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}
placeholder="Enter Father Name" name="fatherName"
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>
@ -290,24 +236,30 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
</label> </label>
<TextField <TextField
fullWidth fullWidth
placeholder="Enter Father Occupation" name="fatherOccupation"
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" id="motherName"> <div className="flex flex-col gap-4">
<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
placeholder="Enter Mother Name" name="motherName"
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>
@ -318,9 +270,13 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
</label> </label>
<TextField <TextField
fullWidth fullWidth
placeholder="Enter Mother Occupation" name="motherOccupation"
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>
@ -330,13 +286,14 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
Brother Count Brother Count
</label> </label>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel>Select Brother Count</InputLabel> <InputLabel id="brotherCount-label">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}
@ -346,27 +303,19 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
</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>Select Sister Count</InputLabel> <InputLabel id="sisterCount-label">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}
@ -376,183 +325,103 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
</FormControl> </FormControl>
</div> </div>
{Number(data.sisterCount) > 0 && ( <div className="flex flex-col gap-4">
<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 fullWidth variant="outlined" error={Boolean(errors.familyStatus)}> <FormControl
<InputLabel>Select Family Status</InputLabel> fullWidth
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} value={item.id}> <MenuItem key={item.id ?? item} value={item.id ?? item}>
{item.family_type_name} {item.family_type_name || item.name || item}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{errors.familyStatus && ( {errors.familyStatus && (
<p className="text-[#d32f2f] text-[0.75rem] mt-1 ml-3">{errors.familyStatus}</p> <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" id="nativePlace"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Native Place</label>
Native Place{requiredMark}
</label>
<TextField <TextField
fullWidth fullWidth
placeholder="Enter Native Place" name="nativePlace"
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>
<div className="flex flex-col gap-4"> {Number(data.brotherCount) > 0 && (
<label className="text-gray-900 text-[15px]"> <div className="mt-6">
Country Living <div className="text-gray-900 text-[16px] font-semibold mb-3">
</label> Brother Details
<FormControl fullWidth variant="outlined"> </div>
<InputLabel>Select Country</InputLabel> <div className="grid grid-cols-1 gap-4">
<Select {Array.from({ length: Number(data.brotherCount) }).map(
label="Select Country" (_, index) => renderSiblingCard("brothers", index)
value={data.familyCountry} )}
onChange={(e) => handleChange("familyCountry", e.target.value)} </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" }}
> >
{countryOptions.map((opt) => ( <Button variant="outlined" onClick={onSkipStep}>
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem>
))}
</Select>
</FormControl>
</div>
{isIndia ? (
<>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Residing State
</label>
<FormControl fullWidth variant="outlined" disabled={!data.familyCountry}>
<InputLabel>Select State</InputLabel>
<Select
label="Select State"
value={data.familyState}
onChange={(e) => handleChange("familyState", e.target.value)}
>
{stateOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Residing City
</label>
<FormControl fullWidth variant="outlined" disabled={!data.familyState || districtQuery.isLoading}>
<InputLabel>Select City</InputLabel>
<Select
label="Select City"
value={data.familyDistrict}
onChange={(e) => handleChange("familyDistrict", e.target.value)}
>
{districtOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem>
))}
</Select>
</FormControl>
</div>
</>
) : data.familyCountry ? (
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
City / Town
</label>
<TextField
fullWidth
placeholder="Enter City / Town"
value={data.familyCity}
onChange={(e) => handleChange("familyCity", e.target.value)}
variant="outlined"
/>
</div>
) : null}
<div className="flex flex-col gap-4 md:col-span-2">
<label className="text-gray-900 text-[15px]">
Address
</label>
<TextField
fullWidth
multiline
rows={3}
placeholder="Enter complete address"
value={data.address}
onChange={(e) => handleChange("address", e.target.value)}
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4 md:col-span-2">
<label className="text-gray-900 text-[15px]">
Expectations / Requirements Details
</label>
<TextField
fullWidth
multiline
rows={4}
placeholder="Describe your expectations"
value={data.expectationDetails}
onChange={(e) => handleChange("expectationDetails", e.target.value)}
variant="outlined"
/>
</div>
<div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]">
Willing to go abroad
</label>
<FormControl fullWidth variant="outlined">
<InputLabel>Select Option</InputLabel>
<Select
label="Select Option"
value={data.willingToGoAbroad}
onChange={(e) => handleChange("willingToGoAbroad", e.target.value)}
>
<MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
<MenuItem value="Any">Any</MenuItem>
</Select>
</FormControl>
</div>
</div>
<div className="mt-10 flex gap-4 justify-center">
{onSkipStep && (
<Button variant="outlined" onClick={onSkipStep} sx={{ minWidth: 120 }}>
Skip Skip
</Button> </Button>
)} <Button variant="contained" color="primary" onClick={handleSubmit}>
<Button variant="contained" color="primary" onClick={handleSubmit} sx={{ minWidth: 120 }}> Submit
{onSkipStep ? "Next" : "Update"}
</Button> </Button>
</div> </Grid>
</form> </form>
</div> </div>
); );

View File

@ -1,12 +1,15 @@
import React, { useState, useMemo } from "react"; import React, { useState } 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,
@ -23,12 +26,8 @@ import {
Accordion, Accordion,
AccordionSummary, AccordionSummary,
AccordionDetails, AccordionDetails,
CircularProgress,
} from "@mui/material"; } from "@mui/material";
import { ChevronDown, Lock, Crown, RotateCcw, Check } from "lucide-react"; import { ChevronDown } 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();
@ -41,61 +40,34 @@ 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-[#f2f2f2]"> <div className="border-b border-pink-200 p-4 bg-[#fff5ed]">
<Typography variant="h5" className="font-semibold text-center"> <Typography variant="h5" className="font-semibold text-center">
Partner Filter preference Partner Filter preference
</Typography> </Typography>
@ -126,7 +98,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.from_age, filters.to_age]} value={filters.age}
onChange={(e, newValue) => { onChange={(e, newValue) => {
dispatch(setAge(newValue)); dispatch(setAge(newValue));
handleSelectionChange("Age", newValue); handleSelectionChange("Age", newValue);
@ -147,7 +119,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.from_height, filters.to_height]} value={filters.height}
onChange={(e, newValue) => { onChange={(e, newValue) => {
dispatch(setHeight(newValue)); dispatch(setHeight(newValue));
handleSelectionChange("Height", newValue); handleSelectionChange("Height", newValue);
@ -169,40 +141,46 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Marital Status</InputLabel> <InputLabel>Marital Status</InputLabel>
<Select <Select
multiple value={filters.maritalStatus}
value={filters.marital_status || []}
label="Marital Status" label="Marital Status"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ marital_status: e.target.value })); dispatch(setMaritalStatus(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) => (
const label = filterMasters?.marital_statuses?.find((s) => s.id === value)?.marital_status_name || value; <Chip key={value} label={value} size="small" />
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>
)} )}
> >
{filterMasters?.marital_statuses?.map((status) => ( {motherTongueOptions.map((lang) => (
<MenuItem key={status.id} value={status.id}> <MenuItem key={lang} value={lang}>
{status.marital_status_name} {lang}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -229,89 +207,54 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Religion</InputLabel> <InputLabel>Religion</InputLabel>
<Select <Select
multiple value={filters.religion}
value={filters.religion || []}
label="Religion" label="Religion"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ religion: e.target.value })); dispatch(setReligion(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>
)}
> >
{filterMasters?.religions?.map((rel) => ( <MenuItem value="Hindu">Hindu</MenuItem>
<MenuItem key={rel.id} value={rel.id}> <MenuItem value="Muslim">Muslim</MenuItem>
{rel.religion_name} <MenuItem value="Christian">Christian</MenuItem>
</MenuItem> <MenuItem value="Sikh">Sikh</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</InputLabel> <InputLabel>Caste (Multi Select)</InputLabel>
<Select <Select
multiple multiple
value={filters.caste} value={filters.caste}
label="Caste" label="Caste (Multi Select)"
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 sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}> <Box
{selected.map((value) => { sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}
const label = filterMasters?.castes?.find((c) => c.id === value)?.caste_name || value;
return (
<Chip
key={value}
label={label}
size="small"
onDelete={() => dispatch(setCaste(filters.caste.filter((v) => v !== value)))}
onMouseDown={(e) => e.stopPropagation()}
/>
);
})}
</Box>
)}
> >
{filterMasters?.castes?.map((caste) => (
<MenuItem key={caste.id} value={caste.id}>
{caste.caste_name}
</MenuItem>
))}
</Select>
</FormControl>
{/* Sub-Caste */}
<FormControl fullWidth>
<InputLabel>Sub-Caste</InputLabel>
<Select
multiple
value={filters.sub_caste}
label="Sub-Caste"
onChange={(e) => {
dispatch(setSubCaste(e.target.value));
handleSelectionChange("Sub-Caste", e.target.value);
}}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => ( {selected.map((value) => (
<Chip key={value} label={value} size="small" onDelete={() => dispatch(setSubCaste(filters.sub_caste.filter((v) => v !== value)))} onMouseDown={(e) => e.stopPropagation()} /> <Chip key={value} label={value} size="small" />
))} ))}
</Box> </Box>
)} )}
@ -324,7 +267,68 @@ const FilterForm = () => {
</Select> </Select>
</FormControl> </FormControl>
{/* Sub-Caste */}
<FormControl fullWidth>
<InputLabel>Sub-Caste (Multi Select)</InputLabel>
<Select
multiple
value={filters.subCaste}
label="Sub-Caste (Multi Select)"
onChange={(e) => {
dispatch(setSubCaste(e.target.value));
handleSelectionChange("Sub-Caste", e.target.value);
}}
renderValue={(selected) => (
<Box
sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}
>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{["Agamudayar", "Pillai", "Vellalar"].map((caste) => (
<MenuItem key={caste} value={caste}>
{caste}
</MenuItem>
))}
</Select>
</FormControl>
{/* Star */}
<FormControl fullWidth>
<InputLabel>Star</InputLabel>
<Select
value={filters.star}
label="Star"
onChange={(e) => {
dispatch(updateFilter({ star: e.target.value }));
handleSelectionChange("Star", e.target.value);
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Ashwini">Ashwini</MenuItem>
<MenuItem value="Bharani">Bharani</MenuItem>
</Select>
</FormControl>
{/* Dasham */}
<FormControl fullWidth>
<InputLabel>Dasham</InputLabel>
<Select
value={filters.dasham}
label="Dasham"
onChange={(e) => {
dispatch(updateFilter({ dasham: e.target.value }));
handleSelectionChange("Dasham", e.target.value);
}}
>
<MenuItem value="Doesn't matter">Doesn't matter</MenuItem>
<MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
</Select>
</FormControl>
</div> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -352,108 +356,66 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Occupation</InputLabel> <InputLabel>Occupation</InputLabel>
<Select <Select
multiple value={filters.occupation}
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>
)}
> >
{filterMasters?.occupations?.map((occ) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={occ.id} value={occ.id}> <MenuItem value="Software Engineer">
{occ.occupation_name} Software Engineer
</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
multiple value={filters.annualIncome}
value={filters.annual_income || []}
label="Annual Income" label="Annual Income"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ annual_income: e.target.value })); dispatch(updateFilter({ annualIncome: 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>
)}
> >
{filterMasters?.annual_incomes?.map((inc) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={inc.id} value={inc.id}> <MenuItem value="0-5 Lakhs">0-5 Lakhs</MenuItem>
{inc.annual_income_name} <MenuItem value="5-10 Lakhs">5-10 Lakhs</MenuItem>
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Employee Type</InputLabel> <InputLabel>Employee Type</InputLabel>
<Select <Select
multiple value={filters.employeeType}
value={filters.employee_type || []}
label="Employee Type" label="Employee Type"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ employee_type: e.target.value })); dispatch(updateFilter({ employeeType: 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>
)}
> >
{filterMasters?.employee_types?.map((emp) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={emp.id} value={emp.id}> <MenuItem value="Government">Government</MenuItem>
{emp.employee_type_name} <MenuItem value="Private">Private</MenuItem>
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Education</InputLabel> <InputLabel>Education</InputLabel>
<Select <Select
multiple value={filters.education}
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>
)}
> >
{filterMasters?.educations?.map((edu) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={edu.id} value={edu.id}> <MenuItem value="Bachelor">Bachelor</MenuItem>
{edu.education_name} <MenuItem value="Master">Master</MenuItem>
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
@ -481,59 +443,50 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>State</InputLabel> <InputLabel>State</InputLabel>
<Select <Select
multiple value={filters.state}
value={filters.state || []}
label="State" label="State"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ state: e.target.value, district: [] })); dispatch(updateFilter({ state: e.target.value }));
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>
)}
> >
{filterMasters?.states?.map((state) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={state.id} value={state.id}> <MenuItem value="Tamil Nadu">Tamil Nadu</MenuItem>
{state.state_name} <MenuItem value="Karnataka">Karnataka</MenuItem>
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>City</InputLabel> <InputLabel>Country</InputLabel>
<Select <Select
multiple value={filters.country}
value={filters.district || []} label="Country"
label="City"
disabled={!filters.state || filters.state.length === 0}
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ district: e.target.value })); dispatch(updateFilter({ country: e.target.value }));
handleSelectionChange("City", e.target.value); handleSelectionChange("Country", 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>
)}
> >
{cityOptions.map((city) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={city.id} value={city.id}> <MenuItem value="India">India</MenuItem>
{city.district_name} <MenuItem value="USA">USA</MenuItem>
</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>
@ -559,22 +512,54 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Eating Habits</InputLabel> <InputLabel>Eating Habits</InputLabel>
<Select <Select
value={filters.diet} value={filters.eatingHabits}
label="Eating Habits" label="Eating Habits"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ diet: e.target.value })); dispatch(updateFilter({ eatingHabits: e.target.value }));
handleSelectionChange("Eating Habits", e.target.value); handleSelectionChange("Eating Habits", e.target.value);
}} }}
> >
{filterMasters?.diets?.map((diet) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={diet.id} value={diet.id}> <MenuItem value="Vegetarian">Vegetarian</MenuItem>
{diet.diet_name} <MenuItem value="Non-Vegetarian">Non-Vegetarian</MenuItem>
</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>
@ -600,116 +585,64 @@ const FilterForm = () => {
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel>Family Type</InputLabel> <InputLabel>Family Type</InputLabel>
<Select <Select
multiple value={filters.familyType}
value={filters.family_type || []}
label="Family Type" label="Family Type"
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ family_type: e.target.value })); dispatch(updateFilter({ familyType: 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>
)}
> >
{filterMasters?.family_types?.map((type) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={type.id} value={type.id}> <MenuItem value="Nuclear">Nuclear</MenuItem>
{type.family_type_name} <MenuItem value="Joint">Joint</MenuItem>
</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
</div>
</AccordionDetails>
</Accordion>
{/* Paid Membership Benefit Section */}
<Accordion
expanded={expandedSections.paidBenefit && filters.isPaidMember}
onChange={(e, isExpanded) => {
if (!filters.isPaidMember) {
toast.error("This feature is locked for paid members only", {
icon: "🔒",
});
return;
}
handleAccordionChange("paidBenefit")(e, isExpanded);
}}
className="mb-4"
>
<AccordionSummary
expandIcon={filters.isPaidMember ? <ChevronDown /> : <Lock size={18} className="text-gray-400" />}
sx={{
backgroundColor: "#f5fbff",
"&:hover": {
backgroundColor: "#fff6f0",
},
}}
>
<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>Star</InputLabel> <InputLabel>Family Status</InputLabel>
<Select <Select
multiple value={filters.familyStatus}
value={filters.star || []} label="Family Status"
label="Star"
disabled={!filters.isPaidMember}
onChange={(e) => { onChange={(e) => {
dispatch(updateFilter({ star: e.target.value })); dispatch(updateFilter({ familyStatus: e.target.value }));
handleSelectionChange("Star", e.target.value); handleSelectionChange("Family Status", e.target.value);
}} }}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => {
const label = filterMasters?.stars?.find((s) => s.id === value)?.star_name || value;
return <Chip key={value} label={label} size="small" onDelete={() => dispatch(updateFilter({ star: filters.star.filter((v) => v !== value) }))} onMouseDown={(e) => e.stopPropagation()} />;
})}
</Box>
)}
> >
{filterMasters?.stars?.map((star) => ( <MenuItem value="Any">Any</MenuItem>
<MenuItem key={star.id} value={star.id}> <MenuItem value="Middle Class">Middle Class</MenuItem>
{star.star_name} <MenuItem value="Upper Middle Class">
Upper Middle Class
</MenuItem> </MenuItem>
))} </Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Family Value</InputLabel>
<Select
value={filters.familyValue}
label="Family Value"
onChange={(e) => {
dispatch(updateFilter({ familyValue: e.target.value }));
handleSelectionChange("Family Value", e.target.value);
}}
>
<MenuItem value="Any">Any</MenuItem>
<MenuItem value="Traditional">Traditional</MenuItem>
<MenuItem value="Moderate">Moderate</MenuItem>
<MenuItem value="Liberal">Liberal</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
{/* Sticky Footer Actions */} {/* Submit Button */}
<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)]"> <div className="mt-6 flex justify-center">
<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="md" // adjust: "sm" | "md" | "lg" maxWidth="lg" // 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, useState } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateLifestyleDetails, clearAllStepsFrom } from "../redux/registrationFormSlice"; import { updateLifestyleDetails } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
FormControl, FormControl,
@ -11,26 +11,18 @@ 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);
@ -61,15 +53,6 @@ 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();
}, []); }, []);
@ -77,10 +60,6 @@ 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) => {
@ -89,74 +68,23 @@ const LifestyleDetailsForm = ({
if (onFieldChange) onFieldChange(field); if (onFieldChange) onFieldChange(field);
}; };
const handleChartChange = (type, house, newValue) => { const handleGrahaChange = (house, value) => {
const currentChart = type === "graha" ? data.graha : data.amsam; const next = { ...(data.graha || {}) };
const currentHouseValue = currentChart?.[house] || []; next[house] = value;
dispatch(updateLifestyleDetails({ graha: next }));
// Find added items if (onFieldChange) onFieldChange("graha");
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 handleConfirmMove = () => { const handleAmsamChange = (house, value) => {
const { type, house, item, sourceHouse, pendingValue } = confirmDialog; const next = { ...(data.amsam || {}) };
const currentChart = type === "graha" ? data.graha : data.amsam; next[house] = value;
const nextChart = { ...(currentChart || {}) }; dispatch(updateLifestyleDetails({ amsam: next }));
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) => {
@ -190,7 +118,6 @@ 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}
@ -203,17 +130,7 @@ 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 ( return selected.join(", ");
<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 } },
@ -228,14 +145,15 @@ 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) =>
handleChartChange(type, house, value); type === "graha"
? handleGrahaChange(house, value)
: handleAmsamChange(house, value);
const label = type === "graha" ? "Rasi" : "Navamsam"; const label = type === "graha" ? "Rasi" : "Navamsam";
return ( return (
@ -247,9 +165,7 @@ 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 bg-white"> <div className="w-full aspect-square rounded-full overflow-hidden border-4 border-gray-200 shadow-lg 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))}
@ -270,7 +186,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{requiredMark} Diet (Multi-select){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>
@ -278,10 +194,21 @@ 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) => handleChange("diets", e.target.value)} onChange={(e) => handleMultiChange("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",
@ -293,7 +220,8 @@ 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}>
{label} <Checkbox checked={data.diets.indexOf(value) > -1} />
<ListItemText primary={label} />
</MenuItem> </MenuItem>
); );
})} })}
@ -478,33 +406,13 @@ const LifestyleDetailsForm = ({
justifyContent: "center", justifyContent: "center",
}} }}
> >
{onSkipStep && (
<Button variant="outlined" onClick={onSkipStep}> <Button variant="outlined" onClick={onSkipStep}>
Skip Skip
</Button> </Button>
)}
<Button variant="contained" color="primary" onClick={handleSubmit}> <Button variant="contained" color="primary" onClick={handleSubmit}>
{onSkipStep ? "Next" : "Update"} Next
</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,13 +417,11 @@ const PartnerPreferencesForm = ({
justifyContent: "center", justifyContent: "center",
}} }}
> >
{onSkipStep && (
<Button variant="outlined" onClick={onSkipStep}> <Button variant="outlined" onClick={onSkipStep}>
Skip Skip
</Button> </Button>
)}
<Button variant="contained" color="primary" onClick={handleSubmit}> <Button variant="contained" color="primary" onClick={handleSubmit}>
{onSkipStep ? "Next" : "Update"} Next
</Button> </Button>
</Grid> </Grid>
</form> </form>

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React from "react";
import { useSelector, useDispatch } from 'react-redux'; import { useSelector } 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,16 +17,9 @@ 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);
@ -35,37 +28,6 @@ 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
? [ ? [
{ {
@ -221,8 +183,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', overflow: 'hidden' }}> <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' }}>
<img src={horoscopeImg} alt={title} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> {title.toUpperCase()}
</Box> </Box>
{renderCell(6)} {renderCell(6)}
{renderCell(7)} {renderCell(8)} {renderCell(7)} {renderCell(8)}
@ -262,10 +224,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
{!isLoading && {!isLoading &&
sections.map((section) => ( sections.map((section) => (
<Card key={section.title} variant="outlined" <Card key={section.title} variant="outlined" sx={{ borderRadius: 2, background:"#fff5ed" }}>
sx={{ borderRadius: 2,
// background:"#fff5ed"
}}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -284,25 +243,11 @@ 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
const hiddenFields = [ if (key === 'id' || key.endsWith('_id') || key.endsWith('Id') || key.endsWith('_ids') || key.endsWith('Ids') || key === 'created_at' || key === 'updated_at') {
"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;
} }
@ -359,21 +304,14 @@ 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: 8px; font-size: 12px;
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;
@ -391,16 +329,15 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
> >
{value.map((sibling, idx) => ( {value.map((sibling, idx) => (
<SwiperSlide key={idx}> <SwiperSlide key={idx}>
<Card variant="outlined" <Card variant="outlined" sx={{ bgcolor: '#fff', height: '100%' }}>
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' || sKey === 'hasChildren' || sKey === 'has_children') { if (sKey === 'haveChildrens' || sKey === 'have_childrens') {
if (sVal === true || sVal === 1 || sVal === '1' || sVal === 'Yes') { if (sVal === true || sVal === 1 || sVal === '1') {
displayValue = 'Yes'; displayValue = 'Yes';
} else if (sVal === false || sVal === 0 || sVal === '0' || sVal === 'No') { } else if (sVal === false || sVal === 0 || sVal === '0') {
displayValue = 'No'; displayValue = 'No';
} }
} }
@ -438,7 +375,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: {
@ -497,35 +434,13 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
color="success" color="success"
size="large" size="large"
onClick={handleConfirmSubmit} onClick={onSubmit}
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,8 +13,9 @@ 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,
@ -24,12 +25,8 @@ 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
@ -45,95 +42,20 @@ const images = [
]; ];
const ProfilePreviewPage = () => { const ProfilePreviewPage = () => {
const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { data, isLoading } = usePreviewDetails(); const navigate = useNavigate();
const {
personalDetails,
educationalDetails,
familyDetails,
lifestyleDetails,
partnerPreferences,
} = useSelector((state) => state.registerform);
// For dummy data on first visit
useEffect(() => { useEffect(() => {
if (data?.personal_details) { dispatch(preloadDummyProfile());
const pd = data.personal_details; }, [dispatch]);
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 } });
@ -142,7 +64,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: {
@ -180,119 +102,8 @@ 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: "#ffff" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -309,9 +120,9 @@ const ProfilePreviewPage = () => {
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f2f2f2" }} sx={{ padding: "15px 15px", background: "#f5fbff" }}
/> />
{/* <Divider /> */} <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Name", personalDetails.name, 1)} {renderField("Name", personalDetails.name, 1)}
@ -331,7 +142,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" },
@ -399,7 +210,7 @@ const ProfilePreviewPage = () => {
); );
const renderEducationalSection = () => ( const renderEducationalSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -416,9 +227,9 @@ const ProfilePreviewPage = () => {
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f2f2f2" }} sx={{ padding: "15px 15px", background: "#f5fbff" }}
/> />
{/* <Divider /> */} <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Qualification", educationalDetails.qualification, 2)} {renderField("Qualification", educationalDetails.qualification, 2)}
@ -434,7 +245,7 @@ const ProfilePreviewPage = () => {
); );
const renderFamilySection = () => ( const renderFamilySection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -445,15 +256,15 @@ const ProfilePreviewPage = () => {
<IconButton <IconButton
aria-label="edit" aria-label="edit"
color="primary" color="primary"
onClick={() => handleEditSection(3)} onClick={() => handleEditSection(2)}
size="large" size="large"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f2f2f2" }} sx={{ padding: "15px 15px", background: "#f5fbff" }}
/> />
{/* <Divider /> */} <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Father Name", familyDetails.fatherName)} {renderField("Father Name", familyDetails.fatherName)}
@ -469,7 +280,7 @@ const ProfilePreviewPage = () => {
); );
const renderLifestyleSection = () => ( const renderLifestyleSection = () => (
<Card variant="outlined" sx={{ borderRadius: 2, background: "#ffff" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
@ -480,15 +291,15 @@ const ProfilePreviewPage = () => {
<IconButton <IconButton
aria-label="edit" aria-label="edit"
color="primary" color="primary"
onClick={() => handleEditSection(4)} onClick={() => handleEditSection(2)}
size="large" size="large"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f2f2f2" }} sx={{ padding: "15px 15px", background: "#f5fbff" }}
/> />
{/* <Divider /> */} <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField( {renderField(
@ -506,61 +317,32 @@ 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: "#ffff" }}> <Card variant="outlined" sx={{ borderRadius: 2, background: "#fff5ed" }}>
<CardHeader <CardHeader
title={ title={
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
Partner Preferences Lifestyle Details
</Typography> </Typography>
} }
action={ action={
<IconButton <IconButton
aria-label="edit" aria-label="edit"
color="primary" color="primary"
onClick={() => handleEditSection(5)} onClick={() => handleEditSection(2)}
size="large" size="large"
> >
<Edit2 size={20} /> <Edit2 size={20} />
</IconButton> </IconButton>
} }
sx={{ padding: "15px 15px", background: "#f2f2f2" }} sx={{ padding: "15px 15px", background: "#f5fbff" }}
/> />
{/* <Divider /> */} <Divider />
<CardContent sx={{ pt: 1 }}> <CardContent sx={{ pt: 1 }}>
{renderField("Age Range", partnerPreferences.ageRange)} {renderField("Age Range", partnerPreferences.ageRange)}
@ -612,14 +394,6 @@ 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

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

View File

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

View File

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

View File

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

View File

@ -1,168 +0,0 @@
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,9 +7,8 @@ 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 { persistor, store } from "./redux/store.js"; import { 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 = () => {};
@ -32,11 +31,9 @@ ReactDOM.createRoot(document.getElementById("root")).render(
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<App /> <App />
</QueryClientProvider> </QueryClientProvider>
</PersistGate>
</Provider> </Provider>
</ThemeProvider> </ThemeProvider>

View File

@ -1,57 +1,42 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { Tabs, Tab, Box, CircularProgress } from '@mui/material'; import { Tabs, Tab, Box, Chip } from '@mui/material';
import { CheckCircle, Phone } from '@mui/icons-material'; import { CheckCircle, Phone, ExpandMore } 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, onUnblock }) => ( const BlockedProfile = ({ profile }) => (
<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.photo || 'https://via.placeholder.com/150'} src={profile.image}
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.member_id} | Profile Created by Parent</p> <p className="text-gray-500 text-sm mb-3">{profile.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 ? `${profile.age} yrs` : ''}{profile.age && profile.height ? ', ' : ''}{profile.height || ''}</p> <p className="font-medium">{profile.age} yrs, {profile.height}, {profile.language},</p>
<p className="font-medium">{profile.location},</p>
<p className="font-medium"> <p className="font-medium">{profile.education}, {profile.occupation}, {profile.income}, {profile.state}, India</p>
{[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 <button className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors">
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>
@ -64,7 +49,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.photo || 'https://via.placeholder.com/150'} src={profile.image}
alt={profile.name} alt={profile.name}
className="w-full h-full object-cover " className="w-full h-full object-cover "
/> />
@ -72,39 +57,25 @@ 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.member_id} {profile.last_seen_at && <span className="text-xs ml-1">{profile.last_seen_at}</span>} ID : {profile.id} <span className="text-xs ml-1">Last seen {profile.lastSeen}</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>
{profile.age && (
<>
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">{profile.age} yrs</span> <span className="text-gray-600">{profile.age} yrs</span>
</>
)}
</div> </div>
{profile.caste_name && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">{profile.caste_name}</span> <span className="text-gray-600">{profile.caste}</span>
</div> </div>
)}
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{profile.occupation && (
<>
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">{profile.occupation}</span> <span className="text-gray-600">{profile.occupation}</span>
</>
)}
{profile.district_name && (
<>
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">{profile.district_name}</span> <span className="text-gray-600">{profile.location}</span>
</>
)}
</div> </div>
</div> </div>
@ -129,20 +100,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.photo || 'https://via.placeholder.com/150'} src={profile.image}
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.member_id}</p> <p className="text-sm text-gray-500">ID : {profile.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.reason} {profile.reportReason}
</p> </p>
</div> </div>
<div className='w-full flex justify-center'> <div className='w-full flex justify-center'>
@ -164,62 +135,116 @@ 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, setBlockedProfiles] = useState([]); const blockedProfiles = [
const [reportedProfiles, setReportedProfiles] = useState([]); {
const [loading, setLoading] = useState(true); id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
}
];
useEffect(() => { const reportedProfiles = [
fetchData(); {
}, []); id: 'TK52586A',
name: 'Pavilash . P',
const fetchData = async () => { age: 23,
setLoading(true); lastSeen: 'Nov 25',
try { caste: 'Agamudayar / Arcot / Thuluva vellala',
const [blockedRes, reportedRes] = await Promise.all([ occupation: 'Engineer-non IT',
getBlockedProfiles(), location: 'Chennai',
getReportedProfiles() 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.'
if (blockedRes.status === "success") { },
setBlockedProfiles(blockedRes.data); {
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
},
{
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
},
{
id: 'TK52586A',
name: 'Pavilash . P',
age: 23,
lastSeen: 'Nov 25',
caste: 'Agamudayar / Arcot / Thuluva vellala',
occupation: 'Engineer-non IT',
location: 'Chennai',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
showReason: true,
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
} }
if (reportedRes.status === "success") { ];
setReportedProfiles(reportedRes.data);
}
} catch (error) {
console.error("Error fetching data:", error);
toast.error("Failed to load profiles");
} finally {
setLoading(false);
}
};
const handleUnblock = async (profileId) => {
try {
const res = await unblockProfile(profileId);
if (res.status === "success") {
toast.success(res.message || "Profile unblocked successfully");
setBlockedProfiles(prev => prev.filter(p => p.id !== profileId));
} else {
toast.error(res.message || "Failed to unblock profile");
}
} catch (error) {
toast.error("Something went wrong");
}
};
const handleTabChange = (event, newValue) => { 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">
@ -234,14 +259,14 @@ function BlockedProfileListPage() {
textTransform: 'none', textTransform: 'none',
fontSize: '1rem', fontSize: '1rem',
fontWeight: 600, fontWeight: 600,
minWidth: 150, minWidth: 120,
}, },
'& .Mui-selected': { '& .Mui-selected': {
color: '#fff !important', color: '#fff !important',
background:"#A70710" background:"#A70710"
}, },
'& .MuiTabs-indicator': { '& .MuiTabs-indicator': {
backgroundColor: 'transparent', backgroundColor: '#A70710',
}, },
}} }}
> >
@ -252,32 +277,28 @@ 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-4 px-4'> <div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-2'>
{blockedProfiles.length > 0 ? ( {blockedProfiles.map((profile, index) => (
blockedProfiles.map((profile, index) => ( <BlockedProfile key={index} profile={profile} />
<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-4 px-4'> <div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-2'>
{reportedProfiles.length > 0 ? ( {reportedProfiles.map((profile, index) => (
reportedProfiles.map((profile, index) => (
<ReportedProfile <ReportedProfile
key={profile.id || index} key={index}
profile={profile} profile={profile}
onViewReason={setSelectedReport} 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
@ -286,7 +307,7 @@ function BlockedProfileListPage() {
/> />
</div> </div>
<style>{` <style jsx>{`
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;

View File

@ -1,16 +1,8 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState } from 'react';
import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2, Loader2, MessageCircle } from 'lucide-react'; import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2 } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import ReportModal from '../components/common/ReportModal'; import ReportModal from '../components/common/ReportModal';
import { getChatList, getChatMessages, sendMessage } from '../services/chatApi';
import toast from 'react-hot-toast';
import { useWebSocket } from '../hooks/useWebSocket';
import { useSelector } from 'react-redux';
const ChatUI = () => { const ChatUI = () => {
const queryClient = useQueryClient();
const { personalDetails } = useSelector((state) => state.registerform);
const [selectedChat, setSelectedChat] = useState(null); const [selectedChat, setSelectedChat] = useState(null);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [showChatOnMobile, setShowChatOnMobile] = useState(false); const [showChatOnMobile, setShowChatOnMobile] = useState(false);
@ -19,316 +11,241 @@ const ChatUI = () => {
const [showChatMenu, setShowChatMenu] = useState(false); const [showChatMenu, setShowChatMenu] = useState(false);
const [openReport, setOpenReport] = useState(false); const [openReport, setOpenReport] = useState(false);
const [contacts, setContacts] = useState([]); const contacts = [
const [chatMessages, setChatMessages] = useState([]); {
const [chatDetails, setChatDetails] = useState(null); id: 1,
const [loadingContacts, setLoadingContacts] = useState(true); name: 'Kalai',
const [loadingMessages, setLoadingMessages] = useState(false); avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
const [sendingMessage, setSendingMessage] = useState(false); lastMessage: 'Hi bro!n how are you Long time no see',
const [searchTerm, setSearchTerm] = useState(""); time: '10 Nov 2025, 10 : 23 AM',
const [loadingMore, setLoadingMore] = useState(false); online: false
const [currentPage, setCurrentPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const messagesEndRef = useRef(null);
const scrollContainerRef = useRef(null);
const isPaginating = useRef(false);
const previousScrollHeight = useRef(0);
const observerRef = useRef(null);
const topMarkerRef = useRef(null);
// WebSocket Integration - Listening to BOTH Notifications and the Active Chat
const profileId = localStorage.getItem("profile_id") || personalDetails?.id;
const userId = localStorage.getItem("user_id") || profileId;
// To show messages INSTANTLY in bubbles, we must listen to the specific chat channel
const activeChatChannel = selectedChat ? `chat-${selectedChat}` : null;
// Flutter uses both dotted and non-dotted formats for channels in some Reverb setups
const wsChannels = React.useMemo(() => {
const channels = [];
if (userId && userId !== "null") {
channels.push(`user-chat-notification${userId}`);
channels.push(`user-chat-notification.${userId}`); // Dotted version
channels.push(`user-notification${userId}`);
channels.push(`user-notification.${userId}`); // Dotted version
}
if (chatDetails?.web_socket_channel) {
channels.push(chatDetails.web_socket_channel);
} else if (activeChatChannel) {
channels.push(activeChatChannel);
}
return [...new Set(channels.filter(Boolean))];
}, [userId, activeChatChannel, chatDetails?.web_socket_channel]);
console.log("[WS-CHANNELS] Subscribing to:", wsChannels.join(", "));
const { messages: wsMessages, isConnected } = useWebSocket(wsChannels);
const processedMsgCount = useRef(wsMessages.length); // Start from current length to avoid processing history
// Initial refresh when socket connects
useEffect(() => {
if (isConnected) {
console.log("[WS-STATUS] WebSocket connected, syncing initial state...");
fetchContacts(searchTerm);
if (selectedChat) fetchMessages(selectedChat);
}
}, [isConnected, selectedChat, searchTerm]);
const fetchContacts = useCallback(async (search = "", silent = false) => {
if (!silent) setLoadingContacts(true);
try {
console.log(`[API] Fetching contacts list (search: "${search}", silent: ${silent})`);
const response = await getChatList(search);
if (response.status) {
setContacts(response.chatLists);
console.log(`[API] Contacts list updated. Total contacts: ${response.chatLists?.length}`);
}
} catch (error) {
console.error("[API] Error fetching contacts:", error);
toast.error("Failed to load chat list");
} finally {
setLoadingContacts(false);
}
}, []);
const fetchMessages = useCallback(async (chatId, silent = false) => {
if (!chatId) return;
if (!silent) setLoadingMessages(true);
try {
const response = await getChatMessages(chatId, 1);
console.log("[API-PAGINATION] Initial Page 1 Response:", {
status: response.status,
current_page: response.messages?.current_page,
last_page: response.messages?.last_page,
total: response.messages?.total
});
if (response.status) {
setChatMessages(response.messages.data.reverse());
setChatDetails(response.messages);
setCurrentPage(1);
setHasMore(response.messages.current_page < response.messages.last_page);
}
} catch (error) {
toast.error("Failed to load messages");
} finally {
setLoadingMessages(false);
}
}, []);
const loadMoreMessages = useCallback(async () => {
if (!selectedChat || loadingMore || !hasMore) return;
setLoadingMore(true);
isPaginating.current = true;
const nextPage = currentPage + 1;
try {
if (scrollContainerRef.current) {
previousScrollHeight.current = scrollContainerRef.current.scrollHeight;
}
const response = await getChatMessages(selectedChat, nextPage);
if (response.status) {
const olderMessages = response.messages.data.reverse();
setChatMessages(prev => [...olderMessages, ...prev]);
setChatDetails(response.messages);
setCurrentPage(nextPage);
setHasMore(response.messages.current_page < response.messages.last_page);
}
} catch (error) {
console.error("Error loading more messages:", error);
isPaginating.current = false;
} finally {
setLoadingMore(false);
}
}, [selectedChat, currentPage, hasMore, loadingMore]);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loadingMore && selectedChat) {
console.log("[PAGINATION] Top marker visible, loading more...");
loadMoreMessages();
}
}, },
{ threshold: 1.0, root: scrollContainerRef.current } {
); id: 2,
name: 'Sabitha',
if (topMarkerRef.current) { avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sabitha',
observer.observe(topMarkerRef.current); lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 3,
name: 'Lia',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lia',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 4,
name: 'Moi',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Moi',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 5,
name: 'Sri',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sri',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 6,
name: 'Lyana',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lyana',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 7,
name: 'Lyana',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lyana',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 8,
name: 'Lyana',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lyana',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 9,
name: 'Lyana',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lyana',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
},
{
id: 10,
name: 'Lyana',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lyana',
lastMessage: 'Hi bro!n how are you Long time no see',
time: '10 Nov 2025, 10 : 23 AM',
online: false
} }
];
return () => observer.disconnect(); const messages = {
}, [hasMore, loadingMore, selectedChat, loadMoreMessages]); 1: [
{
const handleScroll = (e) => { id: 1,
// Keep for manual debugging if needed sender: 'other',
text: 'Let\'s do it! I\'m in a meeting until noon.',
time: '10 Nov',
isDate: false
},
{
id: 2,
sender: 'me',
text: 'That\'s perfect! There\'s a new place on Main St I\'ve been wanting to check out. I hear their hawaiian pizza is awesome!',
time: '07:21',
isDate: false
},
{
id: 3,
sender: 'date',
text: 'Today',
isDate: true
},
{
id: 4,
sender: 'me',
text: 'Can\'s get lunch. How about tomorrow?',
time: '09:42',
isDate: false
},
{
id: 5,
sender: 'other',
text: 'Let\'s do it! I\'m in a meeting until noon.',
time: '',
isDate: false
},
{
id: 6,
sender: 'me',
text: 'That\'s perfect! There\'s a new place on Main St I\'ve been wanting to check out. I hear their hawaiian pizza is awesome!',
time: '',
isDate: false
}
]
}; };
// FORCE REFRESH: Trigger on ANY new websocket message const callHistory = [
useEffect(() => { {
const currentLen = wsMessages.length; id: 1,
const lastProcessed = processedMsgCount.current; name: 'Kalai',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
if (currentLen > lastProcessed) { status: 'Incoming call',
const newWsMsgs = wsMessages.slice(lastProcessed); time: '10 : 00 AM',
console.log(`[WS-REAL-TIME] New messages: ${newWsMsgs.length} (Total: ${currentLen}, Last Processed: ${lastProcessed})`); date: 'Today'
},
let shouldRefreshContacts = false; {
let shouldRefreshMessages = false; id: 2,
name: 'Lia',
newWsMsgs.forEach(lastMsg => { avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lia',
// Skip pings and internal Pusher system events status: 'Outgoing',
if (lastMsg.event?.startsWith('pusher:')) return; time: '10 : 00 AM',
if (lastMsg.event === 'pusher_internal:subscription_succeeded') return; date: 'Today'
},
console.log(`[WS-REAL-TIME] Processing: ${lastMsg.event}`); {
id: 3,
try { name: 'Moi',
const data = lastMsg.data; avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Moi',
const parsedData = typeof data === 'string' ? JSON.parse(data) : data; status: 'Incoming call',
const msgObj = parsedData.message || parsedData.data || parsedData; time: '10 : 00 AM',
date: 'Today'
// LENIENT STRATEGY: If event name suggests a message or if we have data, refresh! },
const isMessageEvent = lastMsg.event?.toLowerCase().includes('message') || {
lastMsg.event?.toLowerCase().includes('chat'); id: 4,
name: 'Sri',
if (isMessageEvent || (msgObj && (msgObj.id || msgObj.message || msgObj.chat_id))) { avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sri',
console.log("[WS-REAL-TIME] Match found!"); status: 'Outgoing',
shouldRefreshContacts = true; time: '10 : 00 AM',
date: 'Today'
// OPTIMISTIC UPDATE: Update the contact list snippet locally for instant feedback },
if (msgObj && msgObj.message) { {
setContacts(prev => { id: 5,
const targetId = msgObj.chat_id || msgObj.sender_id || selectedChat; name: 'Kalai',
return prev.map(c => { avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
if (c.id == targetId) { status: 'Outgoing',
return { time: '10 : 00 AM',
...c, date: 'Today'
last_message: msgObj.message, },
last_message_time: msgObj.time || new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), {
unread_count: (c.id != selectedChat) ? (parseInt(c.unread_count || 0) + 1) : c.unread_count id: 6,
}; name: 'Kalai',
} avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
return c; status: 'Outgoing',
}); time: '10 : 00 AM',
}); date: 'Today'
},
{
id: 7,
name: 'Kalai',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
status: 'Outgoing',
time: '10 : 00 AM',
date: 'Today'
},
{
id: 8,
name: 'Kalai',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
status: 'Outgoing',
time: '10 : 00 AM',
date: 'Today'
} }
];
if (selectedChat) { const [chatMessages, setChatMessages] = useState({
shouldRefreshMessages = true; 1: [
{ id: 1, sender: 'other', text: "Let's do it! I'm in a meeting until noon.", time: '10 Nov', isDate: false , read: false },
{ id: 2, sender: 'me', text: "That's perfect! There's a new place...", time: '07:21 am', isDate: false , read: false },
{ id: 3, sender: 'date', text: 'Today', isDate: true, read: true | false },
{ id: 4, sender: 'me', text: "Can's get lunch. How about tomorrow?", time: '09:42 am', isDate: false , read: true },
{ id: 5, sender: 'other', text: "Let's do it! I'm in a meeting until noon.", time: '', isDate: false, read: true },
{ id: 6, sender: 'me', text: "That's perfect! There's a new place...", time: '', isDate: false, read: true },
],
// 2,3,... if needed
});
const handleSendMessage = () => {
if (!message.trim() || !selectedChat) return;
const now = new Date();
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
// Manual injection for instant UI update
if (msgObj && (msgObj.id || msgObj.message)) {
setChatMessages(prev => { setChatMessages(prev => {
const isDuplicate = prev.some(m => m.id === msgObj.id); const prevMsgs = prev[selectedChat] || [];
if (isDuplicate) return prev; const newMsg = {
const sanitizedMsg = { id: prevMsgs.length ? prevMsgs[prevMsgs.length - 1].id + 1 : 1,
...msgObj, sender: 'me',
chat_by: msgObj.chat_by || (msgObj.sender_id == profileId ? 'me' : 'them'), text: message.trim(),
time: msgObj.time || new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) time: timeString,
}; isDate: false,
return [...prev, sanitizedMsg];
});
}
}
}
} catch (e) {
console.error("[WS-REAL-TIME] Error:", e);
}
});
if (shouldRefreshMessages && selectedChat) {
fetchMessages(selectedChat, true);
}
if (shouldRefreshContacts) {
// Primary refresh after 800ms
const timer1 = setTimeout(() => {
fetchContacts(searchTerm, true);
queryClient.invalidateQueries({ queryKey: ["unreadChatCount"] });
queryClient.invalidateQueries({ queryKey: ["notificationCount"] });
}, 800);
// Secondary "safety" refresh after 3 seconds
const timer2 = setTimeout(() => {
fetchContacts(searchTerm, true);
}, 3000);
// Keep track of timers if needed, but for simplicity here we just use the count
}
// ALWAYS update the ref if we have new messages, even if they were skipped/invalid
processedMsgCount.current = currentLen;
}
}, [wsMessages, fetchContacts, searchTerm, queryClient, selectedChat, profileId]);
useEffect(() => {
fetchContacts();
}, []);
useEffect(() => {
if (selectedChat) {
fetchMessages(selectedChat);
}
}, [selectedChat]);
useEffect(() => {
if (isPaginating.current) {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
const newScrollHeight = container.scrollHeight;
const heightDiff = newScrollHeight - previousScrollHeight.current;
container.scrollTop = heightDiff;
}
isPaginating.current = false;
return;
}
scrollToBottom();
}, [chatMessages]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
}; };
const handleSendMessage = async () => {
if (!message.trim() || !selectedChat || sendingMessage) return;
setSendingMessage(true);
try {
const msgText = message.trim();
const response = await sendMessage(selectedChat, msgText);
if (response.status) {
// Optimistically update the contact list locally for instant feedback
setContacts(prev => prev.map(c => {
if (c.id == selectedChat) {
return { return {
...c, ...prev,
last_message: msgText, [selectedChat]: [...prevMsgs, newMsg],
last_message_time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}; };
} });
return c;
}));
// Refresh messages and contacts silently in the background
setMessage(''); setMessage('');
fetchMessages(selectedChat, true);
fetchContacts(searchTerm, true);
}
} catch (error) {
toast.error("Failed to send message");
} finally {
setSendingMessage(false);
}
}; };
const handleChatSelect = (contactId) => { const handleChatSelect = (contactId) => {
setSelectedChat(contactId); setSelectedChat(contactId);
setShowChatOnMobile(true); setShowChatOnMobile(true);
@ -348,12 +265,15 @@ const ChatUI = () => {
}; };
return ( return (
<> <>
<ReportModal open={openReport} onClose={() => setOpenReport(false)} /> <ReportModal open={openReport} onClose={() => setOpenReport(false)} />
<div className="w-full max-w-[1400px] mx-auto flex h-[85vh] gap-[20px] bg-gray-50 my-4"> <div className="w-full max-w-[1400px] mx-auto flex h-screen gap-[20px] bg-gray-50">
{/* Sidebar - Chat List */} {/* Sidebar - Chat List */}
<div key="chat-sidebar" className={`w-full md:w-96 bg-white border border-1 border-gray-200 rounded-[10px] flex flex-col ${ <div className={`w-full md:w-96 bg-white border border-1 border-gray-200 rounded-[10px] flex flex-col ${
showChatOnMobile || showCallHistory ? 'hidden md:flex' : 'flex' showChatOnMobile || showCallHistory ? 'hidden md:flex' : 'flex'
}`}> }`}>
{/* Header */} {/* Header */}
@ -391,28 +311,16 @@ const ChatUI = () => {
<input <input
type="text" type="text"
placeholder="Search your partner here..." placeholder="Search your partner here..."
value={searchTerm} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500"
onChange={(e) => {
setSearchTerm(e.target.value);
fetchContacts(e.target.value);
}}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-[#034E08]"
/> />
</div> </div>
</div> </div>
{/* Contact List */} {/* Contact List */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{loadingContacts ? ( {contacts.map((contact) => (
<div className="flex justify-center p-8">
<Loader2 className="w-6 h-6 animate-spin text-[#034E08]" />
</div>
) : contacts.length === 0 ? (
<div className="p-8 text-center text-gray-500">No chats found</div>
) : (
contacts.map((contact, index) => (
<div <div
key={`contact-${contact.id}-${index}`} key={contact.id}
onClick={() => handleChatSelect(contact.id)} onClick={() => handleChatSelect(contact.id)}
className={`flex items-center gap-3 p-4 cursor-pointer hover:bg-gray-50 border-b border-gray-100 ${ className={`flex items-center gap-3 p-4 cursor-pointer hover:bg-gray-50 border-b border-gray-100 ${
selectedChat === contact.id ? 'bg-blue-50' : '' selectedChat === contact.id ? 'bg-blue-50' : ''
@ -420,39 +328,109 @@ const ChatUI = () => {
> >
<div className="relative"> <div className="relative">
<img <img
src={contact.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"} src={contact.avatar}
alt={contact.name} alt={contact.name}
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full"
/> />
{contact.is_online && ( {contact.online && (
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div> <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div>
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<h3 className="font-medium text-gray-900 truncate">{contact.name}</h3> <h3 className="font-medium text-gray-900">{contact.name}</h3>
<span className="text-xs text-gray-500 whitespace-nowrap">{contact.time}</span> <span className="text-xs text-gray-500">{contact.time}</span>
</div> </div>
<div className="flex items-center justify-between"> <p className="text-sm text-gray-600 truncate">
<p className="text-sm text-gray-600 truncate mr-2"> {contact.lastMessage}
{contact.latest_message}
</p> </p>
{contact.unread_message_count > 0 && (
<span className="bg-[#034E08] text-white text-[10px] rounded-full w-4 h-4 flex items-center justify-center">
{contact.unread_message_count}
</span>
)}
</div> </div>
</div> </div>
</div> ))}
))
)}
</div> </div>
</div> </div>
{/* Call History View */}
{showCallHistory && (
<div className={`flex-1 bg-white ${showCallHistory ? 'flex' : 'hidden md:flex'} flex-col`}>
{/* Call History Header */}
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
onClick={handleBackToList}
className="md:hidden p-2 hover:bg-gray-100 rounded"
>
<ArrowLeft className="w-5 h-5 text-gray-600" />
</button>
<div className="flex items-center gap-3">
<img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=User"
alt="User"
className="w-10 h-10 rounded-full"
/>
<div>
<h2 className="font-semibold">Dalahamanner-Hv</h2>
<p className="text-xs text-gray-500">ID: TKS258AA</p>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button className="p-2 hover:bg-gray-100 rounded">
<Phone className="w-5 h-5 text-blue-600" />
</button>
<button className="p-2 hover:bg-gray-100 rounded">
<MoreVertical className="w-5 h-5" />
</button>
</div>
</div>
</div>
{/* Filter Tabs */}
<div className="flex gap-2 p-4 border-b border-gray-200">
<button className="px-4 py-1.5 bg-red-500 text-white rounded-full text-sm font-medium">
All
</button>
<button className="px-4 py-1.5 bg-gray-100 text-gray-700 rounded-full text-sm font-medium hover:bg-gray-200">
Incoming Call
</button>
<button className="px-4 py-1.5 bg-gray-100 text-gray-700 rounded-full text-sm font-medium hover:bg-gray-200">
Outgoing
</button>
</div>
{/* Call History List */}
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Today</h3>
{callHistory.map((call) => (
<div
key={call.id}
className="flex items-center gap-3 py-3 border-b border-gray-100"
>
<img
src={call.avatar}
alt={call.name}
className="w-12 h-12 rounded-full"
/>
<div className="flex-1">
<h4 className="font-medium text-gray-900">{call.name}</h4>
<div className="flex items-center gap-2 text-sm text-gray-500">
<Phone className="w-3 h-3" />
<span>{call.status}</span>
</div>
</div>
<span className="text-xs text-gray-500">{call.time}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Chat Area */} {/* Chat Area */}
{selectedChat && !showCallHistory && ( {selectedChat && !showCallHistory && (
<div key="chat-main-area" className={`border border-1 border-gray-200 rounded-[10px] flex-1 flex flex-col bg-white ${ <div className={`border border-1 border-gray-200 rounded-[10px] flex-1 flex flex-col bg-white ${
showChatOnMobile ? 'flex' : 'hidden md:flex' showChatOnMobile ? 'flex' : 'hidden md:flex'
}`}> }`}>
{/* Chat Header */} {/* Chat Header */}
@ -465,21 +443,24 @@ const ChatUI = () => {
<ArrowLeft className="w-5 h-5 text-gray-600" /> <ArrowLeft className="w-5 h-5 text-gray-600" />
</button> </button>
<img <img
src={chatDetails?.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"} src={contacts.find(c => c.id === selectedChat)?.avatar}
alt="Avatar" alt="Avatar"
className="w-10 h-10 rounded-full object-cover" className="w-10 h-10 rounded-full"
/> />
<div> <div>
<h3 className="font-medium text-gray-900"> <h3 className="font-medium text-gray-900">
{chatDetails?.name} Priya
</h3> </h3>
<div className={`flex items-center gap-1 text-xs ${chatDetails?.lastSeen === 'Online' ? 'text-green-500' : 'text-gray-400'}`}> <div className="flex items-center gap-1 text-xs text-green-500">
<div className={`w-2 h-2 rounded-full ${chatDetails?.lastSeen === 'Online' ? 'bg-green-500' : 'bg-gray-300'}`}></div> <div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{chatDetails?.lastSeen || 'Offline'}</span> <span>Online</span>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* <button className="p-2 hover:bg-gray-100 rounded">
<Phone className="w-5 h-5 text-blue-600" />
</button> */}
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowChatMenu(!showChatMenu)} onClick={() => setShowChatMenu(!showChatMenu)}
@ -513,69 +494,60 @@ const ChatUI = () => {
</div> </div>
{/* Messages */} {/* Messages */}
<div <div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 md:h-[400px]">
ref={scrollContainerRef} {chatMessages[selectedChat]?.map((msg) => (
className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50" <div key={msg.id}>
> {msg.isDate ? (
{/* Top Marker for Intersection Observer */} <div className="text-center text-xs text-gray-500 my-4">
<div ref={topMarkerRef} className="h-1" /> {msg.text}
{loadingMore && (
<div className="flex justify-center p-2">
<div className="flex items-center gap-2 text-sm text-gray-500">
<Loader2 className="w-4 h-4 animate-spin" />
<span>Loading older messages...</span>
</div>
</div>
)}
{hasMore && !loadingMore && chatMessages.length > 0 && (
<div className="flex justify-center">
<button
onClick={loadMoreMessages}
className="text-xs text-blue-600 hover:underline py-1"
>
Load older messages
</button>
</div>
)}
{loadingMessages ? (
<div className="flex justify-center p-8">
<Loader2 className="w-6 h-6 animate-spin text-[#034E08]" />
</div> </div>
) : ( ) : (
chatMessages.map((msg, index) => (
<div <div
key={`msg-${msg.id}-${index}`} className={`flex ${
className={`flex ${msg.chat_by === 'me' ? 'justify-end' : 'justify-start'}`} msg.sender === 'me' ? 'justify-end' : 'justify-start'
}`}
> >
<div <div
className={`max-w-[75%] md:max-w-md px-4 py-2 rounded-2xl shadow-sm ${ className={`max-w-xs md:max-w-md px-4 py-2 rounded-2xl ${
msg.chat_by === 'me' msg.sender === 'me'
? 'bg-[#cbf5ea] text-gray-900 rounded-br-sm' ? 'bg-[#cbf5ea] text-gray-900 rounded-br-sm'
: 'bg-white text-gray-900 rounded-bl-sm' : 'bg-white text-gray-900 rounded-bl-sm'
}`} }`}
> >
<p className="text-sm break-words">{msg.message}</p> <p className="text-sm">{msg.text}</p>
<div className='flex gap-1 items-center justify-end mt-1'> <div className='flex gap-1 items-center justify-end mt-1'>
<span className="text-[10px] text-gray-500"> {msg.time && (
<div className="flex items-center justify-end ">
<span className={`text-xs ${msg.sender === 'me' ? 'text-gray-900' : 'text-gray-500'}`}>
{msg.time} {msg.time}
</span> </span>
{msg.chat_by === 'me' && (
<div className="flex items-center">
{msg.is_read === 1 ? (
<CheckCheck className="w-3.5 h-3.5 text-blue-500" /> </div>
)}
{msg.sender === 'me' && (
<div className="flex items-center justify-end">
<span className="text-xs text-blue-100">{msg.time}</span>
{msg.read ? (
<CheckCheck className="w-4 h-4 text-[#034E08]" />
) : ( ) : (
<Check className="w-3.5 h-3.5 text-gray-400" /> <Check className="w-4 h-4 text-[#034E08]" />
)} )}
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
))
)} )}
<div ref={messagesEndRef} /> </div>
))}
</div> </div>
{/* Message Input */} {/* Message Input */}
@ -587,53 +559,28 @@ const ChatUI = () => {
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Start Typing..." placeholder="Start Typing..."
disabled={chatDetails?.disable_chat} className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:border-[#034E08]"
className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:border-[#034E08] disabled:bg-gray-100 disabled:cursor-not-allowed"
/> />
<button <button
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={!message.trim() || sendingMessage || chatDetails?.disable_chat} className="p-2.5 bg-[#034E08] text-white rounded-lg hover:bg-blue-600"
className="p-2.5 bg-[#034E08] text-white rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
> >
{sendingMessage ? <Loader2 className="w-5 h-5 animate-spin" /> : <Send className="w-5 h-5" />} <Send className="w-5 h-5" />
</button> </button>
</div> </div>
{chatDetails?.disable_chat && (
<p className="text-[10px] text-red-500 mt-1 text-center">Chat is currently disabled for this conversation.</p>
)}
</div> </div>
</div> </div>
)} )}
{/* Empty State */} {/* Empty State */}
{!selectedChat && !showCallHistory && ( {!selectedChat && !showCallHistory && (
<div key="chat-empty-state" className="flex-1 hidden md:flex items-center justify-center bg-gray-50"> <div className="flex-1 hidden md:flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="bg-white p-6 rounded-full shadow-sm mb-4 inline-block">
<MessageCircle className="w-12 h-12 text-[#034E08] opacity-20" />
</div>
<p className="text-gray-500">Select a conversation to start messaging</p> <p className="text-gray-500">Select a conversation to start messaging</p>
</div> </div>
</div>
)}
{/* Call History Placeholder (Keep existing or update as needed) */}
{showCallHistory && (
<div key="chat-call-history" className="flex-1 flex items-center justify-center bg-gray-50">
<div className="text-center">
<h2 className="text-xl font-semibold mb-2">Call History</h2>
<p className="text-gray-500">Feature coming soon</p>
<button
onClick={handleBackToList}
className="mt-4 px-6 py-2 bg-[#034E08] text-white rounded-lg"
>
Back to Chat
</button>
</div>
</div>
)} )}
</div> </div>
</> </>
); );
}; };

View File

@ -3,7 +3,6 @@ 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';
@ -40,13 +39,7 @@ 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,58 +1,19 @@
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 data={data} /> <MatrimonyProfile/>
<PartnerPreferences data={data} /> <PartnerPreferences/>
<MatchingList matches={data.all_matches} /> <MatchingList/>
</div> </div>
</> </>
); )
}; }
export default ProfileDetailPage; export default ProfileDetailPage

View File

@ -11,8 +11,7 @@ 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"));
@ -49,15 +48,6 @@ 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">
@ -102,11 +92,11 @@ const UserDashboardHome = () => {
}, },
}} }}
> >
{sliderImages.map((slide, idx) => ( {images.map((img, idx) => (
<SwiperSlide key={slide.id || idx}> <SwiperSlide key={idx}>
<div className="relative overflow-hidden rounded-3xl w-[100%]"> <div className="relative overflow-hidden rounded-3xl w-[100%]">
<img <img
src={slide.image} src={img}
alt={`Slide ${idx + 1}`} alt={`Slide ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
loading={idx === 0 ? "eager" : "lazy"} loading={idx === 0 ? "eager" : "lazy"}
@ -216,32 +206,28 @@ const UserDashboardHome = () => {
`}</style> `}</style>
</div> </div>
<Suspense fallback={<SectionFallback height={320} />}> <Suspense fallback={<SectionFallback height={320} />}>
<Profilecardemo profiles={dashboardData?.daily_recommended} /> <Profilecardemo />
</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 articles={dashboardData?.blogs} /> <MatrimonyArticles />
</Suspense> </Suspense>
<Suspense fallback={<SectionFallback height={320} />}> <Suspense fallback={<SectionFallback height={320} />}>
<MatchingList matches={dashboardData?.all_matches} /> <MatchingList />
</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 videos={dashboardData?.youtube_videos} /> <VideoSwiperGallery />
</Suspense> </Suspense>
</> </>
) )

View File

@ -74,13 +74,6 @@ 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,27 +1,29 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
const initialState = { const initialState = {
from_age: 18, age: [18, 70],
to_age: 70, height: [4.0, 7.11],
from_height: 4.0, maritalStatus: "All Profile",
to_height: 7.11, motherTongue: [],
marital_status: [], religion: "Hindu",
religion: [], matchesWithHoroscope: false,
caste: [], caste: [],
sub_caste: [], subCaste: [],
star: [], star: "Any",
occupation: [], dasham: "Doesn't matter",
annual_income: [], occupation: "Any",
employee_type: [], annualIncome: "Any",
education: [], employeeType: "Any",
state: [], education: "Any",
district: [], state: "Any",
diet: "", country: "Any",
family_type: [], citizenship: "Any",
filter_type: "all_matches", eatingHabits: "Any",
page: 1, smokingHabits: "Doesn't matter",
isPaidMember: false, drinkingHabits: "Doesn't matter",
search: "", familyType: "Any",
familyStatus: "Any",
familyValue: "Any",
}; };
const filterSlice = createSlice({ const filterSlice = createSlice({
@ -29,24 +31,73 @@ const filterSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setAge: (state, action) => { setAge: (state, action) => {
state.from_age = action.payload[0]; state.age = action.payload;
state.to_age = action.payload[1];
}, },
setHeight: (state, action) => { setHeight: (state, action) => {
state.from_height = action.payload[0]; state.height = action.payload;
state.to_height = action.payload[1];
}, },
setMaritalStatus: (state, action) => { setMaritalStatus: (state, action) => {
state.marital_status = Array.isArray(action.payload) ? action.payload : [action.payload]; state.maritalStatus = action.payload;
},
setMotherTongue: (state, action) => {
state.motherTongue = action.payload;
}, },
setReligion: (state, action) => { setReligion: (state, action) => {
state.religion = Array.isArray(action.payload) ? action.payload : [action.payload]; state.religion = 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.sub_caste = action.payload; state.subCaste = action.payload;
},
setStar: (state, action) => {
state.star = action.payload;
},
setDasham: (state, action) => {
state.dasham = action.payload;
},
setOccupation: (state, action) => {
state.occupation = action.payload;
},
setAnnualIncome: (state, action) => {
state.annualIncome = action.payload;
},
setEmployeeType: (state, action) => {
state.employeeType = action.payload;
},
setEducation: (state, action) => {
state.education = action.payload;
},
setState: (state, action) => {
state.state = action.payload;
},
setCountry: (state, action) => {
state.country = action.payload;
},
setCitizenship: (state, action) => {
state.citizenship = action.payload;
},
setEatingHabits: (state, action) => {
state.eatingHabits = action.payload;
},
setSmokingHabits: (state, action) => {
state.smokingHabits = action.payload;
},
setDrinkingHabits: (state, action) => {
state.drinkingHabits = action.payload;
},
setFamilyType: (state, action) => {
state.familyType = action.payload;
},
setFamilyStatus: (state, action) => {
state.familyStatus = action.payload;
},
setFamilyValue: (state, action) => {
state.familyValue = action.payload;
}, },
// universal update // universal update
@ -54,16 +105,7 @@ const filterSlice = createSlice({
return { ...state, ...action.payload }; return { ...state, ...action.payload };
}, },
resetFilters: (state) => { resetFilters: () => initialState,
// 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;
},
}, },
}); });
@ -71,13 +113,28 @@ 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,72 +5,49 @@ const registrationformSlice = createSlice({
initialState: { initialState: {
personalDetails: { personalDetails: {
name: "", name: "",
mobile: "", mobileNumber: "",
email: "",
gender: "", gender: "",
dob: "",
height: "", height: "",
weight: "", weight: "",
marital_status: "", maritalStatus: "",
religion: 1, // Default Hindu religion: "",
profile_for: "", profileFor: "",
caste: 1, // Default Naidu caste: "",
sub_caste: "", subCaste: "",
willing_to_marry: "",
inter_caste_parents: "",
inter_caste_parents_details: "",
gothram: "", gothram: "",
do_you_speak_telugu: "",
about_us: "",
known_languages: [],
mother_language: "",
complexion: "",
physical_status: "",
password: "",
confirmPassword: "",
dob: "",
raasi: "", raasi: "",
star: "", star: "",
bloodGroup: "",
email: "",
password: "",
confirmPassword: "",
state: "", state: "",
city: "", city: "",
pincode: "", pincode: "",
profiles: [], profiles: [],
verifiedMobileNumber: "",
}, },
educationalDetails: { educationalDetails: {
study_field: "", collegeName: "",
education: "", employeeType: "",
education_detail: "", qualification: "",
college_name: "", fieldOfStudy: "",
employee_type: "",
occupation: "", occupation: "",
occupation_detail: "", organization: "",
company_name: "", income: "",
income_currency: "INR", workLocation: "",
annual_income: "",
work_country: 1,
work_city: "",
work_state: "",
work_district: "",
address: "",
}, },
familyDetails: { familyDetails: {
fatherName: "", fatherName: "",
fatherOccupation: "", fatherOccupation: "",
motherName: "", motherName: "",
motherOccupation: "", motherOccupation: "",
brotherCount: "", brotherCount: 0,
sisterCount: "", sisterCount: 0,
brothers: [], brothers: [],
sisters: [], sisters: [],
familyStatus: "", familyStatus: "",
nativePlace: "", nativePlace: "",
familyCountry: "",
familyState: "",
familyDistrict: "",
familyCity: "",
address: "",
expectationDetails: "",
willingToGoAbroad: "",
}, },
lifestyleDetails: { lifestyleDetails: {
diets: [], diets: [],
@ -137,38 +114,14 @@ const registrationformSlice = createSlice({
state.partnerPreferences = { ...state.partnerPreferences, ...action.payload }; state.partnerPreferences = { ...state.partnerPreferences, ...action.payload };
}, },
clearAllStepsFrom: (state, action) => { submitForm: (state) => {
const step = action.payload; console.log("Form Submitted:", {
if (step <= 2) { personalDetails: state.personalDetails,
state.educationalDetails = { educationalDetails: state.educationalDetails,
study_field: "", education: "", education_detail: "", college_name: "", familyDetails: state.familyDetails,
employee_type: "", occupation: "", occupation_detail: "", company_name: "", lifestyleDetails: state.lifestyleDetails,
income_currency: "INR", annual_income: "", work_country: "", work_state: "", partnerPreferences: state.partnerPreferences,
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: [],
};
}
}, },
@ -326,7 +279,6 @@ export const {
updateFamilyDetails, updateFamilyDetails,
updateLifestyleDetails, updateLifestyleDetails,
updatePartnerPreferences, updatePartnerPreferences,
clearAllStepsFrom,
submitForm, submitForm,
preloadDummyProfile, preloadDummyProfile,
} = registrationformSlice.actions; } = registrationformSlice.actions;

View File

@ -1,36 +1,9 @@
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: persistedFiltersReducer, filters:filtersReducer,
}, },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
}); });
export const persistor = persistStore(store);

View File

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

View File

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

View File

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

@ -1,65 +0,0 @@
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

@ -1,28 +0,0 @@
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 */}
{import.meta.env.DEV && this.state.error && ( {process.env.NODE_ENV === "development" && 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,12 +1,7 @@
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(),],
@ -16,4 +11,3 @@ export default defineConfig({
}, },
}, },
}) })