Build and deployment update: Refined registration forms, stepper navigation, and chat synchronization fixes
This commit is contained in:
parent
68f97c40dc
commit
c467271927
@ -57,9 +57,15 @@ NOTIFICATION_COUNT:"notification/un_read_count",
|
||||
PROFILES_FILTER_LIST: "profiles/lists",
|
||||
PROFILES_FILTER_MASTER: "profiles/filter/masters",
|
||||
|
||||
|
||||
DASHBOARD_API: "dashboard",
|
||||
HEADER_API: "header_data",
|
||||
SHORTLIST_API: "shortlist_profile",
|
||||
|
||||
BLOCK_PROFILE_LIST: "block_profile_list",
|
||||
REPORT_PROFILE_LIST: "report_profile_list",
|
||||
PROFILE_DETAIL: "profiles/detail",
|
||||
INTEREST_LIST: "interest_lists",
|
||||
UPDATE_INTEREST_STATUS: "update_interest_status",
|
||||
CHAT_LIST: "chat/lists",
|
||||
CHAT_MESSAGES: (id) => `chat/${id}/messages`,
|
||||
UNREAD_CHAT_COUNT: "chat/un_read_chat_count",
|
||||
};
|
||||
|
||||
@ -242,7 +242,7 @@ const navigate = useNavigate();
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} className="bg-white rounded-2xl shadow-xl overflow-hidden select-none">
|
||||
<div className="relative">
|
||||
<div classname=" relative bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]" style={{height:"300px"}}>
|
||||
<div className=" relative bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]" style={{height:"300px"}}>
|
||||
|
||||
<img
|
||||
src={profile.image}
|
||||
|
||||
@ -4,6 +4,7 @@ import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import SwipeableDrawer from "@mui/material/SwipeableDrawer";
|
||||
import { useWebSocket } from "../../hooks/useWebSocket";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
@ -19,7 +20,7 @@ import Button from "@mui/material/Button";
|
||||
import LazyImage from "./LazyImage";
|
||||
import Logo from "../../assets/images/logo.png";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { useTheme, useMediaQuery, ListItemIcon } from "@mui/material";
|
||||
import { Home, Users, Heart, MessageCircle, Search, Bell } from "lucide-react";
|
||||
import { isAuthenticated } from "../../utills/auth";
|
||||
@ -27,7 +28,7 @@ import userimg from "../../assets/images/bride1.jpg"
|
||||
import axiosInstance, { logoutAPI } from "../../api/axiosInstance";
|
||||
import toast from "react-hot-toast";
|
||||
import { API_ENDPOINTS } from "../../api/apiEndpoints";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getHeaderDetails } from "../../api/preview.api";
|
||||
const NAV_LINKS = [
|
||||
@ -160,6 +161,7 @@ const ProfileHeader = () => {
|
||||
const [profileDrawerOpen, setProfileDrawerOpen] = useState(false);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [logoutModalOpen, setLogoutModalOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { personalDetails } = useSelector((state) => state.registerform);
|
||||
|
||||
@ -189,17 +191,104 @@ const ProfileHeader = () => {
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!auth,
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
|
||||
// WebSocket for real-time updates - Match the robust strategy from ChatPage
|
||||
const profileId = localStorage.getItem("profile_id");
|
||||
const userId = localStorage.getItem("user_id");
|
||||
|
||||
const wsChannels = useMemo(() => {
|
||||
const channels = [];
|
||||
if (profileId && profileId !== "null") {
|
||||
channels.push(`user-chat-notification${profileId}`);
|
||||
channels.push(`user-chat-notification.${profileId}`);
|
||||
channels.push(`partner-chat${profileId}`);
|
||||
channels.push(`partner-chat.${profileId}`);
|
||||
}
|
||||
if (userId && userId !== "null") {
|
||||
channels.push(`user-notification${userId}`);
|
||||
channels.push(`user-notification.${userId}`);
|
||||
}
|
||||
return [...new Set(channels.filter(Boolean))];
|
||||
}, [profileId, userId]);
|
||||
|
||||
const { messages: wsMessages, isConnected } = useWebSocket(wsChannels);
|
||||
const processedMsgCount = useRef(wsMessages.length);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
console.log("[HEADER-WS] Connected, refreshing badges...");
|
||||
queryClient.invalidateQueries({ queryKey: ["notificationCount"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["unreadChatCount"] });
|
||||
}
|
||||
}, [isConnected, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wsMessages.length > processedMsgCount.current) {
|
||||
const newWsMsgs = wsMessages.slice(processedMsgCount.current);
|
||||
console.log(`[HEADER-WS] Detected ${newWsMsgs.length} new signals.`);
|
||||
|
||||
let shouldRefresh = false;
|
||||
|
||||
newWsMsgs.forEach(lastMsg => {
|
||||
if (lastMsg.event?.startsWith('pusher:')) return;
|
||||
|
||||
// Lenient detection matching ChatPage
|
||||
const isMessageEvent = lastMsg.event?.toLowerCase().includes('message') ||
|
||||
lastMsg.event?.toLowerCase().includes('chat') ||
|
||||
lastMsg.event?.toLowerCase().includes('notification');
|
||||
|
||||
if (isMessageEvent) {
|
||||
console.log(`[HEADER-WS] Relevant event detected: ${lastMsg.event}, refreshing counts...`);
|
||||
shouldRefresh = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldRefresh) {
|
||||
queryClient.invalidateQueries({ queryKey: ["unreadChatCount"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["notificationCount"] });
|
||||
}
|
||||
|
||||
processedMsgCount.current = wsMessages.length;
|
||||
}
|
||||
}, [wsMessages, queryClient]);
|
||||
|
||||
const { data: chatCountData } = useQuery({
|
||||
queryKey: ["unreadChatCount"],
|
||||
queryFn: async () => {
|
||||
const res = await axiosInstance.get(API_ENDPOINTS.UNREAD_CHAT_COUNT);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!auth,
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const notificationCount = notificationData?.count || 0;
|
||||
|
||||
|
||||
const chatCount = chatCountData?.count || 0;
|
||||
|
||||
const { data: headerData } = useQuery({
|
||||
queryKey: ["headerDetails"],
|
||||
queryFn: getHeaderDetails,
|
||||
enabled: !!auth,
|
||||
});
|
||||
|
||||
// AUTO-SYNC: Recover missing IDs from Header API data
|
||||
useEffect(() => {
|
||||
if (headerData?.myDetails) {
|
||||
const myId = headerData.myDetails.id || headerData.myDetails.profile_id;
|
||||
const uId = headerData.myDetails.user_id;
|
||||
if (myId && (localStorage.getItem("profile_id") === "null" || !localStorage.getItem("profile_id"))) {
|
||||
localStorage.setItem("profile_id", myId);
|
||||
console.log("Header API auto-synced profileId:", myId);
|
||||
}
|
||||
if (uId && (localStorage.getItem("user_id") === "null" || !localStorage.getItem("user_id"))) {
|
||||
localStorage.setItem("user_id", uId);
|
||||
console.log("Header API auto-synced userId:", uId);
|
||||
}
|
||||
}
|
||||
}, [headerData]);
|
||||
|
||||
const apiProfileImage = headerData?.myDetails?.profile;
|
||||
|
||||
const handleMenuClick = (item) => {
|
||||
@ -327,10 +416,17 @@ const ProfileHeader = () => {
|
||||
{getNavIcon(index)}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={
|
||||
<div className="flex items-center justify-between">
|
||||
{label}
|
||||
<div className="flex items-center justify-between w-full pr-4">
|
||||
<span>{label}</span>
|
||||
{label === "Notifications" && notificationCount > 0 && (
|
||||
<span className="bg-red-600 text-white text-xs px-2 py-0.5 rounded-full">{notificationCount}</span>
|
||||
<span className="bg-red-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] flex items-center justify-center ml-2">
|
||||
{notificationCount}
|
||||
</span>
|
||||
)}
|
||||
{label === "Messages" && chatCount > 0 && (
|
||||
<span className="bg-red-600 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] flex items-center justify-center ml-2">
|
||||
{chatCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
} />
|
||||
@ -376,7 +472,10 @@ const ProfileHeader = () => {
|
||||
items={NAV_LINKS.map(link => link.label)}
|
||||
color="#034E08"
|
||||
activeItem={currentLabel}
|
||||
badges={{ "Notifications": notificationCount }}
|
||||
badges={{
|
||||
"Notifications": notificationCount,
|
||||
"Messages": chatCount
|
||||
}}
|
||||
onItemClick={(item) => {
|
||||
setSelectedItem(item);
|
||||
const link = NAV_LINKS.find(l => l.label === item);
|
||||
@ -386,7 +485,7 @@ const ProfileHeader = () => {
|
||||
</Box>
|
||||
|
||||
{(auth ? (
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<Box key="user-menu-box" sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Account Menu">
|
||||
<IconButton onClick={toggleProfileDrawer(true)}>
|
||||
<Avatar sx={{width:"50px", height:"50px"}} src={apiProfileImage || profileImage || userimg || "/static/images/avatar/2.jpg" }/>
|
||||
@ -395,7 +494,7 @@ const ProfileHeader = () => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
):( <button className="ml-1 bg-red-900 text-white px-4 py-2 rounded-md hover:bg-red-800 transition-colors"
|
||||
):( <button key="sign-in-btn" className="ml-1 bg-red-900 text-white px-4 py-2 rounded-md hover:bg-red-800 transition-colors"
|
||||
onClick={() => navigate("/login")}>Sign In / Sign Up</button>))}
|
||||
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ const AppPromoteSection = () => {
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16 max-w-6xl mx-auto"
|
||||
>
|
||||
{features.map((feature, index) => (
|
||||
<div className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
|
||||
<div key={index} className="relative overflow-hidden bg-white rounded-2xl p-2 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
|
||||
<BorderBeam
|
||||
colorFrom="#ff0000ff"
|
||||
colorTo="#338105ff"
|
||||
|
||||
@ -379,7 +379,7 @@ const DailyRecommendedCard = () => {
|
||||
</div>
|
||||
|
||||
{/* Custom Swiper Styles */}
|
||||
<style jsx global>{`
|
||||
<style>{`
|
||||
.swiper-pagination-bullet {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@ -307,7 +307,7 @@ const MatchingList = ({ matches }) => {
|
||||
</div>
|
||||
|
||||
{/* Custom Swiper Styles */}
|
||||
<style jsx global>{`
|
||||
<style>{`
|
||||
.swiper-pagination-bullet {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@ -84,14 +84,6 @@ const ProfileCard = ({ profile }) => {
|
||||
const zodiac2 = profile.star_name || profile.zodiac2 || null;
|
||||
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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
@ -167,7 +159,7 @@ const NewJoinedProfile = ({ profiles }) => {
|
||||
</div>
|
||||
|
||||
<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" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@ -176,7 +168,7 @@ const NewJoinedProfile = ({ profiles }) => {
|
||||
disabled={declineMutation.isPending}
|
||||
>
|
||||
<X size={18} /> {declineMutation.isPending ? "..." : "Decline"}
|
||||
</button>
|
||||
</button> */}
|
||||
|
||||
<button
|
||||
className={`px-2 py-1 rounded-[20px] border border-green-200 bg-green-50 text-green-900 flex items-center gap-1.5 font-semibold hover:bg-green-100 transition-colors ${interestMutation.isPending ? "opacity-50 cursor-wait" : ""}`}
|
||||
@ -191,9 +183,17 @@ const NewJoinedProfile = ({ profiles }) => {
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
const NewJoinedProfile = ({ profiles }) => {
|
||||
const swiperRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const displayProfiles = profiles || [];
|
||||
if (displayProfiles.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -299,7 +299,7 @@ const NewJoinedProfile = ({ profiles }) => {
|
||||
</div>
|
||||
|
||||
{/* Custom Swiper Styles */}
|
||||
<style jsx global>{`
|
||||
<style>{`
|
||||
.swiper-pagination-bullet {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@ -216,7 +216,7 @@ const VideoSwiperGallery = ({ videos }) => {
|
||||
{selectedVideo && <VideoModal />}
|
||||
|
||||
{/* Custom Swiper Styles */}
|
||||
<style jsx global>{`
|
||||
<style>{`
|
||||
.swiper-pagination-bullet {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
Heart,
|
||||
X,
|
||||
ChevronRight,
|
||||
SkipForward,
|
||||
Bookmark,
|
||||
MessageCircle,
|
||||
Ban,
|
||||
@ -19,8 +18,10 @@ import "swiper/css/navigation";
|
||||
import "swiper/css/pagination";
|
||||
import "swiper/css/thumbs";
|
||||
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 [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
@ -28,26 +29,17 @@ const MatrimonyProfile = () => {
|
||||
const mainSwiperRef = useRef(null);
|
||||
const modalSwiperRef = useRef(null);
|
||||
|
||||
const profile = {
|
||||
name: "Sudharshan M",
|
||||
id: "M8355880",
|
||||
verified: true,
|
||||
lastSeen: "Last seen few hour ago",
|
||||
age: "30 yrs",
|
||||
height: "5'5\"",
|
||||
caste: "Brahmin",
|
||||
education: "Engineer - Non IT",
|
||||
location: "Chennai",
|
||||
maritalStatus: "Never Married",
|
||||
createdBy: "Profile created by sibling",
|
||||
images: [
|
||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=600&h=800&fit=crop",
|
||||
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=600&h=800&fit=crop",
|
||||
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=600&h=800&fit=crop",
|
||||
"https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=600&h=800&fit=crop",
|
||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=600&h=800&fit=crop",
|
||||
],
|
||||
};
|
||||
if (!data) return null;
|
||||
|
||||
const profile = data.profile;
|
||||
const personal = data.personalDetails;
|
||||
const family = data.familyDetails;
|
||||
const education = data.educationalDetails;
|
||||
const lifestyle = data.lifestyleDetails;
|
||||
|
||||
const profileImages = personal.images && personal.images.length > 0
|
||||
? personal.images
|
||||
: [profile.profile_picture || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"];
|
||||
|
||||
const openModal = (index) => {
|
||||
setIsModalOpen(true);
|
||||
@ -58,6 +50,31 @@ const MatrimonyProfile = () => {
|
||||
}, 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 (
|
||||
<div className="">
|
||||
<div
|
||||
@ -76,16 +93,12 @@ const MatrimonyProfile = () => {
|
||||
prevEl: ".swiper-button-prev-custom",
|
||||
nextEl: ".swiper-button-next-custom",
|
||||
}}
|
||||
pagination={{
|
||||
type: "fraction",
|
||||
el: ".swiper-pagination-custom",
|
||||
}}
|
||||
onSwiper={(swiper) => {
|
||||
mainSwiperRef.current = swiper;
|
||||
}}
|
||||
className="h-full w-full"
|
||||
>
|
||||
{profile.images.map((img, idx) => (
|
||||
{profileImages.map((img, idx) => (
|
||||
<SwiperSlide key={idx}>
|
||||
<div
|
||||
className="w-[320px] h-[330px] cursor-pointer"
|
||||
@ -95,35 +108,21 @@ const MatrimonyProfile = () => {
|
||||
src={img}
|
||||
alt={`${profile.name} ${idx + 1}`}
|
||||
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>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</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">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</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">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</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>
|
||||
|
||||
@ -144,7 +143,7 @@ const MatrimonyProfile = () => {
|
||||
/>
|
||||
</svg>
|
||||
</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 className="relative">
|
||||
<button
|
||||
@ -161,10 +160,16 @@ const MatrimonyProfile = () => {
|
||||
</button>
|
||||
{showMenu && (
|
||||
<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
|
||||
</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
|
||||
</button>
|
||||
<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}
|
||||
</h1>
|
||||
<p className="text-gray-500 text-sm mb-4">
|
||||
{profile.id} | {profile.lastSeen}
|
||||
{profile.member_id} | {profile.last_seen_at}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2 mb-6 text-gray-700">
|
||||
<p className="flex flex-wrap gap-2">
|
||||
<span className="font-semibold">{profile.maritalStatus}</span>
|
||||
<span>•</span>
|
||||
<p className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm text-gray-500">
|
||||
{profile.createdBy}
|
||||
Profile created by {personal.profile_for || "N/A"}
|
||||
</span>
|
||||
{(personal.age || profile.age) && (
|
||||
<>
|
||||
<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>{profile.height}</span>
|
||||
<span>•</span>
|
||||
<span>{profile.caste}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">{profile.education}</span>
|
||||
<span> • </span>
|
||||
<span>{profile.location}</span>
|
||||
<span className="text-sm">
|
||||
{[
|
||||
safeVal(profile.religion, 'religion_name'),
|
||||
safeVal(profile.caste, 'caste_name'),
|
||||
safeVal(profile.sub_caste, 'sub_caste_name'),
|
||||
profile.college_name
|
||||
].filter(v => v !== "N/A" && v !== undefined).join(" / ")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-start gap-3">
|
||||
<button
|
||||
// onClick={()=>{
|
||||
// navigate("/chat")
|
||||
// }}
|
||||
className="w-[fit-content] border-2 border-gray-300 text-gray-700 py-2 px-6 rounded-full hover:bg-gray-50 transition-colors flex items-center justify-center gap-2 text-sm">
|
||||
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
|
||||
{/* Message */}
|
||||
</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">
|
||||
<SkipForward className="w-5 h-5" /> Skip
|
||||
</button> */}
|
||||
<button className="w-[fit-content] bg-[#034E08] text-white py-2 px-6 rounded-full hover:bg-[#A70710] transition-colors flex items-center justify-center gap-2 font-semibold text-sm">
|
||||
<button
|
||||
onClick={handleSendInterest}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
@ -227,7 +235,7 @@ const MatrimonyProfile = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Modal with Swiper */}
|
||||
{/* Image Modal */}
|
||||
{isModalOpen && (
|
||||
<div
|
||||
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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
{/* Main Modal Swiper */}
|
||||
<div
|
||||
className="relative bg-gray-900 rounded-lg overflow-hidden"
|
||||
style={{ height: "65vh" }}
|
||||
@ -271,20 +278,22 @@ const MatrimonyProfile = () => {
|
||||
}}
|
||||
className="h-full w-full"
|
||||
>
|
||||
{profile.images.map((img, idx) => (
|
||||
{profileImages.map((img, idx) => (
|
||||
<SwiperSlide key={idx}>
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<img
|
||||
src={img}
|
||||
alt={`${profile.name} ${idx + 1}`}
|
||||
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>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</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">
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
@ -295,24 +304,13 @@ const MatrimonyProfile = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* Top Info Bar */}
|
||||
<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>
|
||||
<p className="text-sm text-gray-600">
|
||||
{profile.id} | {profile.createdBy}
|
||||
{profile.member_id} | Profile created by {personal.profile_for}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{profile.age} • {profile.height} • {profile.caste} • BE
|
||||
• {profile.education} • {profile.location}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Swiper */}
|
||||
<Swiper
|
||||
modules={[Thumbs]}
|
||||
watchSlidesProgress
|
||||
@ -321,25 +319,28 @@ const MatrimonyProfile = () => {
|
||||
slidesPerView={5}
|
||||
className="mb-2"
|
||||
>
|
||||
{profile.images.map((img, idx) => (
|
||||
{profileImages.map((img, 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">
|
||||
<img
|
||||
src={img}
|
||||
alt=""
|
||||
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>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
|
||||
{/* Bottom Action Bar */}
|
||||
<div className="bg-white p-4 rounded-b-lg mt-2 text-center">
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
Like this member?
|
||||
</p>
|
||||
<button className="bg-[#034E08] text-white px-8 py-2 rounded-full hover:bg-orange-700 transition-colors font-semibold">
|
||||
<p className="text-sm text-gray-600 mb-2">Like this member?</p>
|
||||
<button
|
||||
onClick={handleSendInterest}
|
||||
className="bg-[#034E08] text-white px-8 py-2 rounded-full hover:bg-orange-700 transition-colors font-semibold"
|
||||
>
|
||||
Send Interest
|
||||
</button>
|
||||
</div>
|
||||
@ -354,16 +355,8 @@ const MatrimonyProfile = () => {
|
||||
<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="bg-pink-100 p-2 rounded-full">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#A70710]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<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="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="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 className="flex">
|
||||
<span className="text-gray-600 w-40">Height</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 className="flex">
|
||||
<span className="text-gray-600 w-40">Weight</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<span className="ml-3 text-gray-900">97 Kg</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>
|
||||
<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">Religion</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 className="flex">
|
||||
<span className="text-gray-600 w-40">Caste</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 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="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 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="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 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="ml-3 text-gray-900">Don't know</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-600 w-40">Date Of Birth</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<span className="ml-3 text-gray-900"> 23-12-1991</span>
|
||||
{/* <button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Upgrade to view
|
||||
</button> */}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-600 w-40">Star</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<span className="ml-3 text-gray-900">Piscus</span>
|
||||
{/* <button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Upgrade to view
|
||||
</button> */}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-600 w-40">Rassi</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<span className="ml-3 text-gray-900"> Revathy</span>
|
||||
{/* <button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Upgrade to view
|
||||
</button> */}
|
||||
</div>
|
||||
{/* <div className="flex items-center">
|
||||
<span className="text-gray-600 w-40">Horoscope</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<button className="ml-3 text-[#034E08] hover:text-orange-700 flex items-center gap-1 text-xs font-medium">
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Upgrade to view
|
||||
</button>
|
||||
</div> */}
|
||||
<div className="flex">
|
||||
<span className="text-gray-600 w-40">Employment</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<span className="ml-3 text-gray-900">Employed in private</span>
|
||||
<span className="ml-3 text-gray-900">{personal.raasi || safeVal(profile.raasi, 'raasi_name')}</span>
|
||||
</div>
|
||||
<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="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 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="ml-3 text-gray-900">BE</span>
|
||||
<span className="ml-3 text-gray-900">{personal.known_languages || "N/A"}</span>
|
||||
</div>
|
||||
<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="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>
|
||||
@ -548,11 +460,7 @@ const MatrimonyProfile = () => {
|
||||
<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"
|
||||
>
|
||||
<svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
@ -561,104 +469,44 @@ const MatrimonyProfile = () => {
|
||||
|
||||
<div className="p-5 space-y-3 text-sm">
|
||||
<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="ml-3 text-gray-900">
|
||||
Father Passed Away, Mother is a Home Maker
|
||||
</span>
|
||||
<span className="ml-3 text-gray-900">{family.father_name || profile.father_name || "N/A"}</span>
|
||||
</div>
|
||||
<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="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>
|
||||
{/* Contact Information Section */}
|
||||
<div className="my-8">
|
||||
<div className="flex items-center gap-2 p-3 bg-pink-100">
|
||||
<div className="bg-white p-2 rounded-full">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#A70710]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path 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>
|
||||
<div className="flex">
|
||||
<span className="text-gray-600 w-40">Mother Name</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<div className="ml-3 flex items-center gap-2">
|
||||
<svg
|
||||
className="w-3 h-3 text-green-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
|
||||
</svg>
|
||||
<svg
|
||||
className="w-3 h-3 text-red-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900">+91 99••••••••</span>
|
||||
<button className="text-[#034E08] hover:text-orange-700 text-xs font-medium">
|
||||
Upgrade to view
|
||||
</button>
|
||||
<span className="ml-3 text-gray-900">{family.mother_name || profile.mother_name || "N/A"}</span>
|
||||
</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 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 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>
|
||||
|
||||
{/* About Myself Section */}
|
||||
<div className="my-8">
|
||||
<div className="flex items-center gap-2 p-3 bg-pink-100">
|
||||
<div className="bg-white p-2 rounded-full">
|
||||
<svg
|
||||
className="w-5 h-5 text-[#A70710]"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg">About Myself</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-4 text-sm">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">
|
||||
About Sudharshan M
|
||||
</h4>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
I am making this profile for my brother. He completed his
|
||||
bachelor's degree and is now working as a project engineer -
|
||||
non IT. We belong to a middle class, nuclear family with
|
||||
traditional values, currently settled in Chennai.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">
|
||||
What we are looking for
|
||||
</h4>
|
||||
<p className="text-gray-700">
|
||||
Traditional, homely girl with moderate values
|
||||
</p>
|
||||
<div className="flex">
|
||||
<span className="text-gray-600 w-40">Settled</span>
|
||||
<span className="text-gray-400">:</span>
|
||||
<span className="ml-3 text-gray-900">{family.settled || "N/A"}</span>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
@ -666,11 +514,7 @@ const MatrimonyProfile = () => {
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@ -679,56 +523,109 @@ const MatrimonyProfile = () => {
|
||||
|
||||
<div className="p-5 space-y-3 text-sm">
|
||||
<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="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>
|
||||
</div>
|
||||
<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="ml-3 text-gray-900">
|
||||
History, Philosophy / Spiritual
|
||||
</span>
|
||||
<span className="ml-3 text-gray-900">{(personal.age || profile.age || lifestyle.age) ? `${personal.age || profile.age || lifestyle.age} Years` : "N/A"}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-gray-600 w-40">Hobbies</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">
|
||||
Anime, Comedy, Sci-Fi
|
||||
{lifestyle.hobbies && lifestyle.hobbies.length > 0 ? lifestyle.hobbies.join(", ") : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Educational Details Section */}
|
||||
<div className="border border-gray-200 rounded-lg bg-pink-50/30">
|
||||
<div className="flex items-center gap-2 p-3 bg-pink-100">
|
||||
<div className="bg-white p-2 rounded-full">
|
||||
<svg className="w-5 h-5 text-[#A70710]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg">Educational Details</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-3 text-sm">
|
||||
<div className="flex">
|
||||
<span className="text-gray-600 w-40">Sports</span>
|
||||
<span className="text-gray-600 w-40">Highest Qualification</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 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="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 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="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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,24 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Check, X } from 'lucide-react';
|
||||
|
||||
const PartnerPreferences = () => {
|
||||
const PartnerPreferences = ({ data }) => {
|
||||
if (!data) return null;
|
||||
|
||||
const pref = data.preferedDetails;
|
||||
const matchDetails = data.mutual_match.my_preferences_match;
|
||||
const overallMatch = data.mutual_match.overall_match_percentage;
|
||||
|
||||
const basicPreferences = [
|
||||
{ label: "Preferred Bride's Age", value: "22-29 yrs", match: true },
|
||||
{ label: "Preferred Height", value: "5'0\" - 5'5\"", match: false },
|
||||
{ label: "Preferred Marital Status", value: "Never Married", match: true },
|
||||
{ label: "Preferred Mother Tongue", value: "Tamil", match: true },
|
||||
{ label: "Preferred Physical Status", value: "Normal", match: true },
|
||||
{ label: "Preferred Eating Habits", value: "Vegetarian", match: false },
|
||||
{ label: "Preferred Smoking Habits", value: "Doesn't Matter", match: true },
|
||||
{ label: "Preferred Drinking Habits", value: "Doesn't Matter", match: true },
|
||||
{ label: "Preferred Groom's Age", value: pref.preferred_age_range || "Any", match: matchDetails.age },
|
||||
{ label: "Preferred Height", value: (pref.preferred_height_from && pref.preferred_height_to) ? `${pref.preferred_height_from} - ${pref.preferred_height_to} ft` : "Any", match: matchDetails.height },
|
||||
{ label: "Preferred Marital Status", value: pref.preferred_marital_statuses?.join(", ") || "Any", match: matchDetails.marital_status },
|
||||
{ label: "Preferred Mother Tongue", value: pref.preferred_mother_tongues?.join(", ") || "Any", match: matchDetails.mother_tongue },
|
||||
{ label: "Preferred Education", value: pref.preferred_educations?.join(", ") || "Any", match: matchDetails.education },
|
||||
{ label: "Preferred Employee Type", value: pref.preferred_employee_types?.join(", ") || "Any", match: true }, // Not in matchDetails?
|
||||
];
|
||||
|
||||
const religiousPreferences = [
|
||||
{ label: "Preferred Religion", value: "Hindu", match: true },
|
||||
{ label: "Preferred Caste", value: "Brahmin - Iyer", match: false },
|
||||
{ label: "Preferred Subcaste", value: "Any", match: false },
|
||||
{ label: "Preferred Star", value: "Any", match: true },
|
||||
{ label: "Preferred Dosham", value: "No Dosham", match: true },
|
||||
{ label: "Preferred Caste", value: pref.preferred_castes?.join(", ") || "Any", match: matchDetails.caste },
|
||||
{ label: "Preferred Sub-caste", value: pref.preferred_sub_castes?.join(", ") || "Any", match: matchDetails.sub_caste },
|
||||
{ label: "Preferred State", value: pref.preferred_states?.join(", ") || "Any", match: true },
|
||||
{ label: "Preferred City", value: pref.preferred_districts?.join(", ") || "Any", match: true },
|
||||
{ label: "Preferred Occupation", value: pref.preferred_occupations?.join(", ") || "Any", match: matchDetails.occupation },
|
||||
{ label: "Preferred Annual Income", value: pref.preferred_annual_income || "Any", match: matchDetails.annual_income },
|
||||
];
|
||||
|
||||
const PreferenceItem = ({ label, value, match }) => (
|
||||
@ -46,7 +51,7 @@ const PartnerPreferences = () => {
|
||||
<div className="text-center mb-6 ">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-800 mb-2 flex items-center justify-center gap-2">
|
||||
<span className="text-pink-400">✨</span>
|
||||
His Partner Preferences
|
||||
Partner Preferences
|
||||
<span className="text-pink-400">✨</span>
|
||||
</h1>
|
||||
</div>
|
||||
@ -56,38 +61,31 @@ const PartnerPreferences = () => {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=male1"
|
||||
alt="Profile"
|
||||
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100"
|
||||
src={data.my_profile || "https://api.dicebear.com/7.x/avataaars/svg?seed=male1"}
|
||||
alt="Your Profile"
|
||||
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-pink-100 object-cover"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-gray-600 text-sm mb-1">You match</p>
|
||||
<p className="text-gray-600 text-sm mb-1">Overall Match Score</p>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-red-600">
|
||||
14<span className="text-[#034E08]">/20</span>
|
||||
{overallMatch}<span className="text-[#034E08]">%</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">of his preferences</p>
|
||||
<p className="text-xs text-gray-500">of preferences match</p>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=female1"
|
||||
alt="Your Profile"
|
||||
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100"
|
||||
src={data.profile.profile_picture || "https://api.dicebear.com/7.x/avataaars/svg?seed=female1"}
|
||||
alt="Partner Profile"
|
||||
className="w-16 h-16 sm:w-20 sm:h-20 rounded-xl border-4 border-purple-100 object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-2 md:grid-cols-2 mb-8 pt-4'>
|
||||
|
||||
{/* Basic Preferences Section */}
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between mb-4 bg-[#f5fbff] pt-4 pb-4 px-6">
|
||||
<h2 className="text-lg font-bold text-gray-800">Basic Preferences</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">You match</span>
|
||||
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<Check className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 p-6">
|
||||
{basicPreferences.map((pref, index) => (
|
||||
@ -96,17 +94,10 @@ const PartnerPreferences = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Religious Preferences Section */}
|
||||
{/* Other Preferences Section */}
|
||||
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
|
||||
|
||||
<div className="flex items-center justify-between mb-4 bg-[#f5fbff] pt-4 pb-4 px-6">
|
||||
<h2 className="text-lg font-bold text-gray-800">Religious Preferences</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">You match</span>
|
||||
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<Check className="w-4 h-4 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-gray-800">Professional & Location</h2>
|
||||
</div>
|
||||
<div className="space-y-1 p-6">
|
||||
{religiousPreferences.map((pref, index) => (
|
||||
@ -115,6 +106,7 @@ const PartnerPreferences = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
<div className="text-center mt-6 text-sm text-gray-500">
|
||||
<p>Preferences are used to find compatible matches</p>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { updateEducationalDetails } from "../redux/registrationFormSlice";
|
||||
import { updateEducationalDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
@ -9,456 +9,412 @@ import {
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormHelperText,
|
||||
InputAdornment,
|
||||
Box,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useEducationMasters, useEducationList } from "../hooks/useMasters";
|
||||
import { useCityMasters } from "../hooks/useDependentMasters";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
const EducationalDetailsForm = ({
|
||||
onSubmitStep,
|
||||
onSkipStep,
|
||||
errors,
|
||||
onFieldChange,
|
||||
errors: externalErrors,
|
||||
isEditMode,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const data = useSelector((state) => state.registerform.educationalDetails);
|
||||
const inputRef = useRef(null);
|
||||
const [localErrors, setLocalErrors] = useState({});
|
||||
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
|
||||
|
||||
const { data: educationMasters, isLoading: isEducationMastersLoading } =
|
||||
useEducationMasters();
|
||||
const educationListQuery = useEducationList(data.fieldOfStudy);
|
||||
const educationListQuery = useEducationList(data.study_field);
|
||||
const districtQuery = useCityMasters(data.work_state);
|
||||
|
||||
const studyFieldOptions = useMemo(() => {
|
||||
const raw = educationMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.studyFields || raw.study_fields || raw.fieldOfStudy || [];
|
||||
}, [educationMasters]);
|
||||
const studyFieldOptions = educationMasters?.studyFields || [];
|
||||
const qualificationOptions = educationListQuery.data?.education || educationListQuery.data?.data || [];
|
||||
const occupationOptions = educationMasters?.occupation || [];
|
||||
const employeeTypeOptions = educationMasters?.employeeType || [];
|
||||
const countryOptions = educationMasters?.country || [];
|
||||
const stateOptions = educationMasters?.state || [];
|
||||
const districtOptions = districtQuery.data?.districts || districtQuery.data || [];
|
||||
|
||||
const qualificationOptions = useMemo(() => {
|
||||
const raw = educationListQuery.data;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.education || raw.data || [];
|
||||
}, [educationListQuery.data]);
|
||||
|
||||
const occupationOptions = useMemo(() => {
|
||||
const raw = educationMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.occupation || raw.occupations || [];
|
||||
}, [educationMasters]);
|
||||
|
||||
const employeeTypeOptions = useMemo(() => {
|
||||
const raw = educationMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.employeeType || raw.employee_type || [];
|
||||
}, [educationMasters]);
|
||||
|
||||
const annualIncomeOptions = useMemo(() => {
|
||||
const raw = educationMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.annualIncome || raw.annual_income || [];
|
||||
}, [educationMasters]);
|
||||
|
||||
const workLocationOptions = useMemo(() => {
|
||||
const raw = educationMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return [];
|
||||
return raw.workLocation || raw.work_location || raw.workLocations || [];
|
||||
}, [educationMasters]);
|
||||
|
||||
const getOptionLabel = (item, fallback = "") => {
|
||||
if (!item) return fallback;
|
||||
if (typeof item === "string") return item;
|
||||
return (
|
||||
item.study_field_name ||
|
||||
item.education_name ||
|
||||
item.occupation_name ||
|
||||
item.employee_type_name ||
|
||||
item.annual_income_name ||
|
||||
item.work_location_name ||
|
||||
item.name ||
|
||||
fallback
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
const isUnemployed = data.employee_type === 11;
|
||||
const isIndia = Number(data.work_country) === 1;
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
const updates = { [field]: value };
|
||||
const fieldsToClear = [field];
|
||||
if (field === "fieldOfStudy") {
|
||||
updates.qualification = "";
|
||||
fieldsToClear.push("qualification");
|
||||
|
||||
if (field === "study_field") {
|
||||
updates.education = "";
|
||||
}
|
||||
if (field === "work_country") {
|
||||
updates.work_state = "";
|
||||
updates.work_district = "";
|
||||
updates.work_city = "";
|
||||
}
|
||||
if (field === "work_state") {
|
||||
updates.work_district = "";
|
||||
}
|
||||
if (field === "employee_type" && value === 11) {
|
||||
// Clear fields that will be hidden
|
||||
updates.occupation = "";
|
||||
updates.occupation_detail = "";
|
||||
updates.company_name = "";
|
||||
updates.annual_income = "";
|
||||
updates.work_country = "";
|
||||
updates.work_state = "";
|
||||
updates.work_district = "";
|
||||
updates.work_city = "";
|
||||
}
|
||||
|
||||
dispatch(updateEducationalDetails(updates));
|
||||
if (onFieldChange) onFieldChange(fieldsToClear);
|
||||
setLocalErrors((prev) => ({ ...prev, [field]: "" }));
|
||||
|
||||
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 = () => {
|
||||
console.log("Submitting educational details:", data);
|
||||
const freshErrors = validateForm();
|
||||
if (Object.keys(freshErrors).length > 0) {
|
||||
toast.error("Please fill all mandatory fields");
|
||||
scrollToError(freshErrors);
|
||||
return;
|
||||
}
|
||||
onSubmitStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
|
||||
<form noValidate autoComplete="off" style={{ padding: 16 }}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
|
||||
{/* Field of Study */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Field of Study{requiredMark}
|
||||
</label>
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={Boolean(errors.fieldOfStudy)}
|
||||
>
|
||||
<InputLabel id="fieldOfStudy-label">
|
||||
Select Field of Study
|
||||
</InputLabel>
|
||||
{/* 1. Field of Study */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Field of Study{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.study_field)} id="study_field">
|
||||
<InputLabel>Select Field of Study</InputLabel>
|
||||
<Select
|
||||
labelId="fieldOfStudy-label"
|
||||
value={data.study_field}
|
||||
label="Select Field of Study"
|
||||
name="fieldOfStudy"
|
||||
value={data.fieldOfStudy}
|
||||
onChange={(e) => handleChange("fieldOfStudy", e.target.value)}
|
||||
inputRef={inputRef}
|
||||
disabled={isEducationMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => handleChange("study_field", e.target.value)}
|
||||
>
|
||||
{studyFieldOptions.map((field) => (
|
||||
<MenuItem key={field.id ?? field} value={field.id ?? field}>
|
||||
{getOptionLabel(field, "Field of Study")}
|
||||
</MenuItem>
|
||||
{studyFieldOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.study_field_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.fieldOfStudy && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.fieldOfStudy}
|
||||
</p>
|
||||
)}
|
||||
{localErrors.study_field && <FormHelperText>{localErrors.study_field}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* Highest Qualification */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Highest Educational Qualification{requiredMark}
|
||||
</label>
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={Boolean(errors.qualification)}
|
||||
>
|
||||
<InputLabel id="qualification-label">
|
||||
Select Highest Qualification
|
||||
</InputLabel>
|
||||
{/* 2. Highest Qualification */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Highest Qualification{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.education)} id="education" disabled={!data.study_field || educationListQuery.isLoading}>
|
||||
<InputLabel>Select Qualification</InputLabel>
|
||||
<Select
|
||||
labelId="qualification-label"
|
||||
label="Select Highest Qualification"
|
||||
name="qualification"
|
||||
value={data.qualification}
|
||||
onChange={(e) => handleChange("qualification", e.target.value)}
|
||||
disabled={
|
||||
!data.fieldOfStudy || educationListQuery.isLoading
|
||||
}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
value={data.education}
|
||||
label="Select Qualification"
|
||||
onChange={(e) => handleChange("education", e.target.value)}
|
||||
>
|
||||
{qualificationOptions.map((item) => (
|
||||
<MenuItem key={item.id ?? item} value={item.id ?? item}>
|
||||
{getOptionLabel(item, "Qualification")}
|
||||
</MenuItem>
|
||||
{qualificationOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.education_name || opt.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.qualification && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.qualification}
|
||||
</p>
|
||||
)}
|
||||
{localErrors.education && <FormHelperText>{localErrors.education}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* College Name */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
College Name
|
||||
</label>
|
||||
{/* 3. Education in Detail */}
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<label className="text-gray-900 text-[15px]">Education in Detail{requiredMark}</label>
|
||||
<TextField
|
||||
id="education_detail"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="Enter your education details"
|
||||
value={data.education_detail}
|
||||
onChange={(e) => handleChange("education_detail", e.target.value)}
|
||||
error={Boolean(localErrors.education_detail)}
|
||||
helperText={localErrors.education_detail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 4. College Name */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Enter College Name</label>
|
||||
<TextField
|
||||
id="college_name"
|
||||
fullWidth
|
||||
name="collegeName"
|
||||
label="College Name"
|
||||
value={data.collegeName}
|
||||
onChange={(e) => handleChange("collegeName", e.target.value)}
|
||||
error={Boolean(errors.collegeName)}
|
||||
helperText={errors.collegeName}
|
||||
placeholder="Enter College Name"
|
||||
variant="outlined"
|
||||
value={data.college_name}
|
||||
onChange={(e) => handleChange("college_name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Occupation */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Occupation
|
||||
</label>
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={Boolean(errors.occupation)}
|
||||
>
|
||||
<InputLabel id="occupation-label">Select Occupation</InputLabel>
|
||||
{/* 5. Employee Type */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Employee type{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.employee_type)} id="employee_type">
|
||||
<InputLabel>Select Employee Type</InputLabel>
|
||||
<Select
|
||||
labelId="occupation-label"
|
||||
label="Select Occupation"
|
||||
name="occupation"
|
||||
value={data.occupation}
|
||||
onChange={(e) => handleChange("occupation", e.target.value)}
|
||||
disabled={isEducationMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{occupationOptions.map((item) => (
|
||||
<MenuItem key={item.id ?? item} value={item.id ?? item}>
|
||||
{getOptionLabel(item, "Occupation")}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.occupation && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.occupation}
|
||||
</p>
|
||||
)}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* Company / Organization Name */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Company / Organization Name
|
||||
</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"
|
||||
value={data.employee_type}
|
||||
label="Select Employee Type"
|
||||
name="employeeType"
|
||||
value={data.employeeType}
|
||||
onChange={(e) => handleChange("employeeType", e.target.value)}
|
||||
disabled={isEducationMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => handleChange("employee_type", e.target.value)}
|
||||
>
|
||||
{employeeTypeOptions.map((item) => (
|
||||
<MenuItem key={item.id ?? item} value={item.id ?? item}>
|
||||
{getOptionLabel(item, "Employee Type")}
|
||||
</MenuItem>
|
||||
{employeeTypeOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.employee_type_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.employeeType && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.employeeType}
|
||||
</p>
|
||||
)}
|
||||
{localErrors.employee_type && <FormHelperText>{localErrors.employee_type}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* Annual Income */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Annual Income
|
||||
</label>
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={Boolean(errors.income)}
|
||||
>
|
||||
<InputLabel id="income-label">Select Annual Income</InputLabel>
|
||||
{!isUnemployed && (
|
||||
<>
|
||||
{/* 6. Occupation */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Occupation{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.occupation)} id="occupation">
|
||||
<InputLabel>Select Occupation</InputLabel>
|
||||
<Select
|
||||
labelId="income-label"
|
||||
label="Select Annual Income"
|
||||
name="income"
|
||||
value={data.income}
|
||||
onChange={(e) => handleChange("income", e.target.value)}
|
||||
disabled={isEducationMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
value={data.occupation}
|
||||
label="Select Occupation"
|
||||
onChange={(e) => handleChange("occupation", e.target.value)}
|
||||
>
|
||||
{annualIncomeOptions.map((item) => (
|
||||
<MenuItem key={item.id ?? item} value={item.id ?? item}>
|
||||
{getOptionLabel(item, "Annual Income")}
|
||||
</MenuItem>
|
||||
{occupationOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.occupation_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.income && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.income}
|
||||
</p>
|
||||
)}
|
||||
{localErrors.occupation && <FormHelperText>{localErrors.occupation}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* Work Location */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Work Location
|
||||
</label>
|
||||
{workLocationOptions.length > 0 ? (
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={Boolean(errors.workLocation)}
|
||||
>
|
||||
<InputLabel id="workLocation-label">
|
||||
Select Work Location
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="workLocation-label"
|
||||
label="Select Work Location"
|
||||
name="workLocation"
|
||||
value={data.workLocation}
|
||||
onChange={(e) =>
|
||||
handleChange("workLocation", e.target.value)
|
||||
}
|
||||
disabled={isEducationMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{workLocationOptions.map((item) => (
|
||||
<MenuItem key={item.id ?? item} value={item.id ?? item}>
|
||||
{getOptionLabel(item, "Work Location")}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.workLocation && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.workLocation}
|
||||
</p>
|
||||
)}
|
||||
</FormControl>
|
||||
) : (
|
||||
{/* 7. Occupation in Detail */}
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<label className="text-gray-900 text-[15px]">Occupation in Detail{requiredMark}</label>
|
||||
<TextField
|
||||
id="occupation_detail"
|
||||
fullWidth
|
||||
name="workLocation"
|
||||
label="Work Location"
|
||||
value={data.workLocation}
|
||||
onChange={(e) => handleChange("workLocation", e.target.value)}
|
||||
error={Boolean(errors.workLocation)}
|
||||
helperText={errors.workLocation}
|
||||
placeholder="Enter Work Location"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="Enter your occupation details"
|
||||
value={data.occupation_detail}
|
||||
onChange={(e) => handleChange("occupation_detail", e.target.value)}
|
||||
error={Boolean(localErrors.occupation_detail)}
|
||||
helperText={localErrors.occupation_detail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 8. Company / Organization Name */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Company / Organization Name</label>
|
||||
<TextField
|
||||
id="company_name"
|
||||
fullWidth
|
||||
placeholder="Enter Company Name"
|
||||
value={data.company_name}
|
||||
onChange={(e) => handleChange("company_name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 9. Income Currency Type */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Income Currency Type{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.income_currency)} id="income_currency">
|
||||
<InputLabel>Select Currency</InputLabel>
|
||||
<Select
|
||||
value={data.income_currency}
|
||||
label="Select Currency"
|
||||
onChange={(e) => handleChange("income_currency", e.target.value)}
|
||||
>
|
||||
<MenuItem value="INR">INR</MenuItem>
|
||||
<MenuItem value="USD">USD</MenuItem>
|
||||
</Select>
|
||||
{localErrors.income_currency && <FormHelperText>{localErrors.income_currency}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* 10. Annual Income */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Annual Income{requiredMark}</label>
|
||||
<TextField
|
||||
id="annual_income"
|
||||
fullWidth
|
||||
placeholder="Enter Annual Income"
|
||||
value={data.annual_income}
|
||||
onChange={(e) => handleChange("annual_income", e.target.value)}
|
||||
error={Boolean(localErrors.annual_income)}
|
||||
helperText={localErrors.annual_income}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{data.income_currency === "USD" ? "$" : "₹"}
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 11. Country */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">Country{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.work_country)} id="work_country">
|
||||
<InputLabel>Select Country</InputLabel>
|
||||
<Select
|
||||
value={data.work_country}
|
||||
label="Select Country"
|
||||
onChange={(e) => handleChange("work_country", e.target.value)}
|
||||
>
|
||||
{countryOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.work_country && <FormHelperText>{localErrors.work_country}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* 12. City/Town (Only if NOT India) */}
|
||||
{!isIndia && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">City / Town{requiredMark}</label>
|
||||
<TextField
|
||||
id="work_city"
|
||||
fullWidth
|
||||
placeholder="Enter City / Town"
|
||||
value={data.work_city}
|
||||
onChange={(e) => handleChange("work_city", e.target.value)}
|
||||
error={Boolean(localErrors.work_city)}
|
||||
helperText={localErrors.work_city}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 13. State (Only if India) */}
|
||||
{isIndia && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">State{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.work_state)} id="work_state">
|
||||
<InputLabel>Select State</InputLabel>
|
||||
<Select
|
||||
value={data.work_state}
|
||||
label="Select State"
|
||||
onChange={(e) => handleChange("work_state", e.target.value)}
|
||||
>
|
||||
{stateOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.work_state && <FormHelperText>{localErrors.work_state}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 14. City (District) (Only if India) */}
|
||||
{isIndia && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-gray-900 text-[15px]">City{requiredMark}</label>
|
||||
<FormControl fullWidth error={Boolean(localErrors.work_district)} id="work_district" disabled={!data.work_state || districtQuery.isLoading}>
|
||||
<InputLabel>Select City</InputLabel>
|
||||
<Select
|
||||
value={data.work_district}
|
||||
label="Select City"
|
||||
onChange={(e) => handleChange("work_district", e.target.value)}
|
||||
>
|
||||
{districtOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.work_district && <FormHelperText>{localErrors.work_district}</FormHelperText>}
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 15. Address */}
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<label className="text-gray-900 text-[15px]">Address{requiredMark}</label>
|
||||
<TextField
|
||||
id="address"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
placeholder="Enter your address"
|
||||
value={data.address}
|
||||
onChange={(e) => handleChange("address", e.target.value)}
|
||||
error={Boolean(localErrors.address)}
|
||||
helperText={localErrors.address}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
style={{
|
||||
marginTop: "40px",
|
||||
display: "flex",
|
||||
gap: 16,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mt: 5, display: "flex", gap: 2, justifyContent: "center" }}>
|
||||
{onSkipStep && (
|
||||
<Button variant="outlined" onClick={onSkipStep}>
|
||||
<Button variant="outlined" size="large" onClick={onSkipStep} sx={{ minWidth: 120 }}>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
<Button variant="contained" size="large" onClick={handleSubmit} sx={{ minWidth: 120 }}>
|
||||
{onSkipStep ? "Next" : "Update"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Box>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { updateFamilyDetails } from "../redux/registrationFormSlice";
|
||||
import { updateFamilyDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
|
||||
import {
|
||||
Grid,
|
||||
TextField,
|
||||
@ -12,46 +12,55 @@ import {
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { useFamilyMasters } from "../hooks/useMasters";
|
||||
import { useCityMasters } from "../hooks/useDependentMasters";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange }) => {
|
||||
const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, isEditMode }) => {
|
||||
const dispatch = useDispatch();
|
||||
const data = useSelector((state) => state.registerform.familyDetails);
|
||||
const inputRef = useRef(null);
|
||||
const brotherSectionRef = useRef(null);
|
||||
const sisterSectionRef = useRef(null);
|
||||
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
|
||||
|
||||
const { data: familyMasters, isLoading: isFamilyMastersLoading } =
|
||||
useFamilyMasters();
|
||||
const { data: familyMasters, isLoading: isFamilyMastersLoading } = useFamilyMasters();
|
||||
|
||||
// District query for India
|
||||
const districtQuery = useCityMasters(data.familyState);
|
||||
|
||||
const occupationOptions = useMemo(() => {
|
||||
const raw = familyMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.occupation || raw.occupations || [];
|
||||
return raw.occupation || [];
|
||||
}, [familyMasters]);
|
||||
|
||||
const maritalStatusOptions = useMemo(() => {
|
||||
const raw = familyMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
return raw.maritalStatus || raw.marital_status || [];
|
||||
return raw.maritalStatus || [];
|
||||
}, [familyMasters]);
|
||||
|
||||
const familyStatusOptions = useMemo(() => {
|
||||
const raw = familyMasters;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return [];
|
||||
return raw.familyStatus || raw.family_status || [];
|
||||
return raw.familyStatus || [];
|
||||
}, [familyMasters]);
|
||||
|
||||
const countryOptions = useMemo(() => familyMasters?.country || [], [familyMasters]);
|
||||
const stateOptions = useMemo(() => familyMasters?.state || [], [familyMasters]);
|
||||
const districtOptions = useMemo(() => districtQuery.data?.districts || districtQuery.data || [], [districtQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const createSibling = () => ({
|
||||
type: "",
|
||||
name: "",
|
||||
occupation: "",
|
||||
maritalStatus: "",
|
||||
haveChildrens: "",
|
||||
hasChildren: "",
|
||||
details: "",
|
||||
});
|
||||
|
||||
const syncSiblingArray = (arr, count) => {
|
||||
@ -72,20 +81,48 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
|
||||
if (field === "brotherCount") {
|
||||
const count = Number(value) || 0;
|
||||
updates.brotherCount = count;
|
||||
updates.brotherCount = value;
|
||||
updates.brothers = syncSiblingArray(data.brothers, count);
|
||||
fieldsToClear.push("brothers");
|
||||
if (count > 0) {
|
||||
setTimeout(() => {
|
||||
brotherSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
const firstInput = brotherSectionRef.current?.querySelector('.first-sibling-name');
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
if (field === "sisterCount") {
|
||||
const count = Number(value) || 0;
|
||||
updates.sisterCount = count;
|
||||
updates.sisterCount = value;
|
||||
updates.sisters = syncSiblingArray(data.sisters, count);
|
||||
fieldsToClear.push("sisters");
|
||||
if (count > 0) {
|
||||
setTimeout(() => {
|
||||
sisterSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
const firstInput = sisterSectionRef.current?.querySelector('.first-sibling-name');
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
if (field === "familyCountry") {
|
||||
updates.familyState = "";
|
||||
updates.familyDistrict = "";
|
||||
updates.familyCity = "";
|
||||
}
|
||||
|
||||
if (field === "familyState") {
|
||||
updates.familyDistrict = "";
|
||||
}
|
||||
|
||||
dispatch(updateFamilyDetails(updates));
|
||||
if (onFieldChange) onFieldChange(fieldsToClear);
|
||||
|
||||
if (!isEditMode) {
|
||||
dispatch(clearAllStepsFrom(4));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSiblingChange = (type, index, field, value) => {
|
||||
@ -94,14 +131,40 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
list[index] = { ...list[index], [field]: value };
|
||||
dispatch(updateFamilyDetails({ [type]: list }));
|
||||
if (onFieldChange) onFieldChange(type);
|
||||
|
||||
if (!isEditMode) {
|
||||
dispatch(clearAllStepsFrom(4));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log("Submitting family details:", data);
|
||||
const scrollToError = (errorMap) => {
|
||||
const errorFields = Object.keys(errorMap);
|
||||
if (errorFields.length > 0) {
|
||||
const fieldId = errorFields[0];
|
||||
const element = document.getElementById(fieldId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
setTimeout(() => {
|
||||
const focusable = element.querySelector('[role="combobox"]') ||
|
||||
element.querySelector('[role="button"]') ||
|
||||
element.querySelector("input") ||
|
||||
element.querySelector("select") ||
|
||||
element;
|
||||
if (focusable && typeof focusable.focus === "function") {
|
||||
focusable.focus();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
if (e) e.preventDefault();
|
||||
onSubmitStep();
|
||||
};
|
||||
|
||||
const countOptions = Array.from({ length: 11 }, (_, i) => i);
|
||||
const isIndia = Number(data.familyCountry) === 1;
|
||||
|
||||
const renderSiblingCard = (type, index) => {
|
||||
const sibling = (data[type] || [])[index] || createSibling();
|
||||
@ -114,95 +177,88 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
borderRadius: 2,
|
||||
padding: 2,
|
||||
backgroundColor: "#fff",
|
||||
mb: 4
|
||||
}}
|
||||
>
|
||||
<div className="text-gray-900 text-[14px] font-semibold mb-3">
|
||||
{labelPrefix} {index + 1}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel>Type</InputLabel>
|
||||
<Select
|
||||
label="Type"
|
||||
value={sibling.type}
|
||||
onChange={(e) => handleSiblingChange(type, index, "type", e.target.value)}
|
||||
>
|
||||
<MenuItem value=""><em>Select</em></MenuItem>
|
||||
<MenuItem value="Elder">Elder</MenuItem>
|
||||
<MenuItem value="Younger">Younger</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Name"
|
||||
placeholder="Enter Name"
|
||||
value={sibling.name}
|
||||
onChange={(e) =>
|
||||
handleSiblingChange(type, index, "name", e.target.value)
|
||||
}
|
||||
onChange={(e) => handleSiblingChange(type, index, "name", e.target.value)}
|
||||
variant="outlined"
|
||||
inputProps={{ className: index === 0 ? "first-sibling-name" : "" }}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel id={`${type}-${index}-occupation-label`}>
|
||||
Occupation
|
||||
</InputLabel>
|
||||
<InputLabel>Occupation</InputLabel>
|
||||
<Select
|
||||
labelId={`${type}-${index}-occupation-label`}
|
||||
label="Occupation"
|
||||
value={sibling.occupation}
|
||||
onChange={(e) =>
|
||||
handleSiblingChange(type, index, "occupation", e.target.value)
|
||||
}
|
||||
disabled={isFamilyMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => handleSiblingChange(type, index, "occupation", e.target.value)}
|
||||
>
|
||||
<MenuItem value=""><em>Select</em></MenuItem>
|
||||
{occupationOptions.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
{opt}
|
||||
</MenuItem>
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel id={`${type}-${index}-marital-label`}>
|
||||
Marital Status
|
||||
</InputLabel>
|
||||
<InputLabel>Marital Status</InputLabel>
|
||||
<Select
|
||||
labelId={`${type}-${index}-marital-label`}
|
||||
label="Marital Status"
|
||||
value={sibling.maritalStatus}
|
||||
onChange={(e) =>
|
||||
handleSiblingChange(type, index, "maritalStatus", e.target.value)
|
||||
}
|
||||
disabled={isFamilyMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
onChange={(e) => handleSiblingChange(type, index, "maritalStatus", e.target.value)}
|
||||
>
|
||||
<MenuItem value=""><em>Select</em></MenuItem>
|
||||
{maritalStatusOptions.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
{opt}
|
||||
</MenuItem>
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel id={`${type}-${index}-children-label`}>
|
||||
Have Children
|
||||
</InputLabel>
|
||||
<InputLabel>Have Children</InputLabel>
|
||||
<Select
|
||||
labelId={`${type}-${index}-children-label`}
|
||||
label="Have Children"
|
||||
value={sibling.haveChildrens}
|
||||
onChange={(e) =>
|
||||
handleSiblingChange(
|
||||
type,
|
||||
index,
|
||||
"haveChildrens",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
value={sibling.hasChildren}
|
||||
onChange={(e) => handleSiblingChange(type, index, "hasChildren", e.target.value)}
|
||||
>
|
||||
<MenuItem value={1}>Yes</MenuItem>
|
||||
<MenuItem value={0}>No</MenuItem>
|
||||
<MenuItem value=""><em>Select</em></MenuItem>
|
||||
<MenuItem value="Yes">Yes</MenuItem>
|
||||
<MenuItem value="No">No</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
label="Additional Details"
|
||||
value={sibling.details}
|
||||
onChange={(e) => handleSiblingChange(type, index, "details", e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
@ -212,20 +268,18 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
|
||||
<form noValidate autoComplete="off" style={{ padding: 16 }}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4" id="fatherName">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Father Name{requiredMark}
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
inputRef={inputRef}
|
||||
name="fatherName"
|
||||
label="Father Name"
|
||||
placeholder="Enter Father Name"
|
||||
value={data.fatherName}
|
||||
onChange={(e) => handleChange("fatherName", e.target.value)}
|
||||
error={Boolean(errors.fatherName)}
|
||||
helperText={errors.fatherName}
|
||||
placeholder="Enter Father Name"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
@ -236,30 +290,24 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="fatherOccupation"
|
||||
label="Father Occupation"
|
||||
placeholder="Enter Father Occupation"
|
||||
value={data.fatherOccupation}
|
||||
onChange={(e) => handleChange("fatherOccupation", e.target.value)}
|
||||
error={Boolean(errors.fatherOccupation)}
|
||||
helperText={errors.fatherOccupation}
|
||||
placeholder="Enter Father Occupation"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4" id="motherName">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Mother Name{requiredMark}
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="motherName"
|
||||
label="Mother Name"
|
||||
placeholder="Enter Mother Name"
|
||||
value={data.motherName}
|
||||
onChange={(e) => handleChange("motherName", e.target.value)}
|
||||
error={Boolean(errors.motherName)}
|
||||
helperText={errors.motherName}
|
||||
placeholder="Enter Mother Name"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
@ -270,13 +318,9 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="motherOccupation"
|
||||
label="Mother Occupation"
|
||||
placeholder="Enter Mother Occupation"
|
||||
value={data.motherOccupation}
|
||||
onChange={(e) => handleChange("motherOccupation", e.target.value)}
|
||||
error={Boolean(errors.motherOccupation)}
|
||||
helperText={errors.motherOccupation}
|
||||
placeholder="Enter Mother Occupation"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
@ -286,14 +330,13 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
Brother Count
|
||||
</label>
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel id="brotherCount-label">Select Brother Count</InputLabel>
|
||||
<InputLabel>Select Brother Count</InputLabel>
|
||||
<Select
|
||||
labelId="brotherCount-label"
|
||||
label="Select Brother Count"
|
||||
name="brotherCount"
|
||||
value={data.brotherCount}
|
||||
onChange={(e) => handleChange("brotherCount", e.target.value)}
|
||||
>
|
||||
<MenuItem value=""><em>Select</em></MenuItem>
|
||||
{countOptions.map((count) => (
|
||||
<MenuItem key={count} value={count}>
|
||||
{count}
|
||||
@ -303,19 +346,27 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{Number(data.brotherCount) > 0 && (
|
||||
<div className="md:col-span-2 mt-2" ref={brotherSectionRef}>
|
||||
<div className="text-gray-900 text-[16px] font-semibold mb-3">
|
||||
Brother Details
|
||||
</div>
|
||||
{data.brothers.map((_, i) => renderSiblingCard("brothers", i))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Sister Count
|
||||
</label>
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel id="sisterCount-label">Select Sister Count</InputLabel>
|
||||
<InputLabel>Select Sister Count</InputLabel>
|
||||
<Select
|
||||
labelId="sisterCount-label"
|
||||
label="Select Sister Count"
|
||||
name="sisterCount"
|
||||
value={data.sisterCount}
|
||||
onChange={(e) => handleChange("sisterCount", e.target.value)}
|
||||
>
|
||||
<MenuItem value=""><em>Select</em></MenuItem>
|
||||
{countOptions.map((count) => (
|
||||
<MenuItem key={count} value={count}>
|
||||
{count}
|
||||
@ -325,105 +376,183 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange })
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{Number(data.sisterCount) > 0 && (
|
||||
<div className="md:col-span-2 mt-2" ref={sisterSectionRef}>
|
||||
<div className="text-gray-900 text-[16px] font-semibold mb-3">
|
||||
Sister Details
|
||||
</div>
|
||||
{data.sisters.map((_, i) => renderSiblingCard("sisters", i))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4" id="familyStatus">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Family Status{requiredMark}
|
||||
</label>
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
error={Boolean(errors.familyStatus)}
|
||||
>
|
||||
<InputLabel id="familyStatus-label">Select Family Status</InputLabel>
|
||||
<FormControl fullWidth variant="outlined" error={Boolean(errors.familyStatus)}>
|
||||
<InputLabel>Select Family Status</InputLabel>
|
||||
<Select
|
||||
labelId="familyStatus-label"
|
||||
label="Select Family Status"
|
||||
name="familyStatus"
|
||||
value={data.familyStatus}
|
||||
onChange={(e) => handleChange("familyStatus", e.target.value)}
|
||||
disabled={isFamilyMastersLoading}
|
||||
sx={{
|
||||
"& .MuiSelect-select.Mui-disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{familyStatusOptions.map((item) => (
|
||||
<MenuItem key={item.id ?? item} value={item.id ?? item}>
|
||||
{item.family_type_name || item.name || item}
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
{item.family_type_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.familyStatus && (
|
||||
<p
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
margin: "3px 14px 0 14px",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
{errors.familyStatus}
|
||||
</p>
|
||||
<p className="text-[#d32f2f] text-[0.75rem] mt-1 ml-3">{errors.familyStatus}</p>
|
||||
)}
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">Native Place</label>
|
||||
<div className="flex flex-col gap-4" id="nativePlace">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Native Place{requiredMark}
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="nativePlace"
|
||||
label="Native Place"
|
||||
placeholder="Enter Native Place"
|
||||
value={data.nativePlace}
|
||||
onChange={(e) => handleChange("nativePlace", e.target.value)}
|
||||
error={Boolean(errors.nativePlace)}
|
||||
helperText={errors.nativePlace}
|
||||
placeholder="Enter Native Place"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Number(data.brotherCount) > 0 && (
|
||||
<div className="mt-6">
|
||||
<div className="text-gray-900 text-[16px] font-semibold mb-3">
|
||||
Brother Details
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{Array.from({ length: Number(data.brotherCount) }).map(
|
||||
(_, index) => renderSiblingCard("brothers", index)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Number(data.sisterCount) > 0 && (
|
||||
<div className="mt-6">
|
||||
<div className="text-gray-900 text-[16px] font-semibold mb-3">
|
||||
Sister Details
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{Array.from({ length: Number(data.sisterCount) }).map(
|
||||
(_, index) => renderSiblingCard("sisters", index)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sx={{ marginTop: 10, display: "flex", gap: 4, justifyContent: "center" }}
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Country Living
|
||||
</label>
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel>Select Country</InputLabel>
|
||||
<Select
|
||||
label="Select Country"
|
||||
value={data.familyCountry}
|
||||
onChange={(e) => handleChange("familyCountry", e.target.value)}
|
||||
>
|
||||
{countryOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.country_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{isIndia ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Residing State
|
||||
</label>
|
||||
<FormControl fullWidth variant="outlined" disabled={!data.familyCountry}>
|
||||
<InputLabel>Select State</InputLabel>
|
||||
<Select
|
||||
label="Select State"
|
||||
value={data.familyState}
|
||||
onChange={(e) => handleChange("familyState", e.target.value)}
|
||||
>
|
||||
{stateOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.state_name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Residing City
|
||||
</label>
|
||||
<FormControl fullWidth variant="outlined" disabled={!data.familyState || districtQuery.isLoading}>
|
||||
<InputLabel>Select City</InputLabel>
|
||||
<Select
|
||||
label="Select City"
|
||||
value={data.familyDistrict}
|
||||
onChange={(e) => handleChange("familyDistrict", e.target.value)}
|
||||
>
|
||||
{districtOptions.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>{opt.district_name || opt.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</>
|
||||
) : data.familyCountry ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
City / Town
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Enter City / Town"
|
||||
value={data.familyCity}
|
||||
onChange={(e) => handleChange("familyCity", e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-4 md:col-span-2">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Address
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="Enter complete address"
|
||||
value={data.address}
|
||||
onChange={(e) => handleChange("address", e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 md:col-span-2">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Expectations / Requirements Details
|
||||
</label>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
placeholder="Describe your expectations"
|
||||
value={data.expectationDetails}
|
||||
onChange={(e) => handleChange("expectationDetails", e.target.value)}
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="text-gray-900 text-[15px]">
|
||||
Willing to go abroad
|
||||
</label>
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel>Select Option</InputLabel>
|
||||
<Select
|
||||
label="Select Option"
|
||||
value={data.willingToGoAbroad}
|
||||
onChange={(e) => handleChange("willingToGoAbroad", e.target.value)}
|
||||
>
|
||||
<MenuItem value="Yes">Yes</MenuItem>
|
||||
<MenuItem value="No">No</MenuItem>
|
||||
<MenuItem value="Any">Any</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex gap-4 justify-center">
|
||||
{onSkipStep && (
|
||||
<Button variant="outlined" onClick={onSkipStep}>
|
||||
<Button variant="outlined" onClick={onSkipStep} sx={{ minWidth: 120 }}>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit} sx={{ minWidth: 120 }}>
|
||||
{onSkipStep ? "Next" : "Update"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { updateLifestyleDetails } from "../redux/registrationFormSlice";
|
||||
import { updateLifestyleDetails, clearAllStepsFrom } from "../redux/registrationFormSlice";
|
||||
import {
|
||||
Grid,
|
||||
FormControl,
|
||||
@ -30,6 +30,7 @@ const LifestyleDetailsForm = ({
|
||||
onSkipStep,
|
||||
errors,
|
||||
onFieldChange,
|
||||
isEditMode,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const data = useSelector((state) => state.registerform.lifestyleDetails);
|
||||
@ -76,6 +77,10 @@ const LifestyleDetailsForm = ({
|
||||
const handleChange = (field, value) => {
|
||||
dispatch(updateLifestyleDetails({ [field]: value }));
|
||||
if (onFieldChange) onFieldChange(field);
|
||||
|
||||
if (!isEditMode) {
|
||||
dispatch(clearAllStepsFrom(5));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMultiChange = (field, value) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -397,10 +397,10 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
|
||||
{Object.entries(sibling).map(([sKey, sVal]) => {
|
||||
if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null;
|
||||
let displayValue = sVal;
|
||||
if (sKey === 'haveChildrens' || sKey === 'have_childrens') {
|
||||
if (sVal === true || sVal === 1 || sVal === '1') {
|
||||
if (sKey === 'haveChildrens' || sKey === 'have_childrens' || sKey === 'hasChildren' || sKey === 'has_children') {
|
||||
if (sVal === true || sVal === 1 || sVal === '1' || sVal === 'Yes') {
|
||||
displayValue = 'Yes';
|
||||
} else if (sVal === false || sVal === 0 || sVal === '0') {
|
||||
} else if (sVal === false || sVal === 0 || sVal === '0' || sVal === 'No') {
|
||||
displayValue = 'No';
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
updateFamilyDetails,
|
||||
updateLifestyleDetails,
|
||||
updatePartnerPreferences,
|
||||
clearAllStepsFrom,
|
||||
submitForm,
|
||||
} from "../redux/registrationFormSlice";
|
||||
import PersonalDetailsForm from "./PersonalDetailsForm";
|
||||
@ -31,37 +32,47 @@ import { isAuthenticated } from "../utills/auth";
|
||||
import { getPreviewDetails } from "../api/preview.api";
|
||||
const STEP_FIELD_ORDER = {
|
||||
1: [
|
||||
"name",
|
||||
"profile_for",
|
||||
"gender",
|
||||
"mobileNumber",
|
||||
"dob",
|
||||
"height",
|
||||
"weight",
|
||||
"maritalStatus",
|
||||
"religion",
|
||||
"profileFor",
|
||||
"caste",
|
||||
"subCaste",
|
||||
"gothram",
|
||||
"raasi",
|
||||
"star",
|
||||
"name",
|
||||
"mobile",
|
||||
"email",
|
||||
"password",
|
||||
"confirmPassword",
|
||||
"state",
|
||||
"city",
|
||||
"pincode",
|
||||
"marital_status",
|
||||
"height",
|
||||
"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",
|
||||
],
|
||||
2: [
|
||||
"fieldOfStudy",
|
||||
"qualification",
|
||||
"collegeName",
|
||||
"study_field",
|
||||
"education",
|
||||
"education_detail",
|
||||
"college_name",
|
||||
"employee_type",
|
||||
"occupation",
|
||||
"organization",
|
||||
"employeeType",
|
||||
"income",
|
||||
"workLocation",
|
||||
"occupation_detail",
|
||||
"company_name",
|
||||
"income_currency",
|
||||
"annual_income",
|
||||
"work_country",
|
||||
"work_city",
|
||||
"work_state",
|
||||
"work_district",
|
||||
"address",
|
||||
],
|
||||
3: [
|
||||
"fatherName",
|
||||
@ -89,35 +100,29 @@ const STEP_FIELD_ORDER = {
|
||||
|
||||
const STEP1_SERVER_FIELD_MAP = {
|
||||
name: "name",
|
||||
mobile: "mobileNumber",
|
||||
mobile_number: "mobileNumber",
|
||||
mobileNumber: "mobileNumber",
|
||||
phone: "mobileNumber",
|
||||
mobile: "mobile",
|
||||
email: "email",
|
||||
gender: "gender",
|
||||
dob: "dob",
|
||||
height: "height",
|
||||
weight: "weight",
|
||||
marital_status: "maritalStatus",
|
||||
maritalStatus: "maritalStatus",
|
||||
marital_status: "marital_status",
|
||||
religion: "religion",
|
||||
profile_for: "profileFor",
|
||||
profileFor: "profileFor",
|
||||
profile_for: "profile_for",
|
||||
caste: "caste",
|
||||
sub_caste: "subCaste",
|
||||
subCaste: "subCaste",
|
||||
sub_caste: "sub_caste",
|
||||
willing_to_marry: "willing_to_marry",
|
||||
inter_caste_parents: "inter_caste_parents",
|
||||
inter_caste_parents_details: "inter_caste_parents_details",
|
||||
gothram: "gothram",
|
||||
raasi: "raasi",
|
||||
star: "star",
|
||||
state: "state",
|
||||
district: "city",
|
||||
city: "city",
|
||||
pincode: "pincode",
|
||||
do_you_speak_telugu: "do_you_speak_telugu",
|
||||
about_us: "about_us",
|
||||
known_languages: "known_languages",
|
||||
mother_language: "mother_language",
|
||||
complexion: "complexion",
|
||||
physical_status: "physical_status",
|
||||
password: "password",
|
||||
confirm_password: "confirmPassword",
|
||||
confirmPassword: "confirmPassword",
|
||||
profiles: "profiles",
|
||||
profile_images: "profiles",
|
||||
};
|
||||
|
||||
|
||||
@ -171,8 +176,7 @@ const STEP1_SERVER_FIELD_MAP = {
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
const Stepper = ({ currentStep, enabledSteps, completedSteps, onStepClick }) => {
|
||||
|
||||
const Stepper = ({ currentStep, enabledSteps, completedSteps, onStepClick, checkStepValidity }) => {
|
||||
const steps = [
|
||||
{ num: 1, label: "Personal" },
|
||||
{ num: 2, label: "Educational" },
|
||||
@ -183,40 +187,57 @@ const Stepper = ({ currentStep, enabledSteps, completedSteps, onStepClick }) =>
|
||||
];
|
||||
|
||||
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) => {
|
||||
const isEnabled = enabledSteps.includes(step.num);
|
||||
const isCompleted = completedSteps.includes(step.num);
|
||||
const isSkippable = currentStep > 1 && currentStep < 6 && step.num === currentStep + 1;
|
||||
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 (
|
||||
<React.Fragment key={step.num}>
|
||||
<div
|
||||
className={`flex flex-col items-center ${
|
||||
isEnabled ? "cursor-pointer" : "cursor-not-allowed opacity-50"
|
||||
className={`flex flex-col items-center min-w-[70px] ${
|
||||
isEnabled ? "cursor-pointer" : "cursor-not-allowed opacity-60"
|
||||
}`}
|
||||
onClick={() => isEnabled && onStepClick(step.num)}
|
||||
>
|
||||
<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
|
||||
? "bg-green-600 text-white"
|
||||
: currentStep === step.num
|
||||
? "bg-red-600 text-white"
|
||||
: "bg-gray-300 text-gray-600"
|
||||
? "bg-green-600 text-white shadow-md"
|
||||
: isActive
|
||||
? "bg-red-600 text-white ring-4 ring-red-100 shadow-md"
|
||||
: 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>
|
||||
|
||||
<span className="text-xs mt-1">{step.label}</span>
|
||||
<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>
|
||||
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={`flex-1 h-0.5 mx-1 ${
|
||||
completedSteps.includes(step.num)
|
||||
className={`flex-1 h-[2px] mx-1 mb-6 transition-colors duration-500 ${
|
||||
(completedSteps.includes(step.num) || (checkStepValidity && checkStepValidity(step.num)))
|
||||
? "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");
|
||||
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 [isStep1Update, setIsStep1Update] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
@ -362,24 +390,23 @@ const [completedSteps, setCompletedSteps] = useState([]);
|
||||
order[0] ||
|
||||
Object.keys(errorMap).find((key) => key !== "_form");
|
||||
if (!firstKey) return;
|
||||
|
||||
setTimeout(() => {
|
||||
const byAria = document.querySelector(
|
||||
`[aria-labelledby~="${firstKey}-label"]`
|
||||
);
|
||||
if (byAria && typeof byAria.focus === "function") {
|
||||
byAria.focus();
|
||||
return;
|
||||
const element = document.getElementById(firstKey) ||
|
||||
document.querySelector(`[name="${firstKey}"]`) ||
|
||||
document.querySelector(`[aria-labelledby~="${firstKey}-label"]`);
|
||||
|
||||
if (element) {
|
||||
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);
|
||||
if (byId && typeof byId.focus === "function") {
|
||||
byId.focus();
|
||||
}
|
||||
}, 0);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const clearFieldErrors = (fields) => {
|
||||
@ -474,18 +501,27 @@ const [completedSteps, setCompletedSteps] = useState([]);
|
||||
dispatch(
|
||||
updatePersonalDetails({
|
||||
name: pd.name || "",
|
||||
mobileNumber: pd.mobile || "",
|
||||
mobile: pd.mobile || "",
|
||||
email: pd.email || "",
|
||||
gender: pd.gender || "",
|
||||
dob: formattedDob,
|
||||
height: pd.height || "",
|
||||
height: pd.height_id || "",
|
||||
weight: pd.weight || "",
|
||||
maritalStatus: pd.marital_status_id || "",
|
||||
marital_status: pd.marital_status_id || "",
|
||||
religion: pd.religion_id || "",
|
||||
profileFor: pd.profile_for_id || "",
|
||||
profile_for: pd.profile_for_id || "",
|
||||
caste: pd.caste_id || "",
|
||||
subCaste: pd.sub_caste_id || "",
|
||||
gothram: pd.gothram_id || "",
|
||||
sub_caste: pd.sub_caste_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 || "",
|
||||
star: pd.star_id || "",
|
||||
state: pd.state_id || "",
|
||||
@ -518,14 +554,21 @@ useEffect(() => {
|
||||
const ed = educationalData.educational_details;
|
||||
dispatch(
|
||||
updateEducationalDetails({
|
||||
fieldOfStudy: ed.study_field_id || "",
|
||||
qualification: ed.education_id || "",
|
||||
collegeName: ed.college_name || "",
|
||||
study_field: ed.study_field_id || "",
|
||||
education: ed.education_id || "",
|
||||
education_detail: ed.education_detail || "",
|
||||
college_name: ed.college_name || "",
|
||||
employee_type: ed.employee_type_id || "",
|
||||
occupation: ed.occupation_id || "",
|
||||
organization: ed.company_name || "",
|
||||
employeeType: ed.employee_type_id || "",
|
||||
income: ed.annual_income_id || "",
|
||||
workLocation: ed.work_location || "",
|
||||
occupation_detail: ed.occupation_detail || "",
|
||||
company_name: ed.company_name || "",
|
||||
income_currency: ed.income_currency || "INR",
|
||||
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(() => {
|
||||
if (familyData?.status === "success" && 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(
|
||||
updateFamilyDetails({
|
||||
fatherName: fd.father_name || "",
|
||||
@ -572,15 +600,35 @@ useEffect(()=>{
|
||||
motherOccupation: fd.mother_occupation || "",
|
||||
familyStatus: fd.family_status_id || "",
|
||||
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,
|
||||
sisterCount: fd.sister_count || 0,
|
||||
brothers: mappedBrothers,
|
||||
sisters: mappedSisters,
|
||||
brothers: (fd.brothers || []).map((b) => ({
|
||||
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
|
||||
const { data: lifestyleData } = useQuery({
|
||||
@ -668,23 +716,24 @@ useEffect(()=>{
|
||||
if (step === 1) {
|
||||
const required = [
|
||||
"name",
|
||||
"mobileNumber",
|
||||
"mobile",
|
||||
"gender",
|
||||
"dob",
|
||||
"height",
|
||||
"maritalStatus",
|
||||
"profileFor",
|
||||
"marital_status",
|
||||
"profile_for",
|
||||
"caste",
|
||||
"email",
|
||||
"state",
|
||||
"city",
|
||||
"pincode",
|
||||
"mother_language",
|
||||
"complexion",
|
||||
"physical_status",
|
||||
"inter_caste_parents",
|
||||
"do_you_speak_telugu",
|
||||
];
|
||||
|
||||
required.forEach((field) => {
|
||||
if (!personalDetails[field]) {
|
||||
if (!personalDetails[field] && personalDetails[field] !== 0) {
|
||||
const label = field
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^./, (str) => str.toUpperCase());
|
||||
newErrors[field] = `${label} is required`;
|
||||
}
|
||||
@ -702,16 +751,10 @@ useEffect(()=>{
|
||||
newErrors.email = "Invalid email format";
|
||||
}
|
||||
if (
|
||||
personalDetails.mobileNumber &&
|
||||
personalDetails.mobileNumber.length !== 10
|
||||
personalDetails.mobile &&
|
||||
personalDetails.mobile.length !== 10
|
||||
) {
|
||||
newErrors.mobileNumber = "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";
|
||||
newErrors.mobile = "Mobile number must be 10 digits";
|
||||
}
|
||||
if (
|
||||
personalDetails.password &&
|
||||
@ -721,16 +764,24 @@ useEffect(()=>{
|
||||
newErrors.confirmPassword = "Passwords do not match";
|
||||
}
|
||||
} else if (step === 2) {
|
||||
const required = [
|
||||
"qualification",
|
||||
"fieldOfStudy",
|
||||
const isUnemployed = educationalDetails.employee_type === 11;
|
||||
const isIndia = educationalDetails.work_country === 1;
|
||||
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) => {
|
||||
if (!educationalDetails[field]) {
|
||||
const label = field
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^./, (str) => str.toUpperCase());
|
||||
newErrors[field] = `${label} is required`;
|
||||
}
|
||||
@ -740,6 +791,7 @@ useEffect(()=>{
|
||||
"fatherName",
|
||||
"motherName",
|
||||
"familyStatus",
|
||||
"nativePlace",
|
||||
];
|
||||
required.forEach((field) => {
|
||||
if (!familyDetails[field]) {
|
||||
@ -836,24 +888,33 @@ useEffect(()=>{
|
||||
const buildRegisterStep1Payload = async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("name", personalDetails.name);
|
||||
formData.append("mobile", personalDetails.mobileNumber);
|
||||
formData.append("mobile", personalDetails.mobile);
|
||||
formData.append("email", personalDetails.email);
|
||||
formData.append("pincode", personalDetails.pincode);
|
||||
formData.append("gender", personalDetails.gender);
|
||||
formData.append("dob", personalDetails.dob);
|
||||
formData.append("height", personalDetails.height || "");
|
||||
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("profile_for", personalDetails.profileFor || "");
|
||||
formData.append("profile_for", personalDetails.profile_for || "");
|
||||
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("raasi", personalDetails.raasi || "");
|
||||
formData.append("star", personalDetails.star || "");
|
||||
formData.append("state", personalDetails.state);
|
||||
formData.append("district", personalDetails.city);
|
||||
formData.append("do_you_speak_telugu", personalDetails.do_you_speak_telugu);
|
||||
formData.append("about_us", personalDetails.about_us || "");
|
||||
formData.append("mother_language", personalDetails.mother_language || "");
|
||||
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("web_fcm_token", localStorage.getItem("fcm_token") || "");
|
||||
|
||||
if (personalDetails.profiles && Array.isArray(personalDetails.profiles)) {
|
||||
@ -895,14 +956,23 @@ useEffect(()=>{
|
||||
|
||||
const buildRegisterStep2Payload = () => {
|
||||
const formData = new FormData();
|
||||
formData.append("college_name", educationalDetails.collegeName || "");
|
||||
formData.append("study_field", educationalDetails.fieldOfStudy || "");
|
||||
formData.append("education", educationalDetails.qualification || "");
|
||||
formData.append("study_field", educationalDetails.study_field || "");
|
||||
formData.append("education", educationalDetails.education || "");
|
||||
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("company_name", educationalDetails.organization || "");
|
||||
formData.append("employee_type", educationalDetails.employeeType || "");
|
||||
formData.append("annual_income", educationalDetails.income || "");
|
||||
formData.append("work_location", educationalDetails.workLocation || "");
|
||||
formData.append("occupation_detail", educationalDetails.occupation_detail || "");
|
||||
formData.append("company_name", educationalDetails.company_name || "");
|
||||
formData.append("income_currency", educationalDetails.income_currency || "INR");
|
||||
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;
|
||||
};
|
||||
|
||||
@ -916,19 +986,30 @@ useEffect(()=>{
|
||||
formData.append("native_place", familyDetails.nativePlace || "");
|
||||
formData.append("brother_count", familyDetails.brotherCount || 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) => {
|
||||
formData.append(`brothers[${index}][name]`, brother?.name || "");
|
||||
formData.append(`brothers[${index}][occupation]`, brother?.occupation || "");
|
||||
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) => {
|
||||
formData.append(`sisters[${index}][name]`, sister?.name || "");
|
||||
formData.append(`sisters[${index}][occupation]`, sister?.occupation || "");
|
||||
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;
|
||||
@ -1053,6 +1134,12 @@ useEffect(()=>{
|
||||
|
||||
const token = extractAccessToken(res.data || res);
|
||||
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;
|
||||
|
||||
case 2:
|
||||
@ -1144,6 +1231,9 @@ useEffect(()=>{
|
||||
const handleSkip = () => {
|
||||
setErrors({});
|
||||
|
||||
// Remove from completed if skipping (matching Flutter logic)
|
||||
setCompletedSteps((prev) => prev.filter(s => s !== currentStep));
|
||||
|
||||
setCurrentStep((prev) => {
|
||||
const nextStep = Math.min(prev + 1, 6);
|
||||
|
||||
@ -1158,8 +1248,19 @@ useEffect(()=>{
|
||||
};
|
||||
|
||||
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 moving backwards during registration, clear the unsaved data of the step we are leaving
|
||||
if (step < currentStep && !shouldHideStepper) {
|
||||
dispatch(clearAllStepsFrom(currentStep));
|
||||
}
|
||||
|
||||
setCurrentStep(step);
|
||||
setErrors({});
|
||||
window.scrollTo(0, 0);
|
||||
@ -1203,7 +1304,7 @@ useEffect(() => {
|
||||
onSubmitStep={handleStepSubmit}
|
||||
errors={errors}
|
||||
onFieldChange={clearFieldErrors}
|
||||
isStep1Update={isStep1Update}
|
||||
isEditMode={isStep1Update || shouldHideStepper}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
@ -1213,6 +1314,7 @@ useEffect(() => {
|
||||
onSkipStep={shouldHideStepper ? null : handleSkip}
|
||||
errors={errors}
|
||||
onFieldChange={clearFieldErrors}
|
||||
isEditMode={shouldHideStepper}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
@ -1222,6 +1324,7 @@ useEffect(() => {
|
||||
onSkipStep={shouldHideStepper ? null : handleSkip}
|
||||
errors={errors}
|
||||
onFieldChange={clearFieldErrors}
|
||||
isEditMode={shouldHideStepper}
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
@ -1231,6 +1334,7 @@ useEffect(() => {
|
||||
onSkipStep={shouldHideStepper ? null : handleSkip}
|
||||
errors={errors}
|
||||
onFieldChange={clearFieldErrors}
|
||||
isEditMode={shouldHideStepper}
|
||||
/>
|
||||
);
|
||||
case 5:
|
||||
@ -1240,6 +1344,7 @@ useEffect(() => {
|
||||
onSkipStep={shouldHideStepper ? null : handleSkip}
|
||||
errors={errors}
|
||||
onFieldChange={clearFieldErrors}
|
||||
isEditMode={shouldHideStepper}
|
||||
/>
|
||||
);
|
||||
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 titles = {
|
||||
1: "Personal Details",
|
||||
@ -1272,6 +1417,7 @@ useEffect(() => {
|
||||
onStepClick={handleStepClick}
|
||||
enabledSteps={enabledSteps}
|
||||
completedSteps={completedSteps}
|
||||
checkStepValidity={checkStepValidity}
|
||||
/> )}
|
||||
|
||||
|
||||
|
||||
168
src/hooks/useWebSocket.js
Normal file
168
src/hooks/useWebSocket.js
Normal 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 };
|
||||
};
|
||||
@ -1,42 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs, Tab, Box, Chip } from '@mui/material';
|
||||
import { CheckCircle, Phone, ExpandMore } from '@mui/icons-material';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Tabs, Tab, Box, CircularProgress } from '@mui/material';
|
||||
import { CheckCircle, Phone } from '@mui/icons-material';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { getBlockedProfiles, getReportedProfiles, unblockProfile } from '../services/profileActionApi';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
const BlockedProfile = ({ profile }) => (
|
||||
const BlockedProfile = ({ profile, onUnblock }) => (
|
||||
<div className="bg-white border border-1 border-red-100 rounded-lg shadow-sm p-6 mb-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="relative flex-shrink-0">
|
||||
<img
|
||||
src={profile.image}
|
||||
src={profile.photo || 'https://via.placeholder.com/150'}
|
||||
alt={profile.name}
|
||||
className="w-32 h-32 rounded-2xl object-cover border-2 border-[#A70710]"
|
||||
/>
|
||||
<button className="w-8 h-8 flex justify-center items-center absolute bottom-0 right-0 bg-[#A70710] text-white rounded-full shadow-lg">
|
||||
{/* <button className="w-8 h-8 flex justify-center items-center absolute bottom-0 right-0 bg-[#A70710] text-white rounded-full shadow-lg">
|
||||
<Phone className="w-4 h-4" />
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{/* <div className="flex items-center gap-2 mb-1">
|
||||
<CheckCircle className="text-green-500 w-5 h-5" />
|
||||
<span className="text-green-500 font-medium">Verified</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1">{profile.name}</h2>
|
||||
<p className="text-gray-500 text-sm mb-3">{profile.id} | Profile Created by Parent</p>
|
||||
<p className="text-gray-500 text-sm mb-3">{profile.member_id} | Profile Created by Parent</p>
|
||||
|
||||
<div className="space-y-1 text-gray-700">
|
||||
<p className="font-medium">{profile.age} yrs, {profile.height}, {profile.language},</p>
|
||||
<p className="font-medium">{profile.location},</p>
|
||||
<p className="font-medium">{profile.education}, {profile.occupation}, ₹ {profile.income}, {profile.state}, India</p>
|
||||
<p className="font-medium">{profile.age ? `${profile.age} yrs` : ''}{profile.age && profile.height ? ', ' : ''}{profile.height || ''}</p>
|
||||
|
||||
<p className="font-medium">
|
||||
{[profile.district_name, profile.state_name].filter(Boolean).join(', ')}
|
||||
</p>
|
||||
|
||||
<p className="font-medium">
|
||||
{[
|
||||
profile.education,
|
||||
profile.occupation,
|
||||
profile.annual_income_name ? `₹ ${profile.annual_income_name}` : null
|
||||
].filter(Boolean).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center justify-between border-t border-[#A70710] pt-4">
|
||||
<p className="text-gray-600">You have blocked this profile</p>
|
||||
<button className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors">
|
||||
<button
|
||||
onClick={() => onUnblock(profile.id)}
|
||||
className="bg-[#A70710] hover:bg-red-600 text-white px-8 py-2 rounded-full font-medium transition-colors"
|
||||
>
|
||||
UnBlock
|
||||
</button>
|
||||
</div>
|
||||
@ -49,7 +64,7 @@ const ReportedProfile = ({ profile, onViewReason }) => {
|
||||
<div className="flex flex-col sm:flex-row items-start gap-4">
|
||||
<div className='overflow-hidden w-[100%] h-[100%] max-w-50 max-h-45 rounded-lg flex-shrink-0'>
|
||||
<img
|
||||
src={profile.image}
|
||||
src={profile.photo || 'https://via.placeholder.com/150'}
|
||||
alt={profile.name}
|
||||
className="w-full h-full object-cover "
|
||||
/>
|
||||
@ -57,25 +72,39 @@ const ReportedProfile = ({ profile, onViewReason }) => {
|
||||
<div className="flex-1 w-full">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">{profile.name}</h3>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
ID : {profile.id} <span className="text-xs ml-1">Last seen {profile.lastSeen}</span>
|
||||
ID : {profile.member_id} {profile.last_seen_at && <span className="text-xs ml-1">{profile.last_seen_at}</span>}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5 text-sm">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-gray-400">•</span>
|
||||
<span className="text-gray-600">Profile created by Parent</span>
|
||||
{profile.age && (
|
||||
<>
|
||||
<span className="text-gray-400">•</span>
|
||||
<span className="text-gray-600">{profile.age} yrs</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{profile.caste_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400">•</span>
|
||||
<span className="text-gray-600">{profile.caste}</span>
|
||||
<span className="text-gray-600">{profile.caste_name}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{profile.occupation && (
|
||||
<>
|
||||
<span className="text-gray-400">•</span>
|
||||
<span className="text-gray-600">{profile.occupation}</span>
|
||||
</>
|
||||
)}
|
||||
{profile.district_name && (
|
||||
<>
|
||||
<span className="text-gray-400">•</span>
|
||||
<span className="text-gray-600">{profile.location}</span>
|
||||
<span className="text-gray-600">{profile.district_name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -100,20 +129,20 @@ const ReportReasonModal = ({ profile, onClose }) => {
|
||||
<div className="bg-white rounded-lg shadow-2xl max-w-md w-full p-6 animate-slideUp">
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<img
|
||||
src={profile.image}
|
||||
src={profile.photo || 'https://via.placeholder.com/150'}
|
||||
alt={profile.name}
|
||||
className="w-16 h-20 rounded-lg object-cover flex-shrink-0"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">{profile.name}</h3>
|
||||
<p className="text-sm text-gray-500">ID : {profile.id}</p>
|
||||
<p className="text-sm text-gray-500">ID : {profile.member_id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-bold text-gray-900 mb-3">Reason For Report</h4>
|
||||
<p className="text-sm text-gray-600 leading-relaxed bg-gray-50 p-3 rounded-lg mb-4">
|
||||
{profile.reportReason}
|
||||
{profile.reason}
|
||||
</p>
|
||||
</div>
|
||||
<div className='w-full flex justify-center'>
|
||||
@ -135,116 +164,62 @@ const ReportReasonModal = ({ profile, onClose }) => {
|
||||
function BlockedProfileListPage() {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [selectedReport, setSelectedReport] = useState(null);
|
||||
const blockedProfiles = [
|
||||
{
|
||||
id: 'M6075010',
|
||||
name: 'Aravindh Vinayak M',
|
||||
age: 37,
|
||||
height: "5'6\"",
|
||||
language: 'Tamil',
|
||||
location: 'Karuneegar',
|
||||
education: 'BE',
|
||||
occupation: 'Clerk',
|
||||
income: '9 - 10 Lakhs',
|
||||
state: 'Tamil Nadu',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
|
||||
},
|
||||
{
|
||||
id: 'M6075010',
|
||||
name: 'Aravindh Vinayak M',
|
||||
age: 37,
|
||||
height: "5'6\"",
|
||||
language: 'Tamil',
|
||||
location: 'Karuneegar',
|
||||
education: 'BE',
|
||||
occupation: 'Clerk',
|
||||
income: '9 - 10 Lakhs',
|
||||
state: 'Tamil Nadu',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
|
||||
},
|
||||
{
|
||||
id: 'M6075010',
|
||||
name: 'Aravindh Vinayak M',
|
||||
age: 37,
|
||||
height: "5'6\"",
|
||||
language: 'Tamil',
|
||||
location: 'Karuneegar',
|
||||
education: 'BE',
|
||||
occupation: 'Clerk',
|
||||
income: '9 - 10 Lakhs',
|
||||
state: 'Tamil Nadu',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
|
||||
},
|
||||
{
|
||||
id: 'M6075010',
|
||||
name: 'Aravindh Vinayak M',
|
||||
age: 37,
|
||||
height: "5'6\"",
|
||||
language: 'Tamil',
|
||||
location: 'Karuneegar',
|
||||
education: 'BE',
|
||||
occupation: 'Clerk',
|
||||
income: '9 - 10 Lakhs',
|
||||
state: 'Tamil Nadu',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop'
|
||||
}
|
||||
];
|
||||
const [blockedProfiles, setBlockedProfiles] = useState([]);
|
||||
const [reportedProfiles, setReportedProfiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const reportedProfiles = [
|
||||
{
|
||||
id: 'TK52586A',
|
||||
name: 'Pavilash . P',
|
||||
age: 23,
|
||||
lastSeen: 'Nov 25',
|
||||
caste: 'Agamudayar / Arcot / Thuluva vellala',
|
||||
occupation: 'Engineer-non – IT',
|
||||
location: 'Chennai',
|
||||
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
|
||||
showReason: true,
|
||||
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
|
||||
},
|
||||
{
|
||||
id: 'TK52586A',
|
||||
name: 'Pavilash . P',
|
||||
age: 23,
|
||||
lastSeen: 'Nov 25',
|
||||
caste: 'Agamudayar / Arcot / Thuluva vellala',
|
||||
occupation: 'Engineer-non – IT',
|
||||
location: 'Chennai',
|
||||
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
|
||||
showReason: true,
|
||||
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
|
||||
},
|
||||
{
|
||||
id: 'TK52586A',
|
||||
name: 'Pavilash . P',
|
||||
age: 23,
|
||||
lastSeen: 'Nov 25',
|
||||
caste: 'Agamudayar / Arcot / Thuluva vellala',
|
||||
occupation: 'Engineer-non – IT',
|
||||
location: 'Chennai',
|
||||
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
|
||||
showReason: true,
|
||||
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
|
||||
},
|
||||
{
|
||||
id: 'TK52586A',
|
||||
name: 'Pavilash . P',
|
||||
age: 23,
|
||||
lastSeen: 'Nov 25',
|
||||
caste: 'Agamudayar / Arcot / Thuluva vellala',
|
||||
occupation: 'Engineer-non – IT',
|
||||
location: 'Chennai',
|
||||
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=400&fit=crop',
|
||||
showReason: true,
|
||||
reportReason: 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.'
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [blockedRes, reportedRes] = await Promise.all([
|
||||
getBlockedProfiles(),
|
||||
getReportedProfiles()
|
||||
]);
|
||||
|
||||
if (blockedRes.status === "success") {
|
||||
setBlockedProfiles(blockedRes.data);
|
||||
}
|
||||
];
|
||||
if (reportedRes.status === "success") {
|
||||
setReportedProfiles(reportedRes.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
toast.error("Failed to load profiles");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnblock = async (profileId) => {
|
||||
try {
|
||||
const res = await unblockProfile(profileId);
|
||||
if (res.status === "success") {
|
||||
toast.success(res.message || "Profile unblocked successfully");
|
||||
setBlockedProfiles(prev => prev.filter(p => p.id !== profileId));
|
||||
} else {
|
||||
toast.error(res.message || "Failed to unblock profile");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong");
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
|
||||
<CircularProgress color="error" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className=" py-4 md:py-8">
|
||||
<div className="max-w-[1400px] mx-auto">
|
||||
@ -259,14 +234,14 @@ function BlockedProfileListPage() {
|
||||
textTransform: 'none',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
minWidth: 120,
|
||||
minWidth: 150,
|
||||
},
|
||||
'& .Mui-selected': {
|
||||
color: '#fff !important',
|
||||
background:"#A70710"
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
backgroundColor: '#A70710',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
@ -277,28 +252,32 @@ function BlockedProfileListPage() {
|
||||
|
||||
<div className="transition-all duration-300">
|
||||
{activeTab === 0 && (
|
||||
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-2'>
|
||||
{blockedProfiles.map((profile, index) => (
|
||||
<BlockedProfile key={index} profile={profile} />
|
||||
))}
|
||||
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 md:grid-cols-2 gap-4 px-4'>
|
||||
{blockedProfiles.length > 0 ? (
|
||||
blockedProfiles.map((profile, index) => (
|
||||
<BlockedProfile key={profile.id || index} profile={profile} onUnblock={handleUnblock} />
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full text-center py-10 text-gray-500">No blocked profiles found.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-2'>
|
||||
{reportedProfiles.map((profile, index) => (
|
||||
<div className='w-[100%] max-w-[1400px] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-4 px-4'>
|
||||
{reportedProfiles.length > 0 ? (
|
||||
reportedProfiles.map((profile, index) => (
|
||||
<ReportedProfile
|
||||
key={index}
|
||||
key={profile.id || index}
|
||||
profile={profile}
|
||||
onViewReason={setSelectedReport}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full text-center py-10 text-gray-500">No reported profiles found.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
{/* Report Reason Modal */}
|
||||
<ReportReasonModal
|
||||
@ -307,7 +286,7 @@ function BlockedProfileListPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
<style>{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2 } from 'lucide-react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from '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 { 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 queryClient = useQueryClient();
|
||||
const { personalDetails } = useSelector((state) => state.registerform);
|
||||
const [selectedChat, setSelectedChat] = useState(null);
|
||||
const [message, setMessage] = useState('');
|
||||
const [showChatOnMobile, setShowChatOnMobile] = useState(false);
|
||||
@ -11,241 +19,316 @@ const ChatUI = () => {
|
||||
const [showChatMenu, setShowChatMenu] = useState(false);
|
||||
const [openReport, setOpenReport] = useState(false);
|
||||
|
||||
const contacts = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Kalai',
|
||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Kalai',
|
||||
lastMessage: 'Hi bro!n how are you Long time no see',
|
||||
time: '10 Nov 2025, 10 : 23 AM',
|
||||
online: false
|
||||
},
|
||||
{
|
||||
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 [contacts, setContacts] = useState([]);
|
||||
const [chatMessages, setChatMessages] = useState([]);
|
||||
const [chatDetails, setChatDetails] = useState(null);
|
||||
const [loadingContacts, setLoadingContacts] = useState(true);
|
||||
const [loadingMessages, setLoadingMessages] = useState(false);
|
||||
const [sendingMessage, setSendingMessage] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
const messages = {
|
||||
1: [
|
||||
{
|
||||
id: 1,
|
||||
sender: 'other',
|
||||
text: 'Let\'s do it! I\'m in a meeting until noon.',
|
||||
time: '10 Nov',
|
||||
isDate: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: 'me',
|
||||
text: 'That\'s perfect! There\'s a new place on Main St I\'ve been wanting to check out. I hear their hawaiian pizza is awesome!',
|
||||
time: '07:21',
|
||||
isDate: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sender: 'date',
|
||||
text: 'Today',
|
||||
isDate: true
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
sender: 'me',
|
||||
text: 'Can\'s get lunch. How about tomorrow?',
|
||||
time: '09:42',
|
||||
isDate: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
sender: 'other',
|
||||
text: 'Let\'s do it! I\'m in a meeting until noon.',
|
||||
time: '',
|
||||
isDate: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
sender: 'me',
|
||||
text: 'That\'s perfect! There\'s a new place on Main St I\'ve been wanting to check out. I hear their hawaiian pizza is awesome!',
|
||||
time: '',
|
||||
isDate: false
|
||||
}
|
||||
]
|
||||
};
|
||||
const messagesEndRef = useRef(null);
|
||||
const scrollContainerRef = useRef(null);
|
||||
const isPaginating = useRef(false);
|
||||
const previousScrollHeight = useRef(0);
|
||||
const observerRef = useRef(null);
|
||||
const topMarkerRef = useRef(null);
|
||||
|
||||
const callHistory = [
|
||||
{
|
||||
id: 1,
|
||||
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'
|
||||
}
|
||||
];
|
||||
// WebSocket Integration - Listening to BOTH Notifications and the Active Chat
|
||||
const profileId = localStorage.getItem("profile_id") || personalDetails?.id;
|
||||
const userId = localStorage.getItem("user_id") || profileId;
|
||||
|
||||
const [chatMessages, setChatMessages] = useState({
|
||||
1: [
|
||||
{ id: 1, sender: 'other', text: "Let's do it! I'm in a meeting until noon.", time: '10 Nov', isDate: false , read: false },
|
||||
{ id: 2, sender: 'me', text: "That's perfect! There's a new place...", time: '07:21 am', isDate: false , read: false },
|
||||
{ id: 3, sender: 'date', text: 'Today', isDate: true, read: true | false },
|
||||
{ id: 4, sender: 'me', text: "Can's get lunch. How about tomorrow?", time: '09:42 am', isDate: false , read: true },
|
||||
{ id: 5, sender: 'other', text: "Let's do it! I'm in a meeting until noon.", time: '', isDate: false, read: true },
|
||||
{ id: 6, sender: 'me', text: "That's perfect! There's a new place...", time: '', isDate: false, read: true },
|
||||
],
|
||||
// 2,3,... if needed
|
||||
// To show messages INSTANTLY in bubbles, we must listen to the specific chat channel
|
||||
const activeChatChannel = selectedChat ? `chat-${selectedChat}` : null;
|
||||
|
||||
// Flutter uses both dotted and non-dotted formats for channels in some Reverb setups
|
||||
const wsChannels = React.useMemo(() => {
|
||||
const channels = [];
|
||||
|
||||
if (userId && userId !== "null") {
|
||||
channels.push(`user-chat-notification${userId}`);
|
||||
channels.push(`user-chat-notification.${userId}`); // Dotted version
|
||||
channels.push(`user-notification${userId}`);
|
||||
channels.push(`user-notification.${userId}`); // Dotted version
|
||||
}
|
||||
|
||||
if (chatDetails?.web_socket_channel) {
|
||||
channels.push(chatDetails.web_socket_channel);
|
||||
} else if (activeChatChannel) {
|
||||
channels.push(activeChatChannel);
|
||||
}
|
||||
|
||||
return [...new Set(channels.filter(Boolean))];
|
||||
}, [userId, activeChatChannel, chatDetails?.web_socket_channel]);
|
||||
|
||||
console.log("[WS-CHANNELS] Subscribing to:", wsChannels.join(", "));
|
||||
|
||||
const { messages: wsMessages, isConnected } = useWebSocket(wsChannels);
|
||||
const processedMsgCount = useRef(wsMessages.length); // Start from current length to avoid processing history
|
||||
|
||||
// Initial refresh when socket connects
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
console.log("[WS-STATUS] WebSocket connected, syncing initial state...");
|
||||
fetchContacts(searchTerm);
|
||||
if (selectedChat) fetchMessages(selectedChat);
|
||||
}
|
||||
}, [isConnected, selectedChat, searchTerm]);
|
||||
|
||||
const fetchContacts = useCallback(async (search = "", silent = false) => {
|
||||
if (!silent) setLoadingContacts(true);
|
||||
try {
|
||||
console.log(`[API] Fetching contacts list (search: "${search}", silent: ${silent})`);
|
||||
const response = await getChatList(search);
|
||||
if (response.status) {
|
||||
setContacts(response.chatLists);
|
||||
console.log(`[API] Contacts list updated. Total contacts: ${response.chatLists?.length}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[API] Error fetching contacts:", error);
|
||||
toast.error("Failed to load chat list");
|
||||
} finally {
|
||||
setLoadingContacts(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMessages = useCallback(async (chatId, silent = false) => {
|
||||
if (!chatId) return;
|
||||
if (!silent) setLoadingMessages(true);
|
||||
try {
|
||||
const response = await getChatMessages(chatId, 1);
|
||||
console.log("[API-PAGINATION] Initial Page 1 Response:", {
|
||||
status: response.status,
|
||||
current_page: response.messages?.current_page,
|
||||
last_page: response.messages?.last_page,
|
||||
total: response.messages?.total
|
||||
});
|
||||
if (response.status) {
|
||||
setChatMessages(response.messages.data.reverse());
|
||||
setChatDetails(response.messages);
|
||||
setCurrentPage(1);
|
||||
setHasMore(response.messages.current_page < response.messages.last_page);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to load messages");
|
||||
} finally {
|
||||
setLoadingMessages(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadMoreMessages = useCallback(async () => {
|
||||
if (!selectedChat || loadingMore || !hasMore) return;
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!message.trim() || !selectedChat) return;
|
||||
setLoadingMore(true);
|
||||
isPaginating.current = true;
|
||||
const nextPage = currentPage + 1;
|
||||
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
try {
|
||||
if (scrollContainerRef.current) {
|
||||
previousScrollHeight.current = scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
|
||||
setChatMessages(prev => {
|
||||
const prevMsgs = prev[selectedChat] || [];
|
||||
const newMsg = {
|
||||
id: prevMsgs.length ? prevMsgs[prevMsgs.length - 1].id + 1 : 1,
|
||||
sender: 'me',
|
||||
text: message.trim(),
|
||||
time: timeString,
|
||||
isDate: false,
|
||||
const response = await getChatMessages(selectedChat, nextPage);
|
||||
if (response.status) {
|
||||
const olderMessages = response.messages.data.reverse();
|
||||
setChatMessages(prev => [...olderMessages, ...prev]);
|
||||
setChatDetails(response.messages);
|
||||
setCurrentPage(nextPage);
|
||||
setHasMore(response.messages.current_page < response.messages.last_page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading more messages:", error);
|
||||
isPaginating.current = false;
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [selectedChat, currentPage, hasMore, loadingMore]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore && !loadingMore && selectedChat) {
|
||||
console.log("[PAGINATION] Top marker visible, loading more...");
|
||||
loadMoreMessages();
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0, root: scrollContainerRef.current }
|
||||
);
|
||||
|
||||
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 {
|
||||
...prev,
|
||||
[selectedChat]: [...prevMsgs, newMsg],
|
||||
...c,
|
||||
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) => {
|
||||
setSelectedChat(contactId);
|
||||
setShowChatOnMobile(true);
|
||||
@ -265,15 +348,12 @@ const [openReport, setOpenReport] = useState(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
|
||||
|
||||
<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 */}
|
||||
<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'
|
||||
}`}>
|
||||
{/* Header */}
|
||||
@ -311,16 +391,28 @@ const [openReport, setOpenReport] = useState(false);
|
||||
<input
|
||||
type="text"
|
||||
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>
|
||||
|
||||
{/* Contact List */}
|
||||
<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
|
||||
key={contact.id}
|
||||
key={`contact-${contact.id}-${index}`}
|
||||
onClick={() => handleChatSelect(contact.id)}
|
||||
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' : ''
|
||||
@ -328,109 +420,39 @@ const [openReport, setOpenReport] = useState(false);
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={contact.avatar}
|
||||
src={contact.profile || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"}
|
||||
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>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h3 className="font-medium text-gray-900">{contact.name}</h3>
|
||||
<span className="text-xs text-gray-500">{contact.time}</span>
|
||||
<h3 className="font-medium text-gray-900 truncate">{contact.name}</h3>
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">{contact.time}</span>
|
||||
</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 gap-3">
|
||||
<button
|
||||
onClick={handleBackToList}
|
||||
className="md:hidden p-2 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=User"
|
||||
alt="User"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<h2 className="font-semibold">Dalahamanner-Hv</h2>
|
||||
<p className="text-xs text-gray-500">ID: TKS258AA</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="p-2 hover:bg-gray-100 rounded">
|
||||
<Phone className="w-5 h-5 text-blue-600" />
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-100 rounded">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-2 p-4 border-b border-gray-200">
|
||||
<button className="px-4 py-1.5 bg-red-500 text-white rounded-full text-sm font-medium">
|
||||
All
|
||||
</button>
|
||||
<button className="px-4 py-1.5 bg-gray-100 text-gray-700 rounded-full text-sm font-medium hover:bg-gray-200">
|
||||
Incoming Call
|
||||
</button>
|
||||
<button className="px-4 py-1.5 bg-gray-100 text-gray-700 rounded-full text-sm font-medium hover:bg-gray-200">
|
||||
Outgoing
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Call History List */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">Today</h3>
|
||||
{callHistory.map((call) => (
|
||||
<div
|
||||
key={call.id}
|
||||
className="flex items-center gap-3 py-3 border-b border-gray-100"
|
||||
>
|
||||
<img
|
||||
src={call.avatar}
|
||||
alt={call.name}
|
||||
className="w-12 h-12 rounded-full"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{call.name}</h4>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Phone className="w-3 h-3" />
|
||||
<span>{call.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{call.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 truncate mr-2">
|
||||
{contact.latest_message}
|
||||
</p>
|
||||
{contact.unread_message_count > 0 && (
|
||||
<span className="bg-[#034E08] text-white text-[10px] rounded-full w-4 h-4 flex items-center justify-center">
|
||||
{contact.unread_message_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Area */}
|
||||
{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'
|
||||
}`}>
|
||||
{/* Chat Header */}
|
||||
@ -443,24 +465,21 @@ const [openReport, setOpenReport] = useState(false);
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<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"
|
||||
className="w-10 h-10 rounded-full"
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">
|
||||
Priya
|
||||
{chatDetails?.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1 text-xs text-green-500">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Online</span>
|
||||
<div className={`flex items-center gap-1 text-xs ${chatDetails?.lastSeen === 'Online' ? 'text-green-500' : 'text-gray-400'}`}>
|
||||
<div className={`w-2 h-2 rounded-full ${chatDetails?.lastSeen === 'Online' ? 'bg-green-500' : 'bg-gray-300'}`}></div>
|
||||
<span>{chatDetails?.lastSeen || 'Offline'}</span>
|
||||
</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> */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowChatMenu(!showChatMenu)}
|
||||
@ -494,60 +513,69 @@ const [openReport, setOpenReport] = useState(false);
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 md:h-[400px]">
|
||||
{chatMessages[selectedChat]?.map((msg) => (
|
||||
<div key={msg.id}>
|
||||
{msg.isDate ? (
|
||||
<div className="text-center text-xs text-gray-500 my-4">
|
||||
{msg.text}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50"
|
||||
>
|
||||
{/* Top Marker for Intersection Observer */}
|
||||
<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>
|
||||
) : (
|
||||
chatMessages.map((msg, index) => (
|
||||
<div
|
||||
className={`flex ${
|
||||
msg.sender === 'me' ? 'justify-end' : 'justify-start'
|
||||
}`}
|
||||
key={`msg-${msg.id}-${index}`}
|
||||
className={`flex ${msg.chat_by === 'me' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-xs md:max-w-md px-4 py-2 rounded-2xl ${
|
||||
msg.sender === 'me'
|
||||
className={`max-w-[75%] md:max-w-md px-4 py-2 rounded-2xl shadow-sm ${
|
||||
msg.chat_by === 'me'
|
||||
? 'bg-[#cbf5ea] text-gray-900 rounded-br-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'>
|
||||
{msg.time && (
|
||||
<div className="flex items-center justify-end ">
|
||||
<span className={`text-xs ${msg.sender === 'me' ? 'text-gray-900' : 'text-gray-500'}`}>
|
||||
<span className="text-[10px] text-gray-500">
|
||||
{msg.time}
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{msg.sender === 'me' && (
|
||||
<div className="flex items-center justify-end">
|
||||
<span className="text-xs text-blue-100">{msg.time}</span>
|
||||
{msg.read ? (
|
||||
<CheckCheck className="w-4 h-4 text-[#034E08]" />
|
||||
{msg.chat_by === 'me' && (
|
||||
<div className="flex items-center">
|
||||
{msg.is_read === 1 ? (
|
||||
<CheckCheck className="w-3.5 h-3.5 text-blue-500" />
|
||||
) : (
|
||||
<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 ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Message Input */}
|
||||
@ -559,28 +587,53 @@ const [openReport, setOpenReport] = useState(false);
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||
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
|
||||
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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!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>
|
||||
</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>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,19 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from "react-router-dom";
|
||||
import MatrimonyProfile from "../components/profiledetail/MatrimonyProfile"
|
||||
import PartnerPreferences from "../components/profiledetail/PartnerPreferences"
|
||||
import MatchingList from "../components/profiledashboard/MatchingList";
|
||||
import { getProfileDetail } from "../services/profileActionApi";
|
||||
import { CircularProgress, Box } from "@mui/material";
|
||||
|
||||
const ProfileDetailPage = () => {
|
||||
const { id } = useParams();
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="w-[100%] max-w-[1400px] mx-auto my-10">
|
||||
<MatrimonyProfile/>
|
||||
<PartnerPreferences/>
|
||||
<MatchingList/>
|
||||
<MatrimonyProfile data={data} />
|
||||
<PartnerPreferences data={data} />
|
||||
<MatchingList matches={data.all_matches} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileDetailPage
|
||||
export default ProfileDetailPage;
|
||||
@ -74,6 +74,13 @@ const LoginPage = () => {
|
||||
localStorage.setItem("access_token", token);
|
||||
setAccessToken(token);
|
||||
|
||||
// Store profile_id and user_id for WebSocket channels
|
||||
const profileId = data?.profile_id || data?.data?.profile_id;
|
||||
const userId = data?.user_id || data?.data?.user_id;
|
||||
|
||||
if (profileId) localStorage.setItem("profile_id", profileId);
|
||||
if (userId) localStorage.setItem("user_id", userId);
|
||||
|
||||
toast.success("Login Successful!");
|
||||
navigate("/dashboard-home");
|
||||
} else {
|
||||
|
||||
@ -5,49 +5,72 @@ const registrationformSlice = createSlice({
|
||||
initialState: {
|
||||
personalDetails: {
|
||||
name: "",
|
||||
mobileNumber: "",
|
||||
mobile: "",
|
||||
email: "",
|
||||
gender: "",
|
||||
dob: "",
|
||||
height: "",
|
||||
weight: "",
|
||||
maritalStatus: "",
|
||||
religion: "",
|
||||
profileFor: "",
|
||||
caste: "",
|
||||
subCaste: "",
|
||||
marital_status: "",
|
||||
religion: 1, // Default Hindu
|
||||
profile_for: "",
|
||||
caste: 1, // Default Naidu
|
||||
sub_caste: "",
|
||||
willing_to_marry: "",
|
||||
inter_caste_parents: "",
|
||||
inter_caste_parents_details: "",
|
||||
gothram: "",
|
||||
raasi: "",
|
||||
star: "",
|
||||
bloodGroup: "",
|
||||
email: "",
|
||||
do_you_speak_telugu: "",
|
||||
about_us: "",
|
||||
known_languages: [],
|
||||
mother_language: "",
|
||||
complexion: "",
|
||||
physical_status: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
dob: "",
|
||||
raasi: "",
|
||||
star: "",
|
||||
state: "",
|
||||
city: "",
|
||||
pincode: "",
|
||||
profiles: [],
|
||||
verifiedMobileNumber: "",
|
||||
},
|
||||
educationalDetails: {
|
||||
collegeName: "",
|
||||
employeeType: "",
|
||||
qualification: "",
|
||||
fieldOfStudy: "",
|
||||
study_field: "",
|
||||
education: "",
|
||||
education_detail: "",
|
||||
college_name: "",
|
||||
employee_type: "",
|
||||
occupation: "",
|
||||
organization: "",
|
||||
income: "",
|
||||
workLocation: "",
|
||||
occupation_detail: "",
|
||||
company_name: "",
|
||||
income_currency: "INR",
|
||||
annual_income: "",
|
||||
work_country: 1,
|
||||
work_city: "",
|
||||
work_state: "",
|
||||
work_district: "",
|
||||
address: "",
|
||||
},
|
||||
familyDetails: {
|
||||
fatherName: "",
|
||||
fatherOccupation: "",
|
||||
motherName: "",
|
||||
motherOccupation: "",
|
||||
brotherCount: 0,
|
||||
sisterCount: 0,
|
||||
brotherCount: "",
|
||||
sisterCount: "",
|
||||
brothers: [],
|
||||
sisters: [],
|
||||
familyStatus: "",
|
||||
nativePlace: "",
|
||||
familyCountry: "",
|
||||
familyState: "",
|
||||
familyDistrict: "",
|
||||
familyCity: "",
|
||||
address: "",
|
||||
expectationDetails: "",
|
||||
willingToGoAbroad: "",
|
||||
},
|
||||
lifestyleDetails: {
|
||||
diets: [],
|
||||
@ -114,14 +137,38 @@ const registrationformSlice = createSlice({
|
||||
state.partnerPreferences = { ...state.partnerPreferences, ...action.payload };
|
||||
},
|
||||
|
||||
submitForm: (state) => {
|
||||
console.log("Form Submitted:", {
|
||||
personalDetails: state.personalDetails,
|
||||
educationalDetails: state.educationalDetails,
|
||||
familyDetails: state.familyDetails,
|
||||
lifestyleDetails: state.lifestyleDetails,
|
||||
partnerPreferences: state.partnerPreferences,
|
||||
});
|
||||
clearAllStepsFrom: (state, action) => {
|
||||
const step = action.payload;
|
||||
if (step <= 2) {
|
||||
state.educationalDetails = {
|
||||
study_field: "", education: "", education_detail: "", college_name: "",
|
||||
employee_type: "", occupation: "", occupation_detail: "", company_name: "",
|
||||
income_currency: "INR", annual_income: "", work_country: "", work_state: "",
|
||||
work_district: "", work_city: "", address: "",
|
||||
};
|
||||
}
|
||||
if (step <= 3) {
|
||||
state.familyDetails = {
|
||||
fatherName: "", fatherOccupation: "", motherName: "", motherOccupation: "",
|
||||
brotherCount: "", sisterCount: "", brothers: [], sisters: [],
|
||||
familyStatus: "", nativePlace: "", familyCountry: "", familyState: "",
|
||||
familyDistrict: "", familyCity: "", address: "", expectationDetails: "",
|
||||
willingToGoAbroad: "",
|
||||
};
|
||||
}
|
||||
if (step <= 4) {
|
||||
state.lifestyleDetails = {
|
||||
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,
|
||||
updateLifestyleDetails,
|
||||
updatePartnerPreferences,
|
||||
clearAllStepsFrom,
|
||||
submitForm,
|
||||
preloadDummyProfile,
|
||||
} = registrationformSlice.actions;
|
||||
|
||||
34
src/services/chatApi.js
Normal file
34
src/services/chatApi.js
Normal 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;
|
||||
}
|
||||
};
|
||||
65
src/services/profileActionApi.js
Normal file
65
src/services/profileActionApi.js
Normal 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;
|
||||
}
|
||||
};
|
||||
@ -10,7 +10,9 @@ export const shortlistProfile = 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") {
|
||||
throw new Error(response.data.message || "Failed to send interest");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user