Build and deployment update: Refined registration forms, stepper navigation, and chat synchronization fixes

This commit is contained in:
MAGESHWARAN 2026-04-25 10:22:35 +05:30
parent 68f97c40dc
commit c467271927
27 changed files with 3461 additions and 4178 deletions

BIN
dist.zip

Binary file not shown.

View File

@ -57,9 +57,15 @@ NOTIFICATION_COUNT:"notification/un_read_count",
PROFILES_FILTER_LIST: "profiles/lists", PROFILES_FILTER_LIST: "profiles/lists",
PROFILES_FILTER_MASTER: "profiles/filter/masters", PROFILES_FILTER_MASTER: "profiles/filter/masters",
DASHBOARD_API: "dashboard", DASHBOARD_API: "dashboard",
HEADER_API: "header_data", HEADER_API: "header_data",
SHORTLIST_API: "shortlist_profile", 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

@ -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

@ -4,6 +4,7 @@ import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import SwipeableDrawer from "@mui/material/SwipeableDrawer"; import SwipeableDrawer from "@mui/material/SwipeableDrawer";
import { useWebSocket } from "../../hooks/useWebSocket";
import List from "@mui/material/List"; import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from "@mui/material/ListItemButton";
@ -19,7 +20,7 @@ import Button from "@mui/material/Button";
import LazyImage from "./LazyImage"; import LazyImage from "./LazyImage";
import Logo from "../../assets/images/logo.png"; import Logo from "../../assets/images/logo.png";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect, useMemo } from "react";
import { useTheme, useMediaQuery, ListItemIcon } from "@mui/material"; import { useTheme, useMediaQuery, ListItemIcon } from "@mui/material";
import { Home, Users, Heart, MessageCircle, Search, Bell } from "lucide-react"; import { Home, Users, Heart, MessageCircle, Search, Bell } from "lucide-react";
import { isAuthenticated } from "../../utills/auth"; import { isAuthenticated } from "../../utills/auth";
@ -27,7 +28,7 @@ import userimg from "../../assets/images/bride1.jpg"
import axiosInstance, { logoutAPI } from "../../api/axiosInstance"; import axiosInstance, { logoutAPI } from "../../api/axiosInstance";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { API_ENDPOINTS } from "../../api/apiEndpoints"; import { API_ENDPOINTS } from "../../api/apiEndpoints";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { getHeaderDetails } from "../../api/preview.api"; import { getHeaderDetails } from "../../api/preview.api";
const NAV_LINKS = [ const NAV_LINKS = [
@ -160,6 +161,7 @@ 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 { personalDetails } = useSelector((state) => state.registerform);
@ -189,17 +191,104 @@ 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({ const { data: headerData } = useQuery({
queryKey: ["headerDetails"], queryKey: ["headerDetails"],
queryFn: getHeaderDetails, queryFn: getHeaderDetails,
enabled: !!auth, 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 apiProfileImage = headerData?.myDetails?.profile;
const handleMenuClick = (item) => { const handleMenuClick = (item) => {
@ -327,10 +416,17 @@ const ProfileHeader = () => {
{getNavIcon(index)} {getNavIcon(index)}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={ <ListItemText primary={
<div className="flex items-center justify-between"> <div className="flex items-center justify-between w-full pr-4">
{label} <span>{label}</span>
{label === "Notifications" && notificationCount > 0 && ( {label === "Notifications" && notificationCount > 0 && (
<span className="bg-red-600 text-white text-xs px-2 py-0.5 rounded-full">{notificationCount}</span> <span className="bg-red-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] flex items-center justify-center ml-2">
{notificationCount}
</span>
)}
{label === "Messages" && chatCount > 0 && (
<span className="bg-red-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] flex items-center justify-center ml-2">
{chatCount}
</span>
)} )}
</div> </div>
} /> } />
@ -376,7 +472,10 @@ const ProfileHeader = () => {
items={NAV_LINKS.map(link => link.label)} items={NAV_LINKS.map(link => link.label)}
color="#034E08" color="#034E08"
activeItem={currentLabel} activeItem={currentLabel}
badges={{ "Notifications": notificationCount }} badges={{
"Notifications": notificationCount,
"Messages": chatCount
}}
onItemClick={(item) => { onItemClick={(item) => {
setSelectedItem(item); setSelectedItem(item);
const link = NAV_LINKS.find(l => l.label === item); const link = NAV_LINKS.find(l => l.label === item);
@ -386,7 +485,7 @@ const ProfileHeader = () => {
</Box> </Box>
{(auth ? ( {(auth ? (
<Box sx={{ flexGrow: 0 }}> <Box key="user-menu-box" sx={{ flexGrow: 0 }}>
<Tooltip title="Account Menu"> <Tooltip title="Account Menu">
<IconButton onClick={toggleProfileDrawer(true)}> <IconButton onClick={toggleProfileDrawer(true)}>
<Avatar sx={{width:"50px", height:"50px"}} src={apiProfileImage || profileImage || userimg || "/static/images/avatar/2.jpg" }/> <Avatar sx={{width:"50px", height:"50px"}} src={apiProfileImage || profileImage || userimg || "/static/images/avatar/2.jpg" }/>
@ -395,7 +494,7 @@ const ProfileHeader = () => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
):( <button className="ml-1 bg-red-900 text-white px-4 py-2 rounded-md hover:bg-red-800 transition-colors" ):( <button key="sign-in-btn" className="ml-1 bg-red-900 text-white px-4 py-2 rounded-md hover:bg-red-800 transition-colors"
onClick={() => navigate("/login")}>Sign In / Sign Up</button>))} onClick={() => navigate("/login")}>Sign In / Sign Up</button>))}

View File

@ -125,7 +125,7 @@ const AppPromoteSection = () => {
className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16 max-w-6xl mx-auto" className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16 max-w-6xl mx-auto"
> >
{features.map((feature, index) => ( {features.map((feature, index) => (
<div className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden"> <div key={index} className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
<BorderBeam <BorderBeam
colorFrom="#ff0000ff" colorFrom="#ff0000ff"
colorTo="#338105ff" colorTo="#338105ff"

View File

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

View File

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

View File

@ -84,14 +84,6 @@ const ProfileCard = ({ profile }) => {
const zodiac2 = profile.star_name || profile.zodiac2 || null; const zodiac2 = profile.star_name || profile.zodiac2 || null;
const isPremium = profile.is_paid_member !== undefined ? profile.is_paid_member === 1 : profile.isPremium; const isPremium = profile.is_paid_member !== undefined ? profile.is_paid_member === 1 : profile.isPremium;
const NewJoinedProfile = ({ profiles }) => {
const swiperRef = useRef(null);
const navigate = useNavigate();
const dispatch = useDispatch();
const displayProfiles = profiles || [];
if (displayProfiles.length === 0) return null;
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
@ -167,7 +159,7 @@ const NewJoinedProfile = ({ profiles }) => {
</div> </div>
<div className="flex gap-4 mt-[15px] justify-center"> <div className="flex gap-4 mt-[15px] justify-center">
<button {/* <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" : ""}`} 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) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -176,7 +168,7 @@ const NewJoinedProfile = ({ profiles }) => {
disabled={declineMutation.isPending} disabled={declineMutation.isPending}
> >
<X size={18} /> {declineMutation.isPending ? "..." : "Decline"} <X size={18} /> {declineMutation.isPending ? "..." : "Decline"}
</button> </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={`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" : ""}`}
@ -191,9 +183,17 @@ const NewJoinedProfile = ({ profiles }) => {
</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 (
<> <>
@ -299,7 +299,7 @@ const NewJoinedProfile = ({ profiles }) => {
</div> </div>
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style jsx global>{` <style>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

@ -216,7 +216,7 @@ const VideoSwiperGallery = ({ videos }) => {
{selectedVideo && <VideoModal />} {selectedVideo && <VideoModal />}
{/* Custom Swiper Styles */} {/* Custom Swiper Styles */}
<style jsx global>{` <style>{`
.swiper-pagination-bullet { .swiper-pagination-bullet {
width: 10px; width: 10px;
height: 10px; height: 10px;

View File

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

View File

@ -1,24 +1,29 @@
import React from 'react'; import React from 'react';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
const PartnerPreferences = () => { const PartnerPreferences = ({ data }) => {
if (!data) return null;
const pref = data.preferedDetails;
const matchDetails = data.mutual_match.my_preferences_match;
const overallMatch = data.mutual_match.overall_match_percentage;
const basicPreferences = [ const basicPreferences = [
{ label: "Preferred Bride's Age", value: "22-29 yrs", match: true }, { label: "Preferred Groom's Age", value: pref.preferred_age_range || "Any", match: matchDetails.age },
{ label: "Preferred Height", value: "5'0\" - 5'5\"", match: false }, { label: "Preferred Height", value: (pref.preferred_height_from && pref.preferred_height_to) ? `${pref.preferred_height_from} - ${pref.preferred_height_to} ft` : "Any", match: matchDetails.height },
{ label: "Preferred Marital Status", value: "Never Married", match: true }, { label: "Preferred Marital Status", value: pref.preferred_marital_statuses?.join(", ") || "Any", match: matchDetails.marital_status },
{ label: "Preferred Mother Tongue", value: "Tamil", match: true }, { label: "Preferred Mother Tongue", value: pref.preferred_mother_tongues?.join(", ") || "Any", match: matchDetails.mother_tongue },
{ label: "Preferred Physical Status", value: "Normal", match: true }, { label: "Preferred Education", value: pref.preferred_educations?.join(", ") || "Any", match: matchDetails.education },
{ label: "Preferred Eating Habits", value: "Vegetarian", match: false }, { label: "Preferred Employee Type", value: pref.preferred_employee_types?.join(", ") || "Any", match: true }, // Not in matchDetails?
{ label: "Preferred Smoking Habits", value: "Doesn't Matter", match: true },
{ label: "Preferred Drinking Habits", value: "Doesn't Matter", match: true },
]; ];
const religiousPreferences = [ const religiousPreferences = [
{ label: "Preferred Religion", value: "Hindu", match: true }, { label: "Preferred Caste", value: pref.preferred_castes?.join(", ") || "Any", match: matchDetails.caste },
{ label: "Preferred Caste", value: "Brahmin - Iyer", match: false }, { label: "Preferred Sub-caste", value: pref.preferred_sub_castes?.join(", ") || "Any", match: matchDetails.sub_caste },
{ label: "Preferred Subcaste", value: "Any", match: false }, { label: "Preferred State", value: pref.preferred_states?.join(", ") || "Any", match: true },
{ label: "Preferred Star", value: "Any", match: true }, { label: "Preferred City", value: pref.preferred_districts?.join(", ") || "Any", match: true },
{ label: "Preferred Dosham", value: "No Dosham", match: true }, { label: "Preferred Occupation", value: pref.preferred_occupations?.join(", ") || "Any", match: matchDetails.occupation },
{ label: "Preferred Annual Income", value: pref.preferred_annual_income || "Any", match: matchDetails.annual_income },
]; ];
const PreferenceItem = ({ label, value, match }) => ( const PreferenceItem = ({ label, value, match }) => (
@ -46,7 +51,7 @@ const PartnerPreferences = () => {
<div className="text-center mb-6 "> <div className="text-center mb-6 ">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-800 mb-2 flex items-center justify-center gap-2"> <h1 className="text-2xl sm:text-3xl font-bold text-gray-800 mb-2 flex items-center justify-center gap-2">
<span className="text-pink-400"></span> <span className="text-pink-400"></span>
His Partner Preferences Partner Preferences
<span className="text-pink-400"></span> <span className="text-pink-400"></span>
</h1> </h1>
</div> </div>
@ -56,38 +61,31 @@ const PartnerPreferences = () => {
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<img <img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=male1" src={data.my_profile || "https://api.dicebear.com/7.x/avataaars/svg?seed=male1"}
alt="Profile" alt="Your Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100" className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100 object-cover"
/> />
<div> <div>
<p className="text-gray-600 text-sm mb-1">You match</p> <p className="text-gray-600 text-sm mb-1">Overall Match Score</p>
<p className="text-2xl sm:text-3xl font-bold text-red-600"> <p className="text-2xl sm:text-3xl font-bold text-red-600">
14<span className="text-[#034E08]">/20</span> {overallMatch}<span className="text-[#034E08]">%</span>
</p> </p>
<p className="text-xs text-gray-500">of his preferences</p> <p className="text-xs text-gray-500">of preferences match</p>
</div> </div>
</div> </div>
<img <img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=female1" src={data.profile.profile_picture || "https://api.dicebear.com/7.x/avataaars/svg?seed=female1"}
alt="Your Profile" alt="Partner Profile"
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100" className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100 object-cover"
/> />
</div> </div>
</div> </div>
<div className='grid grid-cols-1 gap-2 md:grid-cols-2 mb-8 pt-4'> <div className='grid grid-cols-1 gap-2 md:grid-cols-2 mb-8 pt-4'>
{/* Basic Preferences Section */} {/* Basic Preferences Section */}
<div className="bg-white rounded-2xl shadow-lg overflow-hidden"> <div className="bg-white rounded-2xl shadow-lg overflow-hidden">
<div className="flex items-center justify-between mb-4 bg-[#f5fbff] pt-4 pb-4 px-6"> <div className="flex items-center justify-between mb-4 bg-[#f5fbff] pt-4 pb-4 px-6">
<h2 className="text-lg font-bold text-gray-800">Basic Preferences</h2> <h2 className="text-lg font-bold text-gray-800">Basic Preferences</h2>
<div 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) => (
@ -96,17 +94,10 @@ const PartnerPreferences = () => {
</div> </div>
</div> </div>
{/* Religious Preferences Section */} {/* Other 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">Religious Preferences</h2> <h2 className="text-lg font-bold text-gray-800">Professional & Location</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) => (
@ -115,6 +106,7 @@ const PartnerPreferences = () => {
</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,6 +1,6 @@
import React, { useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateEducationalDetails } from "../redux/registrationFormSlice"; import { updateEducationalDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import { import {
TextField, TextField,
Button, Button,
@ -9,456 +9,412 @@ import {
InputLabel, InputLabel,
Select, Select,
MenuItem, MenuItem,
FormHelperText,
InputAdornment,
Box,
Typography,
} from "@mui/material"; } from "@mui/material";
import { useEducationMasters, useEducationList } from "../hooks/useMasters"; import { useEducationMasters, useEducationList } from "../hooks/useMasters";
import { useCityMasters } from "../hooks/useDependentMasters";
import { toast } from "react-hot-toast";
const EducationalDetailsForm = ({ const EducationalDetailsForm = ({
onSubmitStep, onSubmitStep,
onSkipStep, onSkipStep,
errors, errors: externalErrors,
onFieldChange, isEditMode,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.educationalDetails); const data = useSelector((state) => state.registerform.educationalDetails);
const inputRef = useRef(null); const [localErrors, setLocalErrors] = useState({});
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>; const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: educationMasters, isLoading: isEducationMastersLoading } = const { data: educationMasters, isLoading: isEducationMastersLoading } =
useEducationMasters(); useEducationMasters();
const educationListQuery = useEducationList(data.fieldOfStudy); const educationListQuery = useEducationList(data.study_field);
const districtQuery = useCityMasters(data.work_state);
const studyFieldOptions = useMemo(() => { const studyFieldOptions = educationMasters?.studyFields || [];
const raw = educationMasters; const qualificationOptions = educationListQuery.data?.education || educationListQuery.data?.data || [];
if (!raw) return []; const occupationOptions = educationMasters?.occupation || [];
if (Array.isArray(raw)) return raw; const employeeTypeOptions = educationMasters?.employeeType || [];
return raw.studyFields || raw.study_fields || raw.fieldOfStudy || []; const countryOptions = educationMasters?.country || [];
}, [educationMasters]); const stateOptions = educationMasters?.state || [];
const districtOptions = districtQuery.data?.districts || districtQuery.data || [];
const qualificationOptions = useMemo(() => { const isUnemployed = data.employee_type === 11;
const raw = educationListQuery.data; const isIndia = Number(data.work_country) === 1;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.education || raw.data || [];
}, [educationListQuery.data]);
const occupationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.occupation || raw.occupations || [];
}, [educationMasters]);
const employeeTypeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.employeeType || raw.employee_type || [];
}, [educationMasters]);
const annualIncomeOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.annualIncome || raw.annual_income || [];
}, [educationMasters]);
const workLocationOptions = useMemo(() => {
const raw = educationMasters;
if (!raw) return [];
if (Array.isArray(raw)) return [];
return raw.workLocation || raw.work_location || raw.workLocations || [];
}, [educationMasters]);
const getOptionLabel = (item, fallback = "") => {
if (!item) return fallback;
if (typeof item === "string") return item;
return (
item.study_field_name ||
item.education_name ||
item.occupation_name ||
item.employee_type_name ||
item.annual_income_name ||
item.work_location_name ||
item.name ||
fallback
);
};
useEffect(() => {
inputRef.current?.focus();
}, []);
const handleChange = (field, value) => { const handleChange = (field, value) => {
const updates = { [field]: value }; const updates = { [field]: value };
const fieldsToClear = [field];
if (field === "fieldOfStudy") { if (field === "study_field") {
updates.qualification = ""; updates.education = "";
fieldsToClear.push("qualification");
} }
if (field === "work_country") {
updates.work_state = "";
updates.work_district = "";
updates.work_city = "";
}
if (field === "work_state") {
updates.work_district = "";
}
if (field === "employee_type" && value === 11) {
// Clear fields that will be hidden
updates.occupation = "";
updates.occupation_detail = "";
updates.company_name = "";
updates.annual_income = "";
updates.work_country = "";
updates.work_state = "";
updates.work_district = "";
updates.work_city = "";
}
dispatch(updateEducationalDetails(updates)); dispatch(updateEducationalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear); setLocalErrors((prev) => ({ ...prev, [field]: "" }));
if (!isEditMode) {
dispatch(clearAllStepsFrom(3));
}
};
const validateForm = () => {
const newErrors = {};
if (!data.study_field) newErrors.study_field = "Required";
if (!data.education) newErrors.education = "Required";
if (!data.education_detail) newErrors.education_detail = "Required";
if (!data.employee_type) newErrors.employee_type = "Required";
if (!isUnemployed) {
if (!data.occupation) newErrors.occupation = "Required";
if (!data.occupation_detail) newErrors.occupation_detail = "Required";
if (!data.income_currency) newErrors.income_currency = "Required";
if (!data.annual_income) newErrors.annual_income = "Required";
if (!data.work_country) newErrors.work_country = "Required";
if (isIndia) {
if (!data.work_state) newErrors.work_state = "Required";
if (!data.work_district) newErrors.work_district = "Required";
} else {
if (!data.work_city) newErrors.work_city = "Required";
}
}
if (!data.address) newErrors.address = "Required";
setLocalErrors(newErrors);
return newErrors;
};
const scrollToError = (errorMap) => {
const errorFields = Object.keys(errorMap);
if (errorFields.length > 0) {
const fieldId = errorFields[0];
const element = document.getElementById(fieldId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
const focusable = element.querySelector('[role="combobox"]') ||
element.querySelector('[role="button"]') ||
element.querySelector("input") ||
element.querySelector("select") ||
element;
if (focusable && typeof focusable.focus === "function") {
focusable.focus();
}
}, 300); // Reduced delay slightly for snappier feel
}
}
}; };
const handleSubmit = () => { const handleSubmit = () => {
console.log("Submitting educational details:", data); const freshErrors = validateForm();
if (Object.keys(freshErrors).length > 0) {
toast.error("Please fill all mandatory fields");
scrollToError(freshErrors);
return;
}
onSubmitStep(); onSubmitStep();
}; };
return ( return (
<>
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
{/* Field of Study */} {/* 1. Field of Study */}
<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]">Field of Study{requiredMark}</label>
Field of Study{requiredMark} <FormControl fullWidth error={Boolean(localErrors.study_field)} id="study_field">
</label> <InputLabel>Select Field of Study</InputLabel>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.fieldOfStudy)}
>
<InputLabel id="fieldOfStudy-label">
Select Field of Study
</InputLabel>
<Select <Select
labelId="fieldOfStudy-label" value={data.study_field}
label="Select Field of Study" label="Select Field of Study"
name="fieldOfStudy" onChange={(e) => handleChange("study_field", e.target.value)}
value={data.fieldOfStudy}
onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
inputRef={inputRef}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{studyFieldOptions.map((field) => ( {studyFieldOptions.map((opt) => (
<MenuItem key={field.id ?? field} value={field.id ?? field}> <MenuItem key={opt.id} value={opt.id}>{opt.study_field_name}</MenuItem>
{getOptionLabel(field, "Field of Study")}
</MenuItem>
))} ))}
</Select> </Select>
{errors.fieldOfStudy && ( {localErrors.study_field && <FormHelperText>{localErrors.study_field}</FormHelperText>}
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.fieldOfStudy}
</p>
)}
</FormControl> </FormControl>
</div> </div>
{/* Highest Qualification */} {/* 2. Highest Qualification */}
<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]">Highest Qualification{requiredMark}</label>
Highest Educational Qualification{requiredMark} <FormControl fullWidth error={Boolean(localErrors.education)} id="education" disabled={!data.study_field || educationListQuery.isLoading}>
</label> <InputLabel>Select Qualification</InputLabel>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.qualification)}
>
<InputLabel id="qualification-label">
Select Highest Qualification
</InputLabel>
<Select <Select
labelId="qualification-label" value={data.education}
label="Select Highest Qualification" label="Select Qualification"
name="qualification" onChange={(e) => handleChange("education", e.target.value)}
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) => ( {qualificationOptions.map((opt) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}> <MenuItem key={opt.id} value={opt.id}>{opt.education_name || opt.name}</MenuItem>
{getOptionLabel(item, "Qualification")}
</MenuItem>
))} ))}
</Select> </Select>
{errors.qualification && ( {localErrors.education && <FormHelperText>{localErrors.education}</FormHelperText>}
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.qualification}
</p>
)}
</FormControl> </FormControl>
</div> </div>
{/* College Name */} {/* 3. Education in Detail */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-2 md:col-span-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Education in Detail{requiredMark}</label>
College Name
</label>
<TextField <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 fullWidth
name="collegeName"
label="College Name"
value={data.collegeName}
onChange={(e) => handleChange("collegeName", e.target.value)}
error={Boolean(errors.collegeName)}
helperText={errors.collegeName}
placeholder="Enter College Name" placeholder="Enter College Name"
variant="outlined" value={data.college_name}
onChange={(e) => handleChange("college_name", e.target.value)}
/> />
</div> </div>
{/* Occupation */} {/* 5. Employee Type */}
<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]">Employee type{requiredMark}</label>
Occupation <FormControl fullWidth error={Boolean(localErrors.employee_type)} id="employee_type">
</label> <InputLabel>Select Employee Type</InputLabel>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.occupation)}
>
<InputLabel id="occupation-label">Select Occupation</InputLabel>
<Select <Select
labelId="occupation-label" value={data.employee_type}
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
</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
</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" label="Select Employee Type"
name="employeeType" onChange={(e) => handleChange("employee_type", e.target.value)}
value={data.employeeType}
onChange={(e) => handleChange("employeeType", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{employeeTypeOptions.map((item) => ( {employeeTypeOptions.map((opt) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}> <MenuItem key={opt.id} value={opt.id}>{opt.employee_type_name}</MenuItem>
{getOptionLabel(item, "Employee Type")}
</MenuItem>
))} ))}
</Select> </Select>
{errors.employeeType && ( {localErrors.employee_type && <FormHelperText>{localErrors.employee_type}</FormHelperText>}
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.employeeType}
</p>
)}
</FormControl> </FormControl>
</div> </div>
{/* Annual Income */} {!isUnemployed && (
<div className="flex flex-col gap-4"> <>
<label className="text-gray-900 text-[15px]"> {/* 6. Occupation */}
Annual Income <div className="flex flex-col gap-2">
</label> <label className="text-gray-900 text-[15px]">Occupation{requiredMark}</label>
<FormControl <FormControl fullWidth error={Boolean(localErrors.occupation)} id="occupation">
fullWidth <InputLabel>Select Occupation</InputLabel>
variant="outlined"
error={Boolean(errors.income)}
>
<InputLabel id="income-label">Select Annual Income</InputLabel>
<Select <Select
labelId="income-label" value={data.occupation}
label="Select Annual Income" label="Select Occupation"
name="income" onChange={(e) => handleChange("occupation", e.target.value)}
value={data.income}
onChange={(e) => handleChange("income", e.target.value)}
disabled={isEducationMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{annualIncomeOptions.map((item) => ( {occupationOptions.map((opt) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}> <MenuItem key={opt.id} value={opt.id}>{opt.occupation_name}</MenuItem>
{getOptionLabel(item, "Annual Income")}
</MenuItem>
))} ))}
</Select> </Select>
{errors.income && ( {localErrors.occupation && <FormHelperText>{localErrors.occupation}</FormHelperText>}
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.income}
</p>
)}
</FormControl> </FormControl>
</div> </div>
{/* Work Location */} {/* 7. Occupation in Detail */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-2 md:col-span-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Occupation in Detail{requiredMark}</label>
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 <TextField
id="occupation_detail"
fullWidth fullWidth
name="workLocation" multiline
label="Work Location" rows={3}
value={data.workLocation} placeholder="Enter your occupation details"
onChange={(e) => handleChange("workLocation", e.target.value)} value={data.occupation_detail}
error={Boolean(errors.workLocation)} onChange={(e) => handleChange("occupation_detail", e.target.value)}
helperText={errors.workLocation} error={Boolean(localErrors.occupation_detail)}
placeholder="Enter Work Location" helperText={localErrors.occupation_detail}
variant="outlined"
/> />
</div>
{/* 8. Company / Organization Name */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Company / Organization Name</label>
<TextField
id="company_name"
fullWidth
placeholder="Enter Company Name"
value={data.company_name}
onChange={(e) => handleChange("company_name", e.target.value)}
/>
</div>
{/* 9. Income Currency Type */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Income Currency Type{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.income_currency)} id="income_currency">
<InputLabel>Select Currency</InputLabel>
<Select
value={data.income_currency}
label="Select Currency"
onChange={(e) => handleChange("income_currency", e.target.value)}
>
<MenuItem value="INR">INR</MenuItem>
<MenuItem value="USD">USD</MenuItem>
</Select>
{localErrors.income_currency && <FormHelperText>{localErrors.income_currency}</FormHelperText>}
</FormControl>
</div>
{/* 10. Annual Income */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Annual Income{requiredMark}</label>
<TextField
id="annual_income"
fullWidth
placeholder="Enter Annual Income"
value={data.annual_income}
onChange={(e) => handleChange("annual_income", e.target.value)}
error={Boolean(localErrors.annual_income)}
helperText={localErrors.annual_income}
InputProps={{
startAdornment: (
<InputAdornment position="start">
{data.income_currency === "USD" ? "$" : "₹"}
</InputAdornment>
),
}}
/>
</div>
{/* 11. Country */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Country{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.work_country)} id="work_country">
<InputLabel>Select Country</InputLabel>
<Select
value={data.work_country}
label="Select Country"
onChange={(e) => handleChange("work_country", e.target.value)}
>
{countryOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem>
))}
</Select>
{localErrors.work_country && <FormHelperText>{localErrors.work_country}</FormHelperText>}
</FormControl>
</div>
{/* 12. City/Town (Only if NOT India) */}
{!isIndia && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">City / Town{requiredMark}</label>
<TextField
id="work_city"
fullWidth
placeholder="Enter City / Town"
value={data.work_city}
onChange={(e) => handleChange("work_city", e.target.value)}
error={Boolean(localErrors.work_city)}
helperText={localErrors.work_city}
/>
</div>
)} )}
{/* 13. State (Only if India) */}
{isIndia && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">State{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.work_state)} id="work_state">
<InputLabel>Select State</InputLabel>
<Select
value={data.work_state}
label="Select State"
onChange={(e) => handleChange("work_state", e.target.value)}
>
{stateOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem>
))}
</Select>
{localErrors.work_state && <FormHelperText>{localErrors.work_state}</FormHelperText>}
</FormControl>
</div>
)}
{/* 14. City (District) (Only if India) */}
{isIndia && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">City{requiredMark}</label>
<FormControl fullWidth error={Boolean(localErrors.work_district)} id="work_district" disabled={!data.work_state || districtQuery.isLoading}>
<InputLabel>Select City</InputLabel>
<Select
value={data.work_district}
label="Select City"
onChange={(e) => handleChange("work_district", e.target.value)}
>
{districtOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem>
))}
</Select>
{localErrors.work_district && <FormHelperText>{localErrors.work_district}</FormHelperText>}
</FormControl>
</div>
)}
</>
)}
{/* 15. Address */}
<div className="flex flex-col gap-2 md:col-span-2">
<label className="text-gray-900 text-[15px]">Address{requiredMark}</label>
<TextField
id="address"
fullWidth
multiline
rows={2}
placeholder="Enter your address"
value={data.address}
onChange={(e) => handleChange("address", e.target.value)}
error={Boolean(localErrors.address)}
helperText={localErrors.address}
/>
</div> </div>
</div> </div>
<Grid <Box sx={{ mt: 5, display: "flex", gap: 2, justifyContent: "center" }}>
item
xs={12}
style={{
marginTop: "40px",
display: "flex",
gap: 16,
justifyContent: "center",
}}
>
{onSkipStep && ( {onSkipStep && (
<Button variant="outlined" onClick={onSkipStep}> <Button variant="outlined" size="large" onClick={onSkipStep} sx={{ minWidth: 120 }}>
Skip Skip
</Button> </Button>
)} )}
<Button variant="contained" color="primary" onClick={handleSubmit}> <Button variant="contained" size="large" onClick={handleSubmit} sx={{ minWidth: 120 }}>
{onSkipStep ? "Next" : "Update"} {onSkipStep ? "Next" : "Update"}
</Button> </Button>
</Grid> </Box>
</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 } from "../redux/registrationFormSlice"; import { updateFamilyDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
TextField, TextField,
@ -12,46 +12,55 @@ import {
Box, Box,
} from "@mui/material"; } from "@mui/material";
import { useFamilyMasters } from "../hooks/useMasters"; import { useFamilyMasters } from "../hooks/useMasters";
import { useCityMasters } from "../hooks/useDependentMasters";
import { toast } from "react-hot-toast";
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange }) => { const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, isEditMode }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.familyDetails); const data = useSelector((state) => state.registerform.familyDetails);
const inputRef = useRef(null); const inputRef = useRef(null);
const brotherSectionRef = useRef(null);
const sisterSectionRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>; const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: familyMasters, isLoading: isFamilyMastersLoading } = const { data: familyMasters, isLoading: isFamilyMastersLoading } = useFamilyMasters();
useFamilyMasters();
// District query for India
const districtQuery = useCityMasters(data.familyState);
const occupationOptions = useMemo(() => { const occupationOptions = useMemo(() => {
const raw = familyMasters; const raw = familyMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.occupation || [];
return raw.occupation || raw.occupations || [];
}, [familyMasters]); }, [familyMasters]);
const maritalStatusOptions = useMemo(() => { const maritalStatusOptions = useMemo(() => {
const raw = familyMasters; const raw = familyMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.maritalStatus || [];
return raw.maritalStatus || raw.marital_status || [];
}, [familyMasters]); }, [familyMasters]);
const familyStatusOptions = useMemo(() => { const familyStatusOptions = useMemo(() => {
const raw = familyMasters; const raw = familyMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return []; return raw.familyStatus || [];
return raw.familyStatus || raw.family_status || [];
}, [familyMasters]); }, [familyMasters]);
const countryOptions = useMemo(() => familyMasters?.country || [], [familyMasters]);
const stateOptions = useMemo(() => familyMasters?.state || [], [familyMasters]);
const districtOptions = useMemo(() => districtQuery.data?.districts || districtQuery.data || [], [districtQuery.data]);
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
const createSibling = () => ({ const createSibling = () => ({
type: "",
name: "", name: "",
occupation: "", occupation: "",
maritalStatus: "", maritalStatus: "",
haveChildrens: "", hasChildren: "",
details: "",
}); });
const syncSiblingArray = (arr, count) => { const syncSiblingArray = (arr, count) => {
@ -72,20 +81,48 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
if (field === "brotherCount") { if (field === "brotherCount") {
const count = Number(value) || 0; const count = Number(value) || 0;
updates.brotherCount = count; updates.brotherCount = value;
updates.brothers = syncSiblingArray(data.brothers, count); updates.brothers = syncSiblingArray(data.brothers, count);
fieldsToClear.push("brothers"); fieldsToClear.push("brothers");
if (count > 0) {
setTimeout(() => {
brotherSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
const firstInput = brotherSectionRef.current?.querySelector('.first-sibling-name');
if (firstInput) firstInput.focus();
}, 500);
}
} }
if (field === "sisterCount") { if (field === "sisterCount") {
const count = Number(value) || 0; const count = Number(value) || 0;
updates.sisterCount = count; updates.sisterCount = value;
updates.sisters = syncSiblingArray(data.sisters, count); updates.sisters = syncSiblingArray(data.sisters, count);
fieldsToClear.push("sisters"); fieldsToClear.push("sisters");
if (count > 0) {
setTimeout(() => {
sisterSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
const firstInput = sisterSectionRef.current?.querySelector('.first-sibling-name');
if (firstInput) firstInput.focus();
}, 500);
}
}
if (field === "familyCountry") {
updates.familyState = "";
updates.familyDistrict = "";
updates.familyCity = "";
}
if (field === "familyState") {
updates.familyDistrict = "";
} }
dispatch(updateFamilyDetails(updates)); dispatch(updateFamilyDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear); if (onFieldChange) onFieldChange(fieldsToClear);
if (!isEditMode) {
dispatch(clearAllStepsFrom(4));
}
}; };
const handleSiblingChange = (type, index, field, value) => { const handleSiblingChange = (type, index, field, value) => {
@ -94,14 +131,40 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
list[index] = { ...list[index], [field]: value }; list[index] = { ...list[index], [field]: value };
dispatch(updateFamilyDetails({ [type]: list })); dispatch(updateFamilyDetails({ [type]: list }));
if (onFieldChange) onFieldChange(type); if (onFieldChange) onFieldChange(type);
if (!isEditMode) {
dispatch(clearAllStepsFrom(4));
}
}; };
const handleSubmit = () => { const scrollToError = (errorMap) => {
console.log("Submitting family details:", data); const errorFields = Object.keys(errorMap);
if (errorFields.length > 0) {
const fieldId = errorFields[0];
const element = document.getElementById(fieldId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
const focusable = element.querySelector('[role="combobox"]') ||
element.querySelector('[role="button"]') ||
element.querySelector("input") ||
element.querySelector("select") ||
element;
if (focusable && typeof focusable.focus === "function") {
focusable.focus();
}
}, 300);
}
}
};
const handleSubmit = (e) => {
if (e) e.preventDefault();
onSubmitStep(); onSubmitStep();
}; };
const countOptions = Array.from({ length: 11 }, (_, i) => i); const countOptions = Array.from({ length: 11 }, (_, i) => i);
const isIndia = Number(data.familyCountry) === 1;
const renderSiblingCard = (type, index) => { const renderSiblingCard = (type, index) => {
const sibling = (data[type] || [])[index] || createSibling(); const sibling = (data[type] || [])[index] || createSibling();
@ -114,95 +177,88 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
borderRadius: 2, borderRadius: 2,
padding: 2, padding: 2,
backgroundColor: "#fff", backgroundColor: "#fff",
mb: 4
}} }}
> >
<div className="text-gray-900 text-[14px] font-semibold mb-3"> <div className="text-gray-900 text-[14px] font-semibold mb-3">
{labelPrefix} {index + 1} {labelPrefix} {index + 1}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<FormControl fullWidth variant="outlined">
<InputLabel>Type</InputLabel>
<Select
label="Type"
value={sibling.type}
onChange={(e) => handleSiblingChange(type, index, "type", e.target.value)}
>
<MenuItem value=""><em>Select</em></MenuItem>
<MenuItem value="Elder">Elder</MenuItem>
<MenuItem value="Younger">Younger</MenuItem>
</Select>
</FormControl>
<TextField <TextField
fullWidth fullWidth
label="Name" label="Name"
placeholder="Enter Name"
value={sibling.name} value={sibling.name}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "name", e.target.value)}
handleSiblingChange(type, index, "name", e.target.value)
}
variant="outlined" variant="outlined"
inputProps={{ className: index === 0 ? "first-sibling-name" : "" }}
/> />
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-occupation-label`}> <InputLabel>Occupation</InputLabel>
Occupation
</InputLabel>
<Select <Select
labelId={`${type}-${index}-occupation-label`}
label="Occupation" label="Occupation"
value={sibling.occupation} value={sibling.occupation}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "occupation", e.target.value)}
handleSiblingChange(type, index, "occupation", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{occupationOptions.map((opt) => ( {occupationOptions.map((opt) => (
<MenuItem key={opt} value={opt}> <MenuItem key={opt} value={opt}>{opt}</MenuItem>
{opt}
</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-marital-label`}> <InputLabel>Marital Status</InputLabel>
Marital Status
</InputLabel>
<Select <Select
labelId={`${type}-${index}-marital-label`}
label="Marital Status" label="Marital Status"
value={sibling.maritalStatus} value={sibling.maritalStatus}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "maritalStatus", e.target.value)}
handleSiblingChange(type, index, "maritalStatus", e.target.value)
}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{maritalStatusOptions.map((opt) => ( {maritalStatusOptions.map((opt) => (
<MenuItem key={opt} value={opt}> <MenuItem key={opt} value={opt}>{opt}</MenuItem>
{opt}
</MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id={`${type}-${index}-children-label`}> <InputLabel>Have Children</InputLabel>
Have Children
</InputLabel>
<Select <Select
labelId={`${type}-${index}-children-label`}
label="Have Children" label="Have Children"
value={sibling.haveChildrens} value={sibling.hasChildren}
onChange={(e) => onChange={(e) => handleSiblingChange(type, index, "hasChildren", e.target.value)}
handleSiblingChange(
type,
index,
"haveChildrens",
e.target.value
)
}
> >
<MenuItem value={1}>Yes</MenuItem> <MenuItem value=""><em>Select</em></MenuItem>
<MenuItem value={0}>No</MenuItem> <MenuItem value="Yes">Yes</MenuItem>
<MenuItem value="No">No</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<div className="md:col-span-2">
<TextField
fullWidth
multiline
rows={2}
label="Additional Details"
value={sibling.details}
onChange={(e) => handleSiblingChange(type, index, "details", e.target.value)}
variant="outlined"
/>
</div>
</div> </div>
</Box> </Box>
); );
@ -212,20 +268,18 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4" id="fatherName">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Father Name{requiredMark} Father Name{requiredMark}
</label> </label>
<TextField <TextField
fullWidth fullWidth
inputRef={inputRef} inputRef={inputRef}
name="fatherName" placeholder="Enter Father Name"
label="Father Name"
value={data.fatherName} value={data.fatherName}
onChange={(e) => handleChange("fatherName", e.target.value)} onChange={(e) => handleChange("fatherName", e.target.value)}
error={Boolean(errors.fatherName)} error={Boolean(errors.fatherName)}
helperText={errors.fatherName} helperText={errors.fatherName}
placeholder="Enter Father Name"
variant="outlined" variant="outlined"
/> />
</div> </div>
@ -236,30 +290,24 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</label> </label>
<TextField <TextField
fullWidth fullWidth
name="fatherOccupation" placeholder="Enter Father Occupation"
label="Father Occupation"
value={data.fatherOccupation} value={data.fatherOccupation}
onChange={(e) => handleChange("fatherOccupation", e.target.value)} onChange={(e) => handleChange("fatherOccupation", e.target.value)}
error={Boolean(errors.fatherOccupation)}
helperText={errors.fatherOccupation}
placeholder="Enter Father Occupation"
variant="outlined" variant="outlined"
/> />
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4" id="motherName">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Mother Name{requiredMark} Mother Name{requiredMark}
</label> </label>
<TextField <TextField
fullWidth fullWidth
name="motherName" placeholder="Enter Mother Name"
label="Mother Name"
value={data.motherName} value={data.motherName}
onChange={(e) => handleChange("motherName", e.target.value)} onChange={(e) => handleChange("motherName", e.target.value)}
error={Boolean(errors.motherName)} error={Boolean(errors.motherName)}
helperText={errors.motherName} helperText={errors.motherName}
placeholder="Enter Mother Name"
variant="outlined" variant="outlined"
/> />
</div> </div>
@ -270,13 +318,9 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</label> </label>
<TextField <TextField
fullWidth fullWidth
name="motherOccupation" placeholder="Enter Mother Occupation"
label="Mother Occupation"
value={data.motherOccupation} value={data.motherOccupation}
onChange={(e) => handleChange("motherOccupation", e.target.value)} onChange={(e) => handleChange("motherOccupation", e.target.value)}
error={Boolean(errors.motherOccupation)}
helperText={errors.motherOccupation}
placeholder="Enter Mother Occupation"
variant="outlined" variant="outlined"
/> />
</div> </div>
@ -286,14 +330,13 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
Brother Count Brother Count
</label> </label>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id="brotherCount-label">Select Brother Count</InputLabel> <InputLabel>Select Brother Count</InputLabel>
<Select <Select
labelId="brotherCount-label"
label="Select Brother Count" label="Select Brother Count"
name="brotherCount"
value={data.brotherCount} value={data.brotherCount}
onChange={(e) => handleChange("brotherCount", e.target.value)} onChange={(e) => handleChange("brotherCount", e.target.value)}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{countOptions.map((count) => ( {countOptions.map((count) => (
<MenuItem key={count} value={count}> <MenuItem key={count} value={count}>
{count} {count}
@ -303,19 +346,27 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</FormControl> </FormControl>
</div> </div>
{Number(data.brotherCount) > 0 && (
<div className="md:col-span-2 mt-2" ref={brotherSectionRef}>
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Brother Details
</div>
{data.brothers.map((_, i) => renderSiblingCard("brothers", i))}
</div>
)}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Sister Count Sister Count
</label> </label>
<FormControl fullWidth variant="outlined"> <FormControl fullWidth variant="outlined">
<InputLabel id="sisterCount-label">Select Sister Count</InputLabel> <InputLabel>Select Sister Count</InputLabel>
<Select <Select
labelId="sisterCount-label"
label="Select Sister Count" label="Select Sister Count"
name="sisterCount"
value={data.sisterCount} value={data.sisterCount}
onChange={(e) => handleChange("sisterCount", e.target.value)} onChange={(e) => handleChange("sisterCount", e.target.value)}
> >
<MenuItem value=""><em>Select</em></MenuItem>
{countOptions.map((count) => ( {countOptions.map((count) => (
<MenuItem key={count} value={count}> <MenuItem key={count} value={count}>
{count} {count}
@ -325,105 +376,183 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
</FormControl> </FormControl>
</div> </div>
<div className="flex flex-col gap-4"> {Number(data.sisterCount) > 0 && (
<div className="md:col-span-2 mt-2" ref={sisterSectionRef}>
<div className="text-gray-900 text-[16px] font-semibold mb-3">
Sister Details
</div>
{data.sisters.map((_, i) => renderSiblingCard("sisters", i))}
</div>
)}
<div className="flex flex-col gap-4" id="familyStatus">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Family Status{requiredMark} Family Status{requiredMark}
</label> </label>
<FormControl <FormControl fullWidth variant="outlined" error={Boolean(errors.familyStatus)}>
fullWidth <InputLabel>Select Family Status</InputLabel>
variant="outlined"
error={Boolean(errors.familyStatus)}
>
<InputLabel id="familyStatus-label">Select Family Status</InputLabel>
<Select <Select
labelId="familyStatus-label"
label="Select Family Status" label="Select Family Status"
name="familyStatus"
value={data.familyStatus} value={data.familyStatus}
onChange={(e) => handleChange("familyStatus", e.target.value)} onChange={(e) => handleChange("familyStatus", e.target.value)}
disabled={isFamilyMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
> >
{familyStatusOptions.map((item) => ( {familyStatusOptions.map((item) => (
<MenuItem key={item.id ?? item} value={item.id ?? item}> <MenuItem key={item.id} value={item.id}>
{item.family_type_name || item.name || item} {item.family_type_name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{errors.familyStatus && ( {errors.familyStatus && (
<p <p className="text-[#d32f2f] text-[0.75rem] mt-1 ml-3">{errors.familyStatus}</p>
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.familyStatus}
</p>
)} )}
</FormControl> </FormControl>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4" id="nativePlace">
<label className="text-gray-900 text-[15px]">Native Place</label> <label className="text-gray-900 text-[15px]">
Native Place{requiredMark}
</label>
<TextField <TextField
fullWidth fullWidth
name="nativePlace" placeholder="Enter Native Place"
label="Native Place"
value={data.nativePlace} value={data.nativePlace}
onChange={(e) => handleChange("nativePlace", e.target.value)} onChange={(e) => handleChange("nativePlace", e.target.value)}
error={Boolean(errors.nativePlace)} error={Boolean(errors.nativePlace)}
helperText={errors.nativePlace} helperText={errors.nativePlace}
placeholder="Enter Native Place"
variant="outlined" variant="outlined"
/> />
</div> </div>
</div>
{Number(data.brotherCount) > 0 && ( <div className="flex flex-col gap-4">
<div className="mt-6"> <label className="text-gray-900 text-[15px]">
<div className="text-gray-900 text-[16px] font-semibold mb-3"> Country Living
Brother Details </label>
</div> <FormControl fullWidth variant="outlined">
<div className="grid grid-cols-1 gap-4"> <InputLabel>Select Country</InputLabel>
{Array.from({ length: Number(data.brotherCount) }).map( <Select
(_, index) => renderSiblingCard("brothers", index) label="Select Country"
)} value={data.familyCountry}
</div> onChange={(e) => handleChange("familyCountry", e.target.value)}
</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) => (
<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 && ( {onSkipStep && (
<Button variant="outlined" onClick={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 }}>
{onSkipStep ? "Next" : "Update"} {onSkipStep ? "Next" : "Update"}
</Button> </Button>
</Grid> </div>
</form> </form>
</div> </div>
); );

View File

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { updateLifestyleDetails } from "../redux/registrationFormSlice"; import { updateLifestyleDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
import { import {
Grid, Grid,
FormControl, FormControl,
@ -30,6 +30,7 @@ const LifestyleDetailsForm = ({
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);
@ -76,6 +77,10 @@ const LifestyleDetailsForm = ({
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updateLifestyleDetails({ [field]: value })); dispatch(updateLifestyleDetails({ [field]: value }));
if (onFieldChange) onFieldChange(field); if (onFieldChange) onFieldChange(field);
if (!isEditMode) {
dispatch(clearAllStepsFrom(5));
}
}; };
const handleMultiChange = (field, value) => { const handleMultiChange = (field, value) => {

File diff suppressed because it is too large Load Diff

View File

@ -397,10 +397,10 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
{Object.entries(sibling).map(([sKey, sVal]) => { {Object.entries(sibling).map(([sKey, sVal]) => {
if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null; if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null;
let displayValue = sVal; let displayValue = sVal;
if (sKey === 'haveChildrens' || sKey === 'have_childrens') { if (sKey === 'haveChildrens' || sKey === 'have_childrens' || sKey === 'hasChildren' || sKey === 'has_children') {
if (sVal === true || sVal === 1 || sVal === '1') { if (sVal === true || sVal === 1 || sVal === '1' || sVal === 'Yes') {
displayValue = 'Yes'; displayValue = 'Yes';
} else if (sVal === false || sVal === 0 || sVal === '0') { } else if (sVal === false || sVal === 0 || sVal === '0' || sVal === 'No') {
displayValue = 'No'; displayValue = 'No';
} }
} }

View File

@ -8,6 +8,7 @@ import {
updateFamilyDetails, updateFamilyDetails,
updateLifestyleDetails, updateLifestyleDetails,
updatePartnerPreferences, updatePartnerPreferences,
clearAllStepsFrom,
submitForm, submitForm,
} from "../redux/registrationFormSlice"; } from "../redux/registrationFormSlice";
import PersonalDetailsForm from "./PersonalDetailsForm"; import PersonalDetailsForm from "./PersonalDetailsForm";
@ -31,37 +32,47 @@ import { isAuthenticated } from "../utills/auth";
import { getPreviewDetails } from "../api/preview.api"; import { getPreviewDetails } from "../api/preview.api";
const STEP_FIELD_ORDER = { const STEP_FIELD_ORDER = {
1: [ 1: [
"name", "profile_for",
"gender", "gender",
"mobileNumber", "name",
"dob", "mobile",
"height",
"weight",
"maritalStatus",
"religion",
"profileFor",
"caste",
"subCaste",
"gothram",
"raasi",
"star",
"email", "email",
"password", "password",
"confirmPassword", "confirmPassword",
"state", "marital_status",
"city", "height",
"pincode", "weight",
"complexion",
"physical_status",
"religion",
"caste",
"sub_caste",
"willing_to_marry",
"inter_caste_parents",
"inter_caste_parents_details",
"gothram",
"do_you_speak_telugu",
"about_us",
"known_languages",
"mother_language",
"profiles", "profiles",
], ],
2: [ 2: [
"fieldOfStudy", "study_field",
"qualification", "education",
"collegeName", "education_detail",
"college_name",
"employee_type",
"occupation", "occupation",
"organization", "occupation_detail",
"employeeType", "company_name",
"income", "income_currency",
"workLocation", "annual_income",
"work_country",
"work_city",
"work_state",
"work_district",
"address",
], ],
3: [ 3: [
"fatherName", "fatherName",
@ -89,35 +100,29 @@ const STEP_FIELD_ORDER = {
const STEP1_SERVER_FIELD_MAP = { const STEP1_SERVER_FIELD_MAP = {
name: "name", name: "name",
mobile: "mobileNumber", mobile: "mobile",
mobile_number: "mobileNumber",
mobileNumber: "mobileNumber",
phone: "mobileNumber",
email: "email", email: "email",
gender: "gender", gender: "gender",
dob: "dob",
height: "height", height: "height",
weight: "weight", weight: "weight",
marital_status: "maritalStatus", marital_status: "marital_status",
maritalStatus: "maritalStatus",
religion: "religion", religion: "religion",
profile_for: "profileFor", profile_for: "profile_for",
profileFor: "profileFor",
caste: "caste", caste: "caste",
sub_caste: "subCaste", sub_caste: "sub_caste",
subCaste: "subCaste", willing_to_marry: "willing_to_marry",
inter_caste_parents: "inter_caste_parents",
inter_caste_parents_details: "inter_caste_parents_details",
gothram: "gothram", gothram: "gothram",
raasi: "raasi", do_you_speak_telugu: "do_you_speak_telugu",
star: "star", about_us: "about_us",
state: "state", known_languages: "known_languages",
district: "city", mother_language: "mother_language",
city: "city", complexion: "complexion",
pincode: "pincode", physical_status: "physical_status",
password: "password", password: "password",
confirm_password: "confirmPassword", confirm_password: "confirmPassword",
confirmPassword: "confirmPassword",
profiles: "profiles", profiles: "profiles",
profile_images: "profiles",
}; };
@ -171,8 +176,7 @@ const STEP1_SERVER_FIELD_MAP = {
import { Check } from "lucide-react"; import { Check } from "lucide-react";
const Stepper = ({ currentStep, enabledSteps, completedSteps, onStepClick }) => { const Stepper = ({ currentStep, enabledSteps, completedSteps, onStepClick, checkStepValidity }) => {
const steps = [ const steps = [
{ num: 1, label: "Personal" }, { num: 1, label: "Personal" },
{ num: 2, label: "Educational" }, { num: 2, label: "Educational" },
@ -183,40 +187,57 @@ const Stepper = ({ currentStep, enabledSteps, completedSteps, onStepClick }) =>
]; ];
return ( return (
<div className="flex items-center justify-between px-4 py-6"> <div className="flex items-center justify-between px-4 py-6 overflow-x-auto">
{steps.map((step, index) => { {steps.map((step, index) => {
const isEnabled = enabledSteps.includes(step.num); const isSkippable = currentStep > 1 && currentStep < 6 && step.num === currentStep + 1;
const isCompleted = completedSteps.includes(step.num); const isEnabled = enabledSteps.includes(step.num) || isSkippable;
const isActive = currentStep === step.num;
// Dynamic completion check: Green tick if all mandatory fields are filled
const isValid = checkStepValidity && checkStepValidity(step.num);
const isCompleted = completedSteps.includes(step.num) || isValid;
// A step is Blue (Skipped) ONLY if it's a previous step and NOT completed
const isSkipped = isEnabled && !isCompleted && !isActive && step.num < currentStep;
return ( return (
<React.Fragment key={step.num}> <React.Fragment key={step.num}>
<div <div
className={`flex flex-col items-center ${ className={`flex flex-col items-center min-w-[70px] ${
isEnabled ? "cursor-pointer" : "cursor-not-allowed opacity-50" isEnabled ? "cursor-pointer" : "cursor-not-allowed opacity-60"
}`} }`}
onClick={() => isEnabled && onStepClick(step.num)} onClick={() => isEnabled && onStepClick(step.num)}
> >
<div <div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${ className={`w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold transition-all duration-300 ${
isCompleted isCompleted
? "bg-green-600 text-white" ? "bg-green-600 text-white shadow-md"
: currentStep === step.num : isActive
? "bg-red-600 text-white" ? "bg-red-600 text-white ring-4 ring-red-100 shadow-md"
: "bg-gray-300 text-gray-600" : isSkipped
? "bg-blue-500 text-white shadow-md"
: "bg-gray-200 text-gray-500"
}`} }`}
> >
{isCompleted ? <Check size={16} /> : step.num} {isCompleted ? <Check size={18} /> : step.num}
</div> </div>
<span
<span className="text-xs mt-1">{step.label}</span> className={`text-[10px] mt-2 font-medium text-center uppercase tracking-wider ${
isActive ? "text-red-600 font-bold" : isCompleted ? "text-green-600" : isSkipped ? "text-blue-500" : "text-gray-400"
}`}
>
{step.label}
</span>
</div> </div>
{index < steps.length - 1 && ( {index < steps.length - 1 && (
<div <div
className={`flex-1 h-0.5 mx-1 ${ className={`flex-1 h-[2px] mx-1 mb-6 transition-colors duration-500 ${
completedSteps.includes(step.num) (completedSteps.includes(step.num) || (checkStepValidity && checkStepValidity(step.num)))
? "bg-green-600" ? "bg-green-600"
: "bg-gray-300" : (enabledSteps.includes(step.num) && enabledSteps.includes(step.num + 1))
? "bg-blue-300"
: "bg-gray-200"
}`} }`}
/> />
)} )}
@ -255,7 +276,14 @@ const shouldHideStepper = hideStepperRoutes.some((route) => location.pathname.st
const savedStep = localStorage.getItem("registration_current_step"); const savedStep = localStorage.getItem("registration_current_step");
return savedStep ? Number(savedStep) : 1; return savedStep ? Number(savedStep) : 1;
}); });
const [enabledSteps, setEnabledSteps] = useState([1]); const [enabledSteps, setEnabledSteps] = useState(() => {
if (location.state?.step) {
return Array.from({ length: 6 }, (_, i) => i + 1); // Enable all if coming from edit/preview
}
const savedStep = localStorage.getItem("registration_current_step");
const step = savedStep ? Number(savedStep) : 1;
return Array.from({ length: step }, (_, i) => i + 1);
});
const [completedSteps, setCompletedSteps] = useState([]); const [completedSteps, setCompletedSteps] = useState([]);
const [isStep1Update, setIsStep1Update] = useState(false); const [isStep1Update, setIsStep1Update] = useState(false);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
@ -362,24 +390,23 @@ const [completedSteps, setCompletedSteps] = useState([]);
order[0] || order[0] ||
Object.keys(errorMap).find((key) => key !== "_form"); Object.keys(errorMap).find((key) => key !== "_form");
if (!firstKey) return; if (!firstKey) return;
setTimeout(() => { setTimeout(() => {
const byAria = document.querySelector( const element = document.getElementById(firstKey) ||
`[aria-labelledby~="${firstKey}-label"]` document.querySelector(`[name="${firstKey}"]`) ||
); document.querySelector(`[aria-labelledby~="${firstKey}-label"]`);
if (byAria && typeof byAria.focus === "function") {
byAria.focus(); if (element) {
return; element.scrollIntoView({ behavior: "smooth", block: "center" });
// Try to find a focusable element within the container
const focusable = element.querySelector('input:not([type="hidden"]), select, textarea, [role="combobox"], [role="button"]') || element;
if (focusable && typeof focusable.focus === "function") {
focusable.focus();
} }
const byName = document.querySelector(`[name="${firstKey}"]`);
if (byName && typeof byName.focus === "function") {
byName.focus();
return;
} }
const byId = document.getElementById(firstKey); }, 100);
if (byId && typeof byId.focus === "function") {
byId.focus();
}
}, 0);
}; };
const clearFieldErrors = (fields) => { const clearFieldErrors = (fields) => {
@ -474,18 +501,27 @@ const [completedSteps, setCompletedSteps] = useState([]);
dispatch( dispatch(
updatePersonalDetails({ updatePersonalDetails({
name: pd.name || "", name: pd.name || "",
mobileNumber: pd.mobile || "", mobile: pd.mobile || "",
email: pd.email || "", email: pd.email || "",
gender: pd.gender || "", gender: pd.gender || "",
dob: formattedDob, dob: formattedDob,
height: pd.height || "", height: pd.height_id || "",
weight: pd.weight || "", weight: pd.weight || "",
maritalStatus: pd.marital_status_id || "", marital_status: pd.marital_status_id || "",
religion: pd.religion_id || "", religion: pd.religion_id || "",
profileFor: pd.profile_for_id || "", profile_for: pd.profile_for_id || "",
caste: pd.caste_id || "", caste: pd.caste_id || "",
subCaste: pd.sub_caste_id || "", sub_caste: pd.sub_caste_id || "",
gothram: pd.gothram_id || "", willing_to_marry: pd.willing_to_marry || "",
inter_caste_parents: pd.inter_caste_parents || 0,
inter_caste_parents_details: pd.inter_caste_parents_details || "",
gothram: pd.gothram || "",
do_you_speak_telugu: pd.do_you_speak_telugu || 0,
about_us: pd.about_us || "",
known_languages: pd.known_language_ids || [],
mother_language: pd.mother_language_id || "",
complexion: pd.complexion_id || "",
physical_status: pd.physical_status_id || "",
raasi: pd.raasi_id || "", raasi: pd.raasi_id || "",
star: pd.star_id || "", star: pd.star_id || "",
state: pd.state_id || "", state: pd.state_id || "",
@ -518,14 +554,21 @@ useEffect(() => {
const ed = educationalData.educational_details; const ed = educationalData.educational_details;
dispatch( dispatch(
updateEducationalDetails({ updateEducationalDetails({
fieldOfStudy: ed.study_field_id || "", study_field: ed.study_field_id || "",
qualification: ed.education_id || "", education: ed.education_id || "",
collegeName: ed.college_name || "", education_detail: ed.education_detail || "",
college_name: ed.college_name || "",
employee_type: ed.employee_type_id || "",
occupation: ed.occupation_id || "", occupation: ed.occupation_id || "",
organization: ed.company_name || "", occupation_detail: ed.occupation_detail || "",
employeeType: ed.employee_type_id || "", company_name: ed.company_name || "",
income: ed.annual_income_id || "", income_currency: ed.income_currency || "INR",
workLocation: ed.work_location || "", annual_income: ed.annual_income || "",
work_country: ed.work_country_id || "",
work_state: ed.work_state_id || "",
work_district: ed.work_district_id || "",
work_city: ed.work_city || "",
address: ed.address || ed.work_location || "",
}) })
); );
} }
@ -549,21 +592,6 @@ const {data:familyData} = useQuery({
useEffect(() => { useEffect(() => {
if (familyData?.status === "success" && familyData?.family_details) { if (familyData?.status === "success" && familyData?.family_details) {
const fd = familyData.family_details; const fd = familyData.family_details;
const mappedBrothers = (fd.brothers || []).map((b) => ({
name: b.name || "",
occupation: b.occupation_name || "",
maritalStatus: b.marital_status || "",
haveChildrens: b.have_childrens === true ? 1 : (b.have_childrens === false ? 0 : b.have_childrens),
}));
const mappedSisters = (fd.sisters || []).map((s) => ({
name: s.name || "",
occupation: s.occupation_name || "",
maritalStatus: s.marital_status || "",
haveChildrens: s.have_childrens === true ? 1 : (s.have_childrens === false ? 0 : s.have_childrens),
}));
dispatch( dispatch(
updateFamilyDetails({ updateFamilyDetails({
fatherName: fd.father_name || "", fatherName: fd.father_name || "",
@ -572,15 +600,35 @@ useEffect(()=>{
motherOccupation: fd.mother_occupation || "", motherOccupation: fd.mother_occupation || "",
familyStatus: fd.family_status_id || "", familyStatus: fd.family_status_id || "",
nativePlace: fd.native_place || "", nativePlace: fd.native_place || "",
familyCountry: fd.family_country_id || "",
familyState: fd.family_state_id || "",
familyDistrict: fd.family_district_id || "",
familyCity: fd.family_city || "",
address: fd.address || "",
expectationDetails: fd.expectation_details || "",
willingToGoAbroad: fd.willing_to_go_abroad || "",
brotherCount: fd.brother_count || 0, brotherCount: fd.brother_count || 0,
sisterCount: fd.sister_count || 0, sisterCount: fd.sister_count || 0,
brothers: mappedBrothers, brothers: (fd.brothers || []).map((b) => ({
sisters: mappedSisters, name: b.name || "",
occupation: b.occupation_name || "",
maritalStatus: b.marital_status || "",
type: b.type || "",
hasChildren: b.has_children || "",
details: b.additional_details || ""
})),
sisters: (fd.sisters || []).map((s) => ({
name: s.name || "",
occupation: s.occupation_name || "",
maritalStatus: s.marital_status || "",
type: s.type || "",
hasChildren: s.has_children || "",
details: s.additional_details || ""
})),
}) })
); );
} }
}, [familyData, dispatch]);
},[familyData,dispatch])
// Fetch Lifestyle Details // Fetch Lifestyle Details
const { data: lifestyleData } = useQuery({ const { data: lifestyleData } = useQuery({
@ -668,23 +716,24 @@ useEffect(()=>{
if (step === 1) { if (step === 1) {
const required = [ const required = [
"name", "name",
"mobileNumber", "mobile",
"gender", "gender",
"dob",
"height", "height",
"maritalStatus", "marital_status",
"profileFor", "profile_for",
"caste", "caste",
"email", "email",
"state", "mother_language",
"city", "complexion",
"pincode", "physical_status",
"inter_caste_parents",
"do_you_speak_telugu",
]; ];
required.forEach((field) => { required.forEach((field) => {
if (!personalDetails[field]) { if (!personalDetails[field] && personalDetails[field] !== 0) {
const label = field const label = field
.replace(/([A-Z])/g, " $1") .replace(/_/g, " ")
.replace(/^./, (str) => str.toUpperCase()); .replace(/^./, (str) => str.toUpperCase());
newErrors[field] = `${label} is required`; newErrors[field] = `${label} is required`;
} }
@ -702,16 +751,10 @@ useEffect(()=>{
newErrors.email = "Invalid email format"; newErrors.email = "Invalid email format";
} }
if ( if (
personalDetails.mobileNumber && personalDetails.mobile &&
personalDetails.mobileNumber.length !== 10 personalDetails.mobile.length !== 10
) { ) {
newErrors.mobileNumber = "Mobile number must be 10 digits"; newErrors.mobile = "Mobile number must be 10 digits";
}
if (personalDetails.height && Number(personalDetails.height) > 10) {
newErrors.height = "Height must be 10 or less";
}
if (personalDetails.weight && Number(personalDetails.weight) > 300) {
newErrors.weight = "Weight must be 300 or less";
} }
if ( if (
personalDetails.password && personalDetails.password &&
@ -721,16 +764,24 @@ useEffect(()=>{
newErrors.confirmPassword = "Passwords do not match"; newErrors.confirmPassword = "Passwords do not match";
} }
} else if (step === 2) { } else if (step === 2) {
const required = [ const isUnemployed = educationalDetails.employee_type === 11;
"qualification", const isIndia = educationalDetails.work_country === 1;
"fieldOfStudy", const required = ["study_field", "education", "education_detail", "employee_type"];
if (!isUnemployed) {
required.push("occupation", "occupation_detail", "income_currency", "annual_income", "work_country");
if (isIndia) {
required.push("work_state", "work_district");
} else {
required.push("work_city");
}
}
required.push("address");
];
required.forEach((field) => { required.forEach((field) => {
if (!educationalDetails[field]) { if (!educationalDetails[field]) {
const label = field const label = field
.replace(/([A-Z])/g, " $1") .replace(/_/g, " ")
.replace(/^./, (str) => str.toUpperCase()); .replace(/^./, (str) => str.toUpperCase());
newErrors[field] = `${label} is required`; newErrors[field] = `${label} is required`;
} }
@ -740,6 +791,7 @@ useEffect(()=>{
"fatherName", "fatherName",
"motherName", "motherName",
"familyStatus", "familyStatus",
"nativePlace",
]; ];
required.forEach((field) => { required.forEach((field) => {
if (!familyDetails[field]) { if (!familyDetails[field]) {
@ -836,24 +888,33 @@ useEffect(()=>{
const buildRegisterStep1Payload = async () => { const buildRegisterStep1Payload = async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("name", personalDetails.name); formData.append("name", personalDetails.name);
formData.append("mobile", personalDetails.mobileNumber); formData.append("mobile", personalDetails.mobile);
formData.append("email", personalDetails.email); formData.append("email", personalDetails.email);
formData.append("pincode", personalDetails.pincode);
formData.append("gender", personalDetails.gender); formData.append("gender", personalDetails.gender);
formData.append("dob", personalDetails.dob);
formData.append("height", personalDetails.height || ""); formData.append("height", personalDetails.height || "");
formData.append("weight", personalDetails.weight || ""); formData.append("weight", personalDetails.weight || "");
formData.append("marital_status", personalDetails.maritalStatus); formData.append("marital_status", personalDetails.marital_status);
formData.append("religion", personalDetails.religion); formData.append("religion", personalDetails.religion);
formData.append("profile_for", personalDetails.profileFor || ""); formData.append("profile_for", personalDetails.profile_for || "");
formData.append("caste", personalDetails.caste); formData.append("caste", personalDetails.caste);
formData.append("sub_caste", personalDetails.subCaste || ""); formData.append("sub_caste", personalDetails.sub_caste || "");
formData.append("willing_to_marry", personalDetails.willing_to_marry || "");
formData.append("inter_caste_parents", personalDetails.inter_caste_parents);
formData.append("inter_caste_parents_details", personalDetails.inter_caste_parents_details || "");
formData.append("gothram", personalDetails.gothram || ""); formData.append("gothram", personalDetails.gothram || "");
formData.append("raasi", personalDetails.raasi || ""); formData.append("do_you_speak_telugu", personalDetails.do_you_speak_telugu);
formData.append("star", personalDetails.star || ""); formData.append("about_us", personalDetails.about_us || "");
formData.append("state", personalDetails.state); formData.append("mother_language", personalDetails.mother_language || "");
formData.append("district", personalDetails.city); formData.append("complexion", personalDetails.complexion || "");
formData.append("physical_status", personalDetails.physical_status || "");
(personalDetails.known_languages || []).forEach((id, index) => {
formData.append(`known_languages[${index}]`, id);
});
if (!isStep1Update) {
formData.append("password", personalDetails.password || ""); formData.append("password", personalDetails.password || "");
}
formData.append("web_fcm_token", localStorage.getItem("fcm_token") || ""); formData.append("web_fcm_token", localStorage.getItem("fcm_token") || "");
if (personalDetails.profiles && Array.isArray(personalDetails.profiles)) { if (personalDetails.profiles && Array.isArray(personalDetails.profiles)) {
@ -895,14 +956,23 @@ useEffect(()=>{
const buildRegisterStep2Payload = () => { const buildRegisterStep2Payload = () => {
const formData = new FormData(); const formData = new FormData();
formData.append("college_name", educationalDetails.collegeName || ""); formData.append("study_field", educationalDetails.study_field || "");
formData.append("study_field", educationalDetails.fieldOfStudy || ""); formData.append("education", educationalDetails.education || "");
formData.append("education", educationalDetails.qualification || ""); formData.append("education_detail", educationalDetails.education_detail || "");
formData.append("college_name", educationalDetails.college_name || "");
formData.append("employee_type", educationalDetails.employee_type || "");
formData.append("occupation", educationalDetails.occupation || ""); formData.append("occupation", educationalDetails.occupation || "");
formData.append("company_name", educationalDetails.organization || ""); formData.append("occupation_detail", educationalDetails.occupation_detail || "");
formData.append("employee_type", educationalDetails.employeeType || ""); formData.append("company_name", educationalDetails.company_name || "");
formData.append("annual_income", educationalDetails.income || ""); formData.append("income_currency", educationalDetails.income_currency || "INR");
formData.append("work_location", educationalDetails.workLocation || ""); formData.append("annual_income", educationalDetails.annual_income || "");
formData.append("work_country", educationalDetails.work_country || "");
formData.append("work_city", educationalDetails.work_city || "");
formData.append("work_state", educationalDetails.work_state || "");
formData.append("work_district", educationalDetails.work_district || "");
formData.append("address", educationalDetails.address || "");
// Also append work_location as some APIs might use it for address/city
formData.append("work_location", educationalDetails.address || "");
return formData; return formData;
}; };
@ -916,19 +986,30 @@ useEffect(()=>{
formData.append("native_place", familyDetails.nativePlace || ""); formData.append("native_place", familyDetails.nativePlace || "");
formData.append("brother_count", familyDetails.brotherCount || 0); formData.append("brother_count", familyDetails.brotherCount || 0);
formData.append("sister_count", familyDetails.sisterCount || 0); formData.append("sister_count", familyDetails.sisterCount || 0);
formData.append("family_country", familyDetails.familyCountry || "");
formData.append("family_state", familyDetails.familyState || "");
formData.append("family_district", familyDetails.familyDistrict || "");
formData.append("family_city", familyDetails.familyCity || "");
formData.append("address", familyDetails.address || "");
formData.append("expectation_details", familyDetails.expectationDetails || "");
formData.append("willing_to_go_abroad", familyDetails.willingToGoAbroad || "");
(familyDetails.brothers || []).forEach((brother, index) => { (familyDetails.brothers || []).forEach((brother, index) => {
formData.append(`brothers[${index}][name]`, brother?.name || ""); formData.append(`brothers[${index}][name]`, brother?.name || "");
formData.append(`brothers[${index}][occupation]`, brother?.occupation || ""); formData.append(`brothers[${index}][occupation]`, brother?.occupation || "");
formData.append(`brothers[${index}][marital_status]`, brother?.maritalStatus || ""); formData.append(`brothers[${index}][marital_status]`, brother?.maritalStatus || "");
formData.append(`brothers[${index}][have_childrens]`, brother?.haveChildrens ?? ""); formData.append(`brothers[${index}][type]`, brother?.type || "");
formData.append(`brothers[${index}][has_children]`, brother?.hasChildren || "");
formData.append(`brothers[${index}][details]`, brother?.details || "");
}); });
(familyDetails.sisters || []).forEach((sister, index) => { (familyDetails.sisters || []).forEach((sister, index) => {
formData.append(`sisters[${index}][name]`, sister?.name || ""); formData.append(`sisters[${index}][name]`, sister?.name || "");
formData.append(`sisters[${index}][occupation]`, sister?.occupation || ""); formData.append(`sisters[${index}][occupation]`, sister?.occupation || "");
formData.append(`sisters[${index}][marital_status]`, sister?.maritalStatus || ""); formData.append(`sisters[${index}][marital_status]`, sister?.maritalStatus || "");
formData.append(`sisters[${index}][have_childrens]`, sister?.haveChildrens ?? ""); formData.append(`sisters[${index}][type]`, sister?.type || "");
formData.append(`sisters[${index}][has_children]`, sister?.hasChildren || "");
formData.append(`sisters[${index}][details]`, sister?.details || "");
}); });
return formData; return formData;
@ -1053,6 +1134,12 @@ useEffect(()=>{
const token = extractAccessToken(res.data || res); const token = extractAccessToken(res.data || res);
if (token) setAccessToken(token); if (token) setAccessToken(token);
// Store profile_id and user_id for WebSocket channels
const profileId = res.data?.profile_id || res?.profile_id || res.data?.data?.profile_id;
const userId = res.data?.user_id || res?.user_id || res.data?.data?.user_id;
if (profileId) localStorage.setItem("profile_id", profileId);
if (userId) localStorage.setItem("user_id", userId);
break; break;
case 2: case 2:
@ -1144,6 +1231,9 @@ useEffect(()=>{
const handleSkip = () => { const handleSkip = () => {
setErrors({}); setErrors({});
// Remove from completed if skipping (matching Flutter logic)
setCompletedSteps((prev) => prev.filter(s => s !== currentStep));
setCurrentStep((prev) => { setCurrentStep((prev) => {
const nextStep = Math.min(prev + 1, 6); const nextStep = Math.min(prev + 1, 6);
@ -1158,8 +1248,19 @@ useEffect(()=>{
}; };
const handleStepClick = (step) => { const handleStepClick = (step) => {
// If clicking next step and current is skippable (not Step 1), trigger handleSkip
if (step === currentStep + 1 && currentStep > 1 && currentStep < 6) {
handleSkip();
return;
}
if (!enabledSteps.includes(step)) return; if (!enabledSteps.includes(step)) return;
// If moving backwards during registration, clear the unsaved data of the step we are leaving
if (step < currentStep && !shouldHideStepper) {
dispatch(clearAllStepsFrom(currentStep));
}
setCurrentStep(step); setCurrentStep(step);
setErrors({}); setErrors({});
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -1203,7 +1304,7 @@ useEffect(() => {
onSubmitStep={handleStepSubmit} onSubmitStep={handleStepSubmit}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors} onFieldChange={clearFieldErrors}
isStep1Update={isStep1Update} isEditMode={isStep1Update || shouldHideStepper}
/> />
); );
case 2: case 2:
@ -1213,6 +1314,7 @@ useEffect(() => {
onSkipStep={shouldHideStepper ? null : handleSkip} onSkipStep={shouldHideStepper ? null : handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors} onFieldChange={clearFieldErrors}
isEditMode={shouldHideStepper}
/> />
); );
case 3: case 3:
@ -1222,6 +1324,7 @@ useEffect(() => {
onSkipStep={shouldHideStepper ? null : handleSkip} onSkipStep={shouldHideStepper ? null : handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors} onFieldChange={clearFieldErrors}
isEditMode={shouldHideStepper}
/> />
); );
case 4: case 4:
@ -1231,6 +1334,7 @@ useEffect(() => {
onSkipStep={shouldHideStepper ? null : handleSkip} onSkipStep={shouldHideStepper ? null : handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors} onFieldChange={clearFieldErrors}
isEditMode={shouldHideStepper}
/> />
); );
case 5: case 5:
@ -1240,6 +1344,7 @@ useEffect(() => {
onSkipStep={shouldHideStepper ? null : handleSkip} onSkipStep={shouldHideStepper ? null : handleSkip}
errors={errors} errors={errors}
onFieldChange={clearFieldErrors} onFieldChange={clearFieldErrors}
isEditMode={shouldHideStepper}
/> />
); );
case 6: case 6:
@ -1249,6 +1354,46 @@ useEffect(() => {
} }
}; };
const checkStepValidity = (stepNum) => {
if (stepNum === 1) {
const required = ["name", "mobile", "gender", "height", "marital_status", "profile_for", "caste", "email", "mother_language", "complexion", "physical_status"];
return required.every(field => personalDetails[field] || personalDetails[field] === 0);
}
if (stepNum === 2) {
const required = ["study_field", "education", "education_detail", "employee_type", "address"];
if (!educationalDetails.study_field || !educationalDetails.education || !educationalDetails.education_detail || !educationalDetails.employee_type || !educationalDetails.address) return false;
if (educationalDetails.employee_type !== 11) {
const workReq = ["occupation", "occupation_detail", "income_currency", "annual_income", "work_country"];
if (!workReq.every(f => educationalDetails[f])) return false;
if (educationalDetails.work_country === 1) {
if (!educationalDetails.work_state || !educationalDetails.work_district) return false;
} else {
if (!educationalDetails.work_city) return false;
}
}
return true;
}
if (stepNum === 3) {
const required = ["fatherName", "motherName", "familyStatus", "nativePlace"];
return required.every(field => familyDetails[field]);
}
if (stepNum === 4) {
const required = ["diets", "hobbies", "dob", "tob"];
return required.every(field => {
const val = lifestyleDetails[field];
return Array.isArray(val) ? val.length > 0 : !!val;
});
}
if (stepNum === 5) {
const required = ["ageRange", "castes", "subCastes", "occupations", "educations", "hobbies", "annualIncome", "states", "districts"];
return required.every(field => {
const val = partnerPreferences[field];
return Array.isArray(val) ? val.length > 0 : !!val;
});
}
return false;
};
const getTitle = () => { const getTitle = () => {
const titles = { const titles = {
1: "Personal Details", 1: "Personal Details",
@ -1272,6 +1417,7 @@ useEffect(() => {
onStepClick={handleStepClick} onStepClick={handleStepClick}
enabledSteps={enabledSteps} enabledSteps={enabledSteps}
completedSteps={completedSteps} completedSteps={completedSteps}
checkStepValidity={checkStepValidity}
/> )} /> )}

168
src/hooks/useWebSocket.js Normal file
View File

@ -0,0 +1,168 @@
import { useEffect, useRef, useState } from 'react';
const WS_URL = "wss://www.thirukalyanam.amrithaa.net/backend/reverb/app/xk30gjh2ggmel5szmm5w?protocol=7&client=js&version=1.0";
// SINGLETON state to share across all components
let globalSocket = null;
let globalMessages = [];
let globalIsConnected = false;
let globalActiveChannels = new Set();
let subscribers = new Set();
let reconnectTimer = null;
let heartbeatTimer = null;
const notifySubscribers = () => {
subscribers.forEach(callback => callback({
messages: [...globalMessages],
isConnected: globalIsConnected
}));
};
const connect = () => {
if (globalSocket?.readyState === WebSocket.OPEN) return;
if (globalSocket?.readyState === WebSocket.CONNECTING) return;
console.log("[WS] Connecting to:", WS_URL);
const socket = new WebSocket(WS_URL);
globalSocket = socket;
socket.onopen = () => {
console.log("[WS] Open - Waiting for handshake...");
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log("[WS] RAW:", data);
// 1. Handshake
if (data.event === "pusher:connection_established") {
console.log("[WS] Handshake Complete | Socket ID:", data.data ? JSON.parse(data.data).socket_id : "N/A");
globalIsConnected = true;
// Auto-subscribe to all pending channels
if (globalActiveChannels.size > 0) {
console.log(`[WS] Re-subscribing to ${globalActiveChannels.size} channels...`);
globalActiveChannels.forEach(channel => {
socket.send(JSON.stringify({
event: "pusher:subscribe",
data: { channel }
}));
console.log(`[WS] Subscribing to: ${channel}`);
});
}
notifySubscribers();
return;
}
// 2. Heartbeat
if (data.event === "pusher:ping") {
socket.send(JSON.stringify({ event: "pusher:pong", data: {} }));
return;
}
if (data.event === "pusher:pong") return;
// 3. Subscription Succeeded
if (data.event === "pusher_internal:subscription_succeeded") {
console.log(`[WS] ✅ Subscribed to ${data.channel}`);
return;
}
// 4. Subscription Error
if (data.event === "pusher:subscription_error") {
console.error(`[WS] ❌ Subscription failed for ${data.channel}`, data.data);
return;
}
// 5. Regular Events
console.log(`[WS] 📩 Event: ${data.event} | Channel: ${data.channel}`);
globalMessages = [...globalMessages, data];
notifySubscribers();
} catch (err) {
console.error("[WS] Parse Error:", err, event.data);
}
};
socket.onclose = (event) => {
console.log(`[WS] Disconnected (Code: ${event.code}) - Reconnecting in 5s...`);
globalIsConnected = false;
notifySubscribers();
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 5000);
};
socket.onerror = (err) => {
console.error("[WS] Error:", err);
socket.close();
};
// Heartbeat
if (heartbeatTimer) clearInterval(heartbeatTimer);
heartbeatTimer = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ event: "pusher:ping", data: {} }));
}
}, 20000);
};
export const useWebSocket = (channels = []) => {
const [state, setState] = useState({
messages: globalMessages,
isConnected: globalIsConnected
});
useEffect(() => {
// Add this component's listener
const callback = (newState) => setState(newState);
subscribers.add(callback);
// Initialize connection if needed
if (!globalSocket) {
connect();
}
return () => {
subscribers.delete(callback);
};
}, []);
// Handle Dynamic Subscriptions
useEffect(() => {
if (!channels || channels.length === 0) return;
channels.forEach(channel => {
if (channel && !globalActiveChannels.has(channel)) {
globalActiveChannels.add(channel);
if (globalIsConnected && globalSocket?.readyState === WebSocket.OPEN) {
globalSocket.send(JSON.stringify({
event: "pusher:subscribe",
data: { channel }
}));
console.log(`[WS] Dynamic Subscribe: ${channel}`);
}
}
});
// Optional: Cleanup old channels if they are no longer in the provided array
// But for a chat app, we often want to keep listening to notification channels.
// So we'll leave them for now unless we implement a more complex cleanup.
}, [channels]);
const unsubscribe = (channel) => {
if (channel && globalActiveChannels.has(channel)) {
globalActiveChannels.delete(channel);
if (globalIsConnected && globalSocket?.readyState === WebSocket.OPEN) {
globalSocket.send(JSON.stringify({
event: "pusher:unsubscribe",
data: { channel }
}));
console.log(`[WS] Unsubscribed from: ${channel}`);
}
}
};
return { ...state, unsubscribe };
};

View File

@ -1,42 +1,57 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Tabs, Tab, Box, Chip } from '@mui/material'; import { Tabs, Tab, Box, CircularProgress } from '@mui/material';
import { CheckCircle, Phone, ExpandMore } from '@mui/icons-material'; import { CheckCircle, Phone } from '@mui/icons-material';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { getBlockedProfiles, getReportedProfiles, unblockProfile } from '../services/profileActionApi';
import { toast } from 'react-hot-toast';
const BlockedProfile = ({ profile }) => ( const BlockedProfile = ({ profile, onUnblock }) => (
<div className="bg-white border border-1 border-red-100 rounded-lg shadow-sm p-6 mb-4"> <div className="bg-white border border-1 border-red-100 rounded-lg shadow-sm p-6 mb-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<img <img
src={profile.image} src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name} alt={profile.name}
className="w-32 h-32 rounded-2xl object-cover border-2 border-[#A70710]" className="w-32 h-32 rounded-2xl object-cover border-2 border-[#A70710]"
/> />
<button className="w-8 h-8 flex justify-center items-center absolute bottom-0 right-0 bg-[#A70710] text-white rounded-full shadow-lg"> {/* <button className="w-8 h-8 flex justify-center items-center absolute bottom-0 right-0 bg-[#A70710] text-white rounded-full shadow-lg">
<Phone className="w-4 h-4" /> <Phone className="w-4 h-4" />
</button> </button> */}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-1"> {/* <div className="flex items-center gap-2 mb-1">
<CheckCircle className="text-green-500 w-5 h-5" /> <CheckCircle className="text-green-500 w-5 h-5" />
<span className="text-green-500 font-medium">Verified</span> <span className="text-green-500 font-medium">Verified</span>
</div> </div> */}
<h2 className="text-2xl font-bold text-gray-900 mb-1">{profile.name}</h2> <h2 className="text-2xl font-bold text-gray-900 mb-1">{profile.name}</h2>
<p className="text-gray-500 text-sm mb-3">{profile.id} | Profile Created by Parent</p> <p className="text-gray-500 text-sm mb-3">{profile.member_id} | Profile Created by Parent</p>
<div className="space-y-1 text-gray-700"> <div className="space-y-1 text-gray-700">
<p className="font-medium">{profile.age} yrs, {profile.height}, {profile.language},</p> <p className="font-medium">{profile.age ? `${profile.age} yrs` : ''}{profile.age && profile.height ? ', ' : ''}{profile.height || ''}</p>
<p className="font-medium">{profile.location},</p>
<p className="font-medium">{profile.education}, {profile.occupation}, {profile.income}, {profile.state}, India</p> <p className="font-medium">
{[profile.district_name, profile.state_name].filter(Boolean).join(', ')}
</p>
<p className="font-medium">
{[
profile.education,
profile.occupation,
profile.annual_income_name ? `${profile.annual_income_name}` : null
].filter(Boolean).join(', ')}
</p>
</div> </div>
</div> </div>
</div> </div>
<div className="mt-6 flex items-center justify-between border-t border-[#A70710] pt-4"> <div className="mt-6 flex items-center justify-between border-t border-[#A70710] pt-4">
<p className="text-gray-600">You have blocked this profile</p> <p className="text-gray-600">You have blocked this profile</p>
<button className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors"> <button
onClick={() => onUnblock(profile.id)}
className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors"
>
UnBlock UnBlock
</button> </button>
</div> </div>
@ -49,7 +64,7 @@ const ReportedProfile = ({ profile, onViewReason }) => {
<div className="flex flex-col sm:flex-row items-start gap-4"> <div className="flex flex-col sm:flex-row items-start gap-4">
<div className='overflow-hidden w-[100%] h-[100%] max-w-50 max-h-45 rounded-lg flex-shrink-0'> <div className='overflow-hidden w-[100%] h-[100%] max-w-50 max-h-45 rounded-lg flex-shrink-0'>
<img <img
src={profile.image} src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name} alt={profile.name}
className="w-full h-full object-cover " className="w-full h-full object-cover "
/> />
@ -57,25 +72,39 @@ const ReportedProfile = ({ profile, onViewReason }) => {
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<h3 className="text-lg font-bold text-gray-900 mb-1">{profile.name}</h3> <h3 className="text-lg font-bold text-gray-900 mb-1">{profile.name}</h3>
<p className="text-sm text-gray-500 mb-2"> <p className="text-sm text-gray-500 mb-2">
ID : {profile.id} <span className="text-xs ml-1">Last seen {profile.lastSeen}</span> ID : {profile.member_id} {profile.last_seen_at && <span className="text-xs ml-1">{profile.last_seen_at}</span>}
</p> </p>
<div className="space-y-1.5 text-sm"> <div className="space-y-1.5 text-sm">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-gray-400"></span> <span className="text-gray-400"></span>
<span className="text-gray-600">Profile created by Parent</span> <span className="text-gray-600">Profile created by Parent</span>
{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}</span> <span className="text-gray-600">{profile.caste_name}</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.location}</span> <span className="text-gray-600">{profile.district_name}</span>
</>
)}
</div> </div>
</div> </div>
@ -100,20 +129,20 @@ const ReportReasonModal = ({ profile, onClose }) => {
<div className="bg-white rounded-lg shadow-2xl max-w-md w-full p-6 animate-slideUp"> <div className="bg-white rounded-lg shadow-2xl max-w-md w-full p-6 animate-slideUp">
<div className="flex items-start gap-4 mb-4"> <div className="flex items-start gap-4 mb-4">
<img <img
src={profile.image} src={profile.photo || 'https://via.placeholder.com/150'}
alt={profile.name} alt={profile.name}
className="w-16 h-20 rounded-lg object-cover flex-shrink-0" className="w-16 h-20 rounded-lg object-cover flex-shrink-0"
/> />
<div> <div>
<h3 className="text-lg font-bold text-gray-900">{profile.name}</h3> <h3 className="text-lg font-bold text-gray-900">{profile.name}</h3>
<p className="text-sm text-gray-500">ID : {profile.id}</p> <p className="text-sm text-gray-500">ID : {profile.member_id}</p>
</div> </div>
</div> </div>
<div className="border-t pt-4"> <div className="border-t pt-4">
<h4 className="font-bold text-gray-900 mb-3">Reason For Report</h4> <h4 className="font-bold text-gray-900 mb-3">Reason For Report</h4>
<p className="text-sm text-gray-600 leading-relaxed bg-gray-50 p-3 rounded-lg mb-4"> <p className="text-sm text-gray-600 leading-relaxed bg-gray-50 p-3 rounded-lg mb-4">
{profile.reportReason} {profile.reason}
</p> </p>
</div> </div>
<div className='w-full flex justify-center'> <div className='w-full flex justify-center'>
@ -135,116 +164,62 @@ const ReportReasonModal = ({ profile, onClose }) => {
function BlockedProfileListPage() { function BlockedProfileListPage() {
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [selectedReport, setSelectedReport] = useState(null); const [selectedReport, setSelectedReport] = useState(null);
const blockedProfiles = [ const [blockedProfiles, setBlockedProfiles] = useState([]);
{ const [reportedProfiles, setReportedProfiles] = useState([]);
id: 'M6075010', const [loading, setLoading] = useState(true);
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
},
{
id: 'M6075010',
name: 'Aravindh Vinayak M',
age: 37,
height: "5'6\"",
language: 'Tamil',
location: 'Karuneegar',
education: 'BE',
occupation: 'Clerk',
income: '9 - 10 Lakhs',
state: 'Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
}
];
const reportedProfiles = [ useEffect(() => {
{ fetchData();
id: 'TK52586A', }, []);
name: 'Pavilash . P',
age: 23, const fetchData = async () => {
lastSeen: 'Nov 25', setLoading(true);
caste: 'Agamudayar / Arcot / Thuluva vellala', try {
occupation: 'Engineer-non IT', const [blockedRes, reportedRes] = await Promise.all([
location: 'Chennai', getBlockedProfiles(),
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop', getReportedProfiles()
showReason: true, ]);
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
}, if (blockedRes.status === "success") {
{ setBlockedProfiles(blockedRes.data);
id: 'TK52586A',
name: 'Pavilash . P',
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">
@ -259,14 +234,14 @@ function BlockedProfileListPage() {
textTransform: 'none', textTransform: 'none',
fontSize: '1rem', fontSize: '1rem',
fontWeight: 600, fontWeight: 600,
minWidth: 120, minWidth: 150,
}, },
'& .Mui-selected': { '& .Mui-selected': {
color: '#fff !important', color: '#fff !important',
background:"#A70710" background:"#A70710"
}, },
'& .MuiTabs-indicator': { '& .MuiTabs-indicator': {
backgroundColor: '#A70710', backgroundColor: 'transparent',
}, },
}} }}
> >
@ -277,28 +252,32 @@ function BlockedProfileListPage() {
<div className="transition-all duration-300"> <div className="transition-all duration-300">
{activeTab === 0 && ( {activeTab === 0 && (
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-2'> <div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-4 px-4'>
{blockedProfiles.map((profile, index) => ( {blockedProfiles.length > 0 ? (
<BlockedProfile key={index} profile={profile} /> blockedProfiles.map((profile, index) => (
))} <BlockedProfile key={profile.id || index} profile={profile} onUnblock={handleUnblock} />
))
) : (
<div className="col-span-full text-center py-10 text-gray-500">No blocked profiles found.</div>
)}
</div> </div>
)} )}
{activeTab === 1 && ( {activeTab === 1 && (
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-2'> <div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-4 px-4'>
{reportedProfiles.map((profile, index) => ( {reportedProfiles.length > 0 ? (
reportedProfiles.map((profile, index) => (
<ReportedProfile <ReportedProfile
key={index} key={profile.id || index}
profile={profile} profile={profile}
onViewReason={setSelectedReport} onViewReason={setSelectedReport}
/> />
))} ))
</div> ) : (
<div className="col-span-full text-center py-10 text-gray-500">No reported profiles found.</div>
)}
</div>
)} )}
</div> </div>
{/* Report Reason Modal */} {/* Report Reason Modal */}
<ReportReasonModal <ReportReasonModal
@ -307,7 +286,7 @@ function BlockedProfileListPage() {
/> />
</div> </div>
<style jsx>{` <style>{`
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;

View File

@ -1,8 +1,16 @@
import React, { useState } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2 } from 'lucide-react'; import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2, Loader2, MessageCircle } 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);
@ -11,241 +19,316 @@ const ChatUI = () => {
const [showChatMenu, setShowChatMenu] = useState(false); const [showChatMenu, setShowChatMenu] = useState(false);
const [openReport, setOpenReport] = useState(false); const [openReport, setOpenReport] = useState(false);
const contacts = [ const [contacts, setContacts] = useState([]);
{ const [chatMessages, setChatMessages] = useState([]);
id: 1, const [chatDetails, setChatDetails] = useState(null);
name: 'Kalai', const [loadingContacts, setLoadingContacts] = useState(true);
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai', const [loadingMessages, setLoadingMessages] = useState(false);
lastMessage: 'Hi bro!n how are you Long time no see', const [sendingMessage, setSendingMessage] = useState(false);
time: '10 Nov 2025, 10 : 23 AM', const [searchTerm, setSearchTerm] = useState("");
online: false const [loadingMore, setLoadingMore] = useState(false);
}, const [currentPage, setCurrentPage] = useState(1);
{ const [hasMore, setHasMore] = useState(true);
id: 2,
name: 'Sabitha',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sabitha',
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
}
];
const messages = { const messagesEndRef = useRef(null);
1: [ const scrollContainerRef = useRef(null);
{ const isPaginating = useRef(false);
id: 1, const previousScrollHeight = useRef(0);
sender: 'other', const observerRef = useRef(null);
text: 'Let\'s do it! I\'m in a meeting until noon.', const topMarkerRef = useRef(null);
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
}
]
};
const callHistory = [ // WebSocket Integration - Listening to BOTH Notifications and the Active Chat
{ const profileId = localStorage.getItem("profile_id") || personalDetails?.id;
id: 1, const userId = localStorage.getItem("user_id") || profileId;
name: 'Kalai',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
status: 'Incoming call',
time: '10 : 00 AM',
date: 'Today'
},
{
id: 2,
name: 'Lia',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lia',
status: 'Outgoing',
time: '10 : 00 AM',
date: 'Today'
},
{
id: 3,
name: 'Moi',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Moi',
status: 'Incoming call',
time: '10 : 00 AM',
date: 'Today'
},
{
id: 4,
name: 'Sri',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Sri',
status: 'Outgoing',
time: '10 : 00 AM',
date: 'Today'
},
{
id: 5,
name: 'Kalai',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
status: 'Outgoing',
time: '10 : 00 AM',
date: 'Today'
},
{
id: 6,
name: 'Kalai',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
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'
}
];
const [chatMessages, setChatMessages] = useState({ // To show messages INSTANTLY in bubbles, we must listen to the specific chat channel
1: [ const activeChatChannel = selectedChat ? `chat-${selectedChat}` : null;
{ 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 }, // Flutter uses both dotted and non-dotted formats for channels in some Reverb setups
{ id: 3, sender: 'date', text: 'Today', isDate: true, read: true | false }, const wsChannels = React.useMemo(() => {
{ id: 4, sender: 'me', text: "Can's get lunch. How about tomorrow?", time: '09:42 am', isDate: false , read: true }, const channels = [];
{ 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 }, if (userId && userId !== "null") {
], channels.push(`user-chat-notification${userId}`);
// 2,3,... if needed 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;
const handleSendMessage = () => { setLoadingMore(true);
if (!message.trim() || !selectedChat) return; isPaginating.current = true;
const nextPage = currentPage + 1;
const now = new Date(); try {
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (scrollContainerRef.current) {
previousScrollHeight.current = scrollContainerRef.current.scrollHeight;
}
setChatMessages(prev => { const response = await getChatMessages(selectedChat, nextPage);
const prevMsgs = prev[selectedChat] || []; if (response.status) {
const newMsg = { const olderMessages = response.messages.data.reverse();
id: prevMsgs.length ? prevMsgs[prevMsgs.length - 1].id + 1 : 1, setChatMessages(prev => [...olderMessages, ...prev]);
sender: 'me', setChatDetails(response.messages);
text: message.trim(), setCurrentPage(nextPage);
time: timeString, setHasMore(response.messages.current_page < response.messages.last_page);
isDate: false, }
} 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 }
);
if (topMarkerRef.current) {
observer.observe(topMarkerRef.current);
}
return () => observer.disconnect();
}, [hasMore, loadingMore, selectedChat, loadMoreMessages]);
const handleScroll = (e) => {
// Keep for manual debugging if needed
}; };
// FORCE REFRESH: Trigger on ANY new websocket message
useEffect(() => {
const currentLen = wsMessages.length;
const lastProcessed = processedMsgCount.current;
if (currentLen > lastProcessed) {
const newWsMsgs = wsMessages.slice(lastProcessed);
console.log(`[WS-REAL-TIME] New messages: ${newWsMsgs.length} (Total: ${currentLen}, Last Processed: ${lastProcessed})`);
let shouldRefreshContacts = false;
let shouldRefreshMessages = false;
newWsMsgs.forEach(lastMsg => {
// Skip pings and internal Pusher system events
if (lastMsg.event?.startsWith('pusher:')) return;
if (lastMsg.event === 'pusher_internal:subscription_succeeded') return;
console.log(`[WS-REAL-TIME] Processing: ${lastMsg.event}`);
try {
const data = lastMsg.data;
const parsedData = typeof data === 'string' ? JSON.parse(data) : data;
const msgObj = parsedData.message || parsedData.data || parsedData;
// 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');
if (isMessageEvent || (msgObj && (msgObj.id || msgObj.message || msgObj.chat_id))) {
console.log("[WS-REAL-TIME] Match found!");
shouldRefreshContacts = true;
// OPTIMISTIC UPDATE: Update the contact list snippet locally for instant feedback
if (msgObj && msgObj.message) {
setContacts(prev => {
const targetId = msgObj.chat_id || msgObj.sender_id || selectedChat;
return prev.map(c => {
if (c.id == targetId) {
return { return {
...prev, ...c,
[selectedChat]: [...prevMsgs, newMsg], 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
}; };
}
return c;
});
});
}
if (selectedChat) {
shouldRefreshMessages = true;
// Manual injection for instant UI update
if (msgObj && (msgObj.id || msgObj.message)) {
setChatMessages(prev => {
const isDuplicate = prev.some(m => m.id === msgObj.id);
if (isDuplicate) return prev;
const sanitizedMsg = {
...msgObj,
chat_by: msgObj.chat_by || (msgObj.sender_id == profileId ? 'me' : 'them'),
time: msgObj.time || new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
};
return [...prev, sanitizedMsg];
});
}
}
}
} catch (e) {
console.error("[WS-REAL-TIME] Error:", e);
}
}); });
setMessage(''); 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 {
...c,
last_message: msgText,
last_message_time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
};
}
return c;
}));
// Refresh messages and contacts silently in the background
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);
@ -265,15 +348,12 @@ const [openReport, setOpenReport] = useState(false);
}; };
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-screen gap-[20px] bg-gray-50"> <div className="w-full max-w-[1400px] mx-auto flex h-[85vh] gap-[20px] bg-gray-50 my-4">
{/* Sidebar - Chat List */} {/* Sidebar - Chat List */}
<div className={`w-full md:w-96 bg-white border border-1 border-gray-200 rounded-[10px] flex flex-col ${ <div key="chat-sidebar" 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 */}
@ -311,16 +391,28 @@ const [openReport, setOpenReport] = useState(false);
<input <input
type="text" type="text"
placeholder="Search your partner here..." placeholder="Search your partner here..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500" value={searchTerm}
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">
{contacts.map((contact) => ( {loadingContacts ? (
<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.id} key={`contact-${contact.id}-${index}`}
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' : ''
@ -328,109 +420,39 @@ const [openReport, setOpenReport] = useState(false);
> >
<div className="relative"> <div className="relative">
<img <img
src={contact.avatar} src={contact.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"}
alt={contact.name} alt={contact.name}
className="w-12 h-12 rounded-full" className="w-12 h-12 rounded-full object-cover"
/> />
{contact.online && ( {contact.is_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">{contact.name}</h3> <h3 className="font-medium text-gray-900 truncate">{contact.name}</h3>
<span className="text-xs text-gray-500">{contact.time}</span> <span className="text-xs text-gray-500 whitespace-nowrap">{contact.time}</span>
</div> </div>
<p className="text-sm text-gray-600 truncate">
{contact.lastMessage}
</p>
</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 justify-between">
<div className="flex items-center gap-3"> <p className="text-sm text-gray-600 truncate mr-2">
<button {contact.latest_message}
onClick={handleBackToList} </p>
className="md:hidden p-2 hover:bg-gray-100 rounded" {contact.unread_message_count > 0 && (
> <span className="bg-[#034E08] text-white text-[10px] rounded-full w-4 h-4 flex items-center justify-center">
<ArrowLeft className="w-5 h-5 text-gray-600" /> {contact.unread_message_count}
</button> </span>
<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>
)} )}
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Chat Area */} {/* Chat Area */}
{selectedChat && !showCallHistory && ( {selectedChat && !showCallHistory && (
<div className={`border border-1 border-gray-200 rounded-[10px] flex-1 flex flex-col bg-white ${ <div key="chat-main-area" 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 */}
@ -443,24 +465,21 @@ const [openReport, setOpenReport] = useState(false);
<ArrowLeft className="w-5 h-5 text-gray-600" /> <ArrowLeft className="w-5 h-5 text-gray-600" />
</button> </button>
<img <img
src={contacts.find(c => c.id === selectedChat)?.avatar} src={chatDetails?.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"}
alt="Avatar" alt="Avatar"
className="w-10 h-10 rounded-full" className="w-10 h-10 rounded-full object-cover"
/> />
<div> <div>
<h3 className="font-medium text-gray-900"> <h3 className="font-medium text-gray-900">
Priya {chatDetails?.name}
</h3> </h3>
<div className="flex items-center gap-1 text-xs text-green-500"> <div className={`flex items-center gap-1 text-xs ${chatDetails?.lastSeen === 'Online' ? 'text-green-500' : 'text-gray-400'}`}>
<div className="w-2 h-2 bg-green-500 rounded-full"></div> <div className={`w-2 h-2 rounded-full ${chatDetails?.lastSeen === 'Online' ? 'bg-green-500' : 'bg-gray-300'}`}></div>
<span>Online</span> <span>{chatDetails?.lastSeen || 'Offline'}</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)}
@ -494,60 +513,69 @@ const [openReport, setOpenReport] = useState(false);
</div> </div>
{/* Messages */} {/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 md:h-[400px]"> <div
{chatMessages[selectedChat]?.map((msg) => ( ref={scrollContainerRef}
<div key={msg.id}> className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50"
{msg.isDate ? ( >
<div className="text-center text-xs text-gray-500 my-4"> {/* Top Marker for Intersection Observer */}
{msg.text} <div ref={topMarkerRef} className="h-1" />
{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
className={`flex ${ key={`msg-${msg.id}-${index}`}
msg.sender === 'me' ? 'justify-end' : 'justify-start' className={`flex ${msg.chat_by === 'me' ? 'justify-end' : 'justify-start'}`}
}`}
> >
<div <div
className={`max-w-xs md:max-w-md px-4 py-2 rounded-2xl ${ className={`max-w-[75%] md:max-w-md px-4 py-2 rounded-2xl shadow-sm ${
msg.sender === 'me' msg.chat_by === '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">{msg.text}</p> <p className="text-sm break-words">{msg.message}</p>
<div className='flex gap-1 items-center justify-end mt-1'> <div className='flex gap-1 items-center justify-end mt-1'>
{msg.time && ( <span className="text-[10px] text-gray-500">
<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 ? (
</div> <CheckCheck className="w-3.5 h-3.5 text-blue-500" />
)}
{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-4 h-4 text-[#034E08]" /> <Check className="w-3.5 h-3.5 text-gray-400" />
)} )}
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
))
)} )}
</div> <div ref={messagesEndRef} />
))}
</div> </div>
{/* Message Input */} {/* Message Input */}
@ -559,28 +587,53 @@ const [openReport, setOpenReport] = useState(false);
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..."
className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:border-[#034E08]" disabled={chatDetails?.disable_chat}
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}
className="p-2.5 bg-[#034E08] text-white rounded-lg hover:bg-blue-600" disabled={!message.trim() || sendingMessage || chatDetails?.disable_chat}
className="p-2.5 bg-[#034E08] text-white rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
> >
<Send className="w-5 h-5" /> {sendingMessage ? <Loader2 className="w-5 h-5 animate-spin" /> : <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 className="flex-1 hidden md:flex items-center justify-center bg-gray-50"> <div key="chat-empty-state" 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>
</> </>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,58 @@
import React, { useState, useEffect } from 'react';
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import MatrimonyProfile from "../components/profiledetail/MatrimonyProfile" import MatrimonyProfile from "../components/profiledetail/MatrimonyProfile"
import PartnerPreferences from "../components/profiledetail/PartnerPreferences" import PartnerPreferences from "../components/profiledetail/PartnerPreferences"
import MatchingList from "../components/profiledashboard/MatchingList"; import MatchingList from "../components/profiledashboard/MatchingList";
import { getProfileDetail } from "../services/profileActionApi";
import { CircularProgress, Box } from "@mui/material";
const ProfileDetailPage = () => { const ProfileDetailPage = () => {
const { id } = useParams(); const { id } = useParams();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDetail = async () => {
setLoading(true);
try {
const res = await getProfileDetail(id);
setData(res);
} catch (error) {
console.error("Failed to fetch profile details:", error);
} finally {
setLoading(false);
}
};
if (id) {
fetchDetail();
}
}, [id]);
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<CircularProgress color="error" />
</Box>
);
}
if (!data) {
return (
<div className="text-center py-20 text-gray-500 text-xl">
Profile details not found.
</div>
);
}
return ( return (
<> <>
<div className="w-[100%] max-w-[1400px] mx-auto my-10"> <div className="w-[100%] max-w-[1400px] mx-auto my-10">
<MatrimonyProfile/> <MatrimonyProfile data={data} />
<PartnerPreferences/> <PartnerPreferences data={data} />
<MatchingList/> <MatchingList matches={data.all_matches} />
</div> </div>
</> </>
) );
} };
export default ProfileDetailPage export default ProfileDetailPage;

View File

@ -74,6 +74,13 @@ const LoginPage = () => {
localStorage.setItem("access_token", token); localStorage.setItem("access_token", token);
setAccessToken(token); setAccessToken(token);
// Store profile_id and user_id for WebSocket channels
const profileId = data?.profile_id || data?.data?.profile_id;
const userId = data?.user_id || data?.data?.user_id;
if (profileId) localStorage.setItem("profile_id", profileId);
if (userId) localStorage.setItem("user_id", userId);
toast.success("Login Successful!"); toast.success("Login Successful!");
navigate("/dashboard-home"); navigate("/dashboard-home");
} else { } else {

View File

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

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

@ -0,0 +1,34 @@
import axiosInstance from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
export const getChatList = async (searchValue = "") => {
try {
// Add timestamp to prevent caching
const response = await axiosInstance.get(`${API_ENDPOINTS.CHAT_LIST}?search_value=${searchValue}&_t=${Date.now()}`);
return response.data;
} catch (error) {
console.error("Error fetching chat list:", error);
throw error;
}
};
export const getChatMessages = async (chatId, page = 1) => {
try {
const response = await axiosInstance.get(`${API_ENDPOINTS.CHAT_MESSAGES(chatId)}?page=${page}`);
return response.data;
} catch (error) {
console.error("Error fetching chat messages:", error);
throw error;
}
};
export const sendMessage = async (chatId, message) => {
try {
// Correct endpoint based on user request: chat/message/send?chat_id={id}&message={text}
const response = await axiosInstance.post(`chat/message/send?chat_id=${chatId}&message=${encodeURIComponent(message)}`);
return response.data;
} catch (error) {
console.error("Error sending message:", error);
throw error;
}
};

View File

@ -0,0 +1,65 @@
import axiosInstance from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
export const getBlockedProfiles = async () => {
try {
const response = await axiosInstance.get(API_ENDPOINTS.BLOCK_PROFILE_LIST);
return response.data;
} catch (error) {
console.error("Error fetching blocked profiles:", error);
throw error;
}
};
export const getReportedProfiles = async () => {
try {
const response = await axiosInstance.get(API_ENDPOINTS.REPORT_PROFILE_LIST);
return response.data;
} catch (error) {
console.error("Error fetching reported profiles:", error);
throw error;
}
};
export const unblockProfile = async (profileId) => {
try {
const response = await axiosInstance.post(`unblock_profile?profile_id=${profileId}`);
return response.data;
} catch (error) {
console.error("Error unblocking profile:", error);
throw error;
}
};
export const getProfileDetail = async (profile_id) => {
try {
const response = await axiosInstance.get(`${API_ENDPOINTS.PROFILE_DETAIL}?profile_id=${profile_id}`);
return response.data;
} catch (error) {
console.error("Error fetching profile detail:", error);
throw error;
}
};
export const getInterestList = async (tab, type) => {
try {
const response = await axiosInstance.get(`${API_ENDPOINTS.INTEREST_LIST}?tab=${tab}&type=${type}`);
return response.data;
} catch (error) {
console.error("Error fetching interest list:", error);
throw error;
}
};
export const updateInterestStatus = async (profile_id, status) => {
try {
const response = await axiosInstance.post(API_ENDPOINTS.UPDATE_INTEREST_STATUS, {
profile_id,
status
});
return response.data;
} catch (error) {
console.error("Error updating interest status:", error);
throw error;
}
};

View File

@ -10,7 +10,9 @@ export const shortlistProfile = async (profileId) => {
}; };
export const sendInterest = async (profileId) => { export const sendInterest = async (profileId) => {
const response = await axiosInstance.post(`interest?profile_id=${profileId}`); const response = await axiosInstance.post(`interest_send`, {
profile_id: profileId // ✅ sent in request body
});
if (response.data?.status === "error") { if (response.data?.status === "error") {
throw new Error(response.data.message || "Failed to send interest"); throw new Error(response.data.message || "Failed to send interest");
} }