Implement Subscription History and fix Interest Status Update flow

This commit is contained in:
MAGESHWARAN 2026-04-28 15:41:54 +05:30
parent 4ba4ce1e1b
commit b3d33aca9c
42 changed files with 3950 additions and 2677 deletions

BIN
dist_new.zip Normal file

Binary file not shown.

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.png" /> <link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> --> <!-- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> -->
@ -14,6 +14,7 @@
/> />
<title>thirukalyanam</title> <title>thirukalyanam</title>
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -7,6 +7,7 @@ export const API_ENDPOINTS = {
SUB_CASTE_MASTER: "get_sub_caste_masters", SUB_CASTE_MASTER: "get_sub_caste_masters",
CITY_MASTER: "get_district_masters", CITY_MASTER: "get_district_masters",
STAR_MASTER: "get_star_masters", STAR_MASTER: "get_star_masters",
PATHAM_MASTER: "get_patham_masters",
MOBILE_SEND_OTP: "send_otp", MOBILE_SEND_OTP: "send_otp",
MOBILE_VERIFY_OTP: "verify_otp", MOBILE_VERIFY_OTP: "verify_otp",
@ -64,8 +65,18 @@ export const API_ENDPOINTS = {
REPORT_PROFILE_LIST: "report_profile_list", REPORT_PROFILE_LIST: "report_profile_list",
PROFILE_DETAIL: "profiles/detail", PROFILE_DETAIL: "profiles/detail",
INTEREST_LIST: "interest_lists", INTEREST_LIST: "interest_lists",
UPDATE_INTEREST_STATUS: "update_interest_status", UPDATE_INTEREST_STATUS: "interest_status_update",
CHAT_LIST: "chat/lists", CHAT_LIST: "chat/lists",
CHAT_MESSAGES: (id) => `chat/${id}/messages`, CHAT_MESSAGES: (id) => `chat/${id}/messages`,
UNREAD_CHAT_COUNT: "chat/un_read_chat_count", UNREAD_CHAT_COUNT: "chat/un_read_chat_count",
INTEREST_SEND: "interest_send",
DAILY_RECOMMENDED_DONT_SHOW: "daily_recomended-dont_show",
VIEW_CONTACT: "profiles/view_contact",
CHAT_CREATE: "chat/create",
SUBSCRIPTION_PLANS: "subscription-plans",
SUBSCRIPTION_PURCHASE_RAZORPAY: "subscription-purchase-razorpay",
SUBSCRIPTION_HISTORY: "subscription-history",
}; };

View File

@ -7,8 +7,9 @@ import { API_ENDPOINTS } from "./apiEndpoints";
* and default headers for JSON communication. * and default headers for JSON communication.
*/ */
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL || baseURL: (import.meta.env.DEV)
"https://www.thirukalyanam.amrithaa.net/backend/api/" , ? "/backend/api/"
: (import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL || "https://www.thirukalyanam.amrithaa.net/backend/api/"),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@ -19,7 +20,9 @@ const axiosInstance = axios.create({
* while sharing the same base URL and authorization mechanism. * while sharing the same base URL and authorization mechanism.
*/ */
const apiForFiles = axios.create({ const apiForFiles = axios.create({
baseURL: import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL, baseURL: (import.meta.env.DEV)
? "/backend/api/"
: (import.meta.env.VITE_THIRUKALYANAM_API_BASE_URL || "https://www.thirukalyanam.amrithaa.net/backend/api/"),
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
@ -123,8 +126,13 @@ export const clearUserData = () => {
export const urlToFile = async (url, filename = "file") => { export const urlToFile = async (url, filename = "file") => {
try { try {
// Rewrite URL to use proxy in development to avoid CORS
const finalUrl = (import.meta.env.DEV && url && url.startsWith('https://www.thirukalyanam.amrithaa.net/backend'))
? url.replace('https://www.thirukalyanam.amrithaa.net/backend', '/backend')
: url;
// Use your axios instance to request the URL as a Blob // Use your axios instance to request the URL as a Blob
const response = await apiForFiles.get(url, { const response = await apiForFiles.get(finalUrl, {
responseType: "blob", responseType: "blob",
}); });

View File

@ -35,6 +35,13 @@ export const getStarMasters = async (raasi_id) => {
return res.data; return res.data;
}; };
export const getPathamMasters = async (star_id) => {
const res = await axiosInstance.get(API_ENDPOINTS.PATHAM_MASTER, {
params: { star_id },
});
return res.data;
};
export const getEducationMasters = async () => { export const getEducationMasters = async () => {
const res = await axiosInstance.get(API_ENDPOINTS.EDUCATION_DETAILS_MASTER); const res = await axiosInstance.get(API_ENDPOINTS.EDUCATION_DETAILS_MASTER);
return res.data; return res.data;

View File

@ -0,0 +1,27 @@
import axiosInstance from "./axiosInstance";
import { API_ENDPOINTS } from "./apiEndpoints";
export const getSubscriptionPlans = async () => {
const response = await axiosInstance.get(API_ENDPOINTS.SUBSCRIPTION_PLANS);
return response.data;
};
export const purchaseSubscription = async (planId, paymentData = null) => {
if (!paymentData) {
// Step 1: Initiate purchase to get order details
const response = await axiosInstance.post(`${API_ENDPOINTS.SUBSCRIPTION_PURCHASE_RAZORPAY}?supscription_plan_id=${planId}`);
return response.data;
} else {
// Step 2: Verify payment
const response = await axiosInstance.post(API_ENDPOINTS.SUBSCRIPTION_PURCHASE_RAZORPAY, {
...paymentData,
supscription_plan_id: planId
});
return response.data;
}
};
export const getSubscriptionHistory = async () => {
const response = await axiosInstance.get(API_ENDPOINTS.SUBSCRIPTION_HISTORY);
return response.data;
};

View File

@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8716 6.28503V4.88837C11.8716 4.51795 11.7245 4.1627 11.4626 3.90077C11.2006 3.63885 10.8454 3.4917 10.475 3.4917H3.49164C3.12122 3.4917 2.76597 3.63885 2.50404 3.90077C2.24212 4.1627 2.09497 4.51795 2.09497 4.88837V9.07837C2.09497 9.44878 2.24212 9.80403 2.50404 10.066C2.76597 10.3279 3.12122 10.475 3.49164 10.475H4.8883M6.28497 13.2684H13.2683C13.6387 13.2684 13.994 13.1212 14.2559 12.8593C14.5178 12.5974 14.665 12.2421 14.665 11.8717V7.6817C14.665 7.31128 14.5178 6.95603 14.2559 6.69411C13.994 6.43218 13.6387 6.28503 13.2683 6.28503H6.28497C5.91455 6.28503 5.5593 6.43218 5.29738 6.69411C5.03545 6.95603 4.8883 7.31128 4.8883 7.6817V11.8717C4.8883 12.2421 5.03545 12.5974 5.29738 12.8593C5.5593 13.1212 5.91455 13.2684 6.28497 13.2684ZM11.1733 9.7767C11.1733 10.1471 11.0262 10.5024 10.7642 10.7643C10.5023 11.0262 10.1471 11.1734 9.77664 11.1734C9.40622 11.1734 9.05097 11.0262 8.78905 10.7643C8.52712 10.5024 8.37997 10.1471 8.37997 9.7767C8.37997 9.40628 8.52712 9.05103 8.78905 8.78911C9.05097 8.52718 9.40622 8.38003 9.77664 8.38003C10.1471 8.38003 10.5023 8.52718 10.7642 8.78911C11.0262 9.05103 11.1733 9.40628 11.1733 9.7767Z" stroke="#AA5806" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 847 B

View File

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

After

Width:  |  Height:  |  Size: 663 B

View File

@ -1,25 +1,415 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { Crown, Bookmark, Receipt, Sparkles, MoonStar, IdCard } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion";
import CakeIcon from "@mui/icons-material/Cake";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { motion } from "framer-motion";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Crown, Bookmark, X, Heart, Eye, Phone, MessageSquare, ChevronLeft } from "lucide-react";
import VisibilityIcon from "@mui/icons-material/Visibility";
import axiosInstance, { apiForFiles } from "../../api/axiosInstance";
import { API_ENDPOINTS } from "../../api/apiEndpoints";
import UpgradeModal from "./UpgradeModal";
import toast from "react-hot-toast";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
import { sendMessage } from "../../services/chatApi";
import { getHeaderDetails } from "../../api/preview.api";
export default function ProfileCardUI({ profile }) { export default function ProfileCardUI({ profile }) {
const [isLiked, setIsLiked] = useState(false); const [isLiked, setIsLiked] = useState(false);
const [isShortlisted, setIsShortlisted] = useState(profile.is_shortlisted === 1);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const [isViewContactModalOpen, setIsViewContactModalOpen] = useState(false);
const [isInterestStatusModalOpen, setIsInterestStatusModalOpen] = useState(false);
const [isInterestRejectedModalOpen, setIsInterestRejectedModalOpen] = useState(false);
const [isContactSuccessModalOpen, setIsContactSuccessModalOpen] = useState(false);
const [isChatConfirmModalOpen, setIsChatConfirmModalOpen] = useState(false);
const [isCreatingChat, setIsCreatingChat] = useState(false);
const [modalTitle, setModalTitle] = useState("");
const [modalMessage, setModalMessage] = useState("");
const [unlockedMobile, setUnlockedMobile] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: headerData } = useQuery({
queryKey: ["headerDetails"],
queryFn: getHeaderDetails,
staleTime: 60000, // Reuse data for 1 minute
});
const isUserPaid = headerData?.myDetails?.is_paid_member === true;
const handleInterest = async (e) => {
e.stopPropagation();
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
try {
await axiosInstance.post(`${API_ENDPOINTS.INTEREST_SEND}?profile_id=${profile.id}`);
setIsLiked(true);
toast.success(`Interest sent to ${profile.name}!`, {
icon: '❤️',
style: { borderRadius: '10px', background: '#333', color: '#fff' }
});
} catch (error) {
toast.error("Failed to send interest.");
}
};
const handleDecline = async (e) => {
e.stopPropagation();
try {
await axiosInstance.post(`${API_ENDPOINTS.UPDATE_INTEREST_STATUS}?profile_id=${profile.id}&status=reject`);
toast.success("Profile declined");
// Optionally hide card or trigger refresh
} catch (error) {
toast.error("Failed to decline profile.");
}
};
const handleCall = (e) => {
e.stopPropagation();
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
const interestStatus = (profile.is_send_interest_status || "").toLowerCase();
if (profile.call_protection === 1) {
if (profile.is_send_interest) {
if (interestStatus === 'pending') {
setModalTitle("Note");
setModalMessage("An interest request has already been sent for this profile. Please wait for their response");
setIsInterestStatusModalOpen(true);
return;
} else if (interestStatus === 'reject' || interestStatus === 'rejected') {
setIsInterestRejectedModalOpen(true);
return;
}
}
}
_handleCallTap();
};
const _handleCallTap = () => {
const currentMobile = unlockedMobile || profile.mobile_number || "";
const mobile = currentMobile.toLowerCase();
if (mobile.includes("upgrade to view")) {
setIsUpgradeModalOpen(true);
} else if (mobile.includes("view contact")) {
setIsViewContactModalOpen(true);
} else {
// It's a real number - Show the success modal with the number
setUnlockedMobile(currentMobile);
setIsContactSuccessModalOpen(true);
}
};
const handleMessage = (e) => {
e.stopPropagation();
// 1. Check if chat already exists
if (profile.chat_id) {
navigate(`/chat/${profile.chat_id}`);
return;
}
// 2. Check membership
if (!isUserPaid) {
setIsUpgradeModalOpen(true);
return;
}
const interestStatus = (profile.is_send_interest_status || "").toLowerCase();
// 3. Check protection & interest status
if (profile.chat_protection === 1 || profile.call_protection === 1) {
if (profile.is_send_interest) {
if (interestStatus === 'pending') {
setModalTitle("Note");
setModalMessage("An interest request has already been sent for this profile. Please wait for their response");
setIsInterestStatusModalOpen(true);
return;
} else if (interestStatus === 'reject' || interestStatus === 'rejected') {
setIsInterestRejectedModalOpen(true);
return;
}
}
}
// 4. Show confirmation dialog
setIsChatConfirmModalOpen(true);
};
const _handleCreateChat = async () => {
if (isCreatingChat) return;
setIsChatConfirmModalOpen(false);
setIsCreatingChat(true);
try {
const response = await axiosInstance.post(API_ENDPOINTS.CHAT_CREATE, { profile_id: profile.id });
if (response.data?.status === true || response.data?.status === 'success') {
const newChatId = response.data?.chat_id;
// Send default initiation message manually
try {
await sendMessage(newChatId, "This profile has initiated a chat with you");
} catch (msgErr) {
console.error("Error sending initial message:", msgErr);
}
toast.success("Chat initiated!");
navigate(`/chat/${newChatId}`);
queryClient.invalidateQueries();
}
else {
toast.error(response.data?.message || "Could not create chat.");
}
} catch (error) {
console.error("Error creating chat", error);
toast.error("Failed to start conversation");
} finally {
setIsCreatingChat(false);
}
};
const _handleViewContact = async () => {
setIsViewContactModalOpen(false);
try {
const formData = new FormData();
formData.append("profile_id", profile.id);
const response = await apiForFiles.post(API_ENDPOINTS.VIEW_CONTACT, formData);
if (response.data?.status === 'success') {
const newMobile = response.data?.mobile_number;
setUnlockedMobile(newMobile);
setIsContactSuccessModalOpen(true);
toast.success("Contact details unlocked!");
// Refresh all queries to update the UI globally
queryClient.invalidateQueries();
} else {
setIsUpgradeModalOpen(true);
}
} catch (error) {
console.error("Error viewing contact", error);
toast.error("Failed to view contact");
}
};
const handleShortlistClick = async (e) => {
e.stopPropagation();
try {
const res = await axiosInstance.post(`${API_ENDPOINTS.SHORTLIST_API}?profile_id=${profile.id}`);
if (res.data?.status === "success") {
setIsShortlisted(!isShortlisted);
toast.success(res.data.message || "Updated shortlist status");
}
} catch (error) {
toast.error("Failed to update shortlist");
}
};
// Map API fields to UI, handling missing values // Map API fields to UI, handling missing values
const imageSrc = profile.photo || profile.image || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"; const imageSrc = profile.photo || profile.image || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
return ( return (
<div <>
onClick={() => navigate(`/profile-details/${profile.id}`)} <UpgradeModal
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200 bg-white cursor-pointer hover:shadow-2xl transition-all duration-300" isOpen={isUpgradeModalOpen}
> onClose={() => setIsUpgradeModalOpen(false)}
<div className="relative"> />
{/* View Contact Confirmation Modal */}
<AnimatePresence>
{isViewContactModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center relative"
>
<button onClick={() => setIsViewContactModalOpen(false)} className="absolute top-6 left-6 text-gray-400 hover:text-gray-600">
<ChevronLeft size={24} />
</button>
<h3 className="text-2xl font-bold text-gray-900 mb-6">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
You need to view this profile's contact details. If you choose to <span className="text-pink-600 font-bold">"Proceed"</span> one count will be deducted from your subscription.
</p>
<div className="flex gap-4">
<button onClick={() => setIsViewContactModalOpen(false)} className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500 hover:bg-gray-50">Cancel</button>
<button onClick={_handleViewContact} className="flex-1 py-4 bg-[#DF1D46] text-white rounded-xl font-bold hover:bg-red-700 shadow-lg shadow-red-200">Proceed</button>
</div>
</motion.div>
</div>
)}
{/* Interest Status Modal (Pending) */}
{isInterestStatusModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<h3 className="text-2xl font-bold text-gray-900 mb-6">{modalTitle}</h3>
<p className="text-gray-600 mb-8 leading-relaxed">{modalMessage}</p>
<button onClick={() => setIsInterestStatusModalOpen(false)} className="w-full py-4 bg-[#DF1D46] text-white rounded-xl font-bold hover:bg-red-700">OK</button>
</motion.div>
</div>
)}
{/* Interest Rejected Modal */}
{isInterestRejectedModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<div className="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-6">
<X size={40} className="text-[#DF1D46]" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
Your previous interest request was declined. You may resend your interest to this profile
</p>
<button onClick={() => setIsInterestRejectedModalOpen(false)} className="w-full py-4 bg-[#DF1D46] text-white rounded-xl font-bold hover:bg-red-700">OK</button>
</motion.div>
</div>
)}
{/* Contact Success Modal */}
{isContactSuccessModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center mx-auto mb-6">
<Phone size={40} className="text-[#0C8626]" />
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">Success!</h3>
<p className="text-gray-500 mb-6">The contact number has been unlocked.</p>
<div className="bg-gray-50 rounded-2xl p-6 mb-8 border border-gray-100 min-h-[80px] flex items-center justify-center">
<span className="text-3xl font-black text-gray-900 tracking-wider">
{unlockedMobile || (!profile.mobile_number?.toLowerCase().includes("view") ? profile.mobile_number : "Fetching...")}
</span>
</div>
<div className="flex gap-4">
<button
onClick={() => setIsContactSuccessModalOpen(false)}
className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500 hover:bg-gray-50"
>
Close
</button>
<button
onClick={() => {
const finalMobile = unlockedMobile || profile.mobile_number;
if (finalMobile) {
const cleanMobile = finalMobile.replace(/[^0-9+]/g, '');
window.location.href = `tel:${cleanMobile}`;
}
}}
className="flex-1 py-4 bg-[#0C8626] text-white rounded-xl font-bold hover:bg-green-700 shadow-lg shadow-green-200 flex items-center justify-center gap-2"
>
<Phone size={18} /> Call Now
</button>
</div>
</motion.div>
</div>
)}
{/* Chat Confirmation Modal */}
{isChatConfirmModalOpen && (() => {
const currentMobile = profile.mobile_number || "";
const mobileLower = currentMobile.toLowerCase();
const isMobileVisible = currentMobile !== "" &&
!mobileLower.includes("upgrade") &&
!mobileLower.includes("view contact");
return (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center"
>
<h3 className="text-2xl font-bold text-gray-900 mb-6">
{isMobileVisible ? "Ready to Chat?" : "Note"}
</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
{isMobileVisible
? `Are you ready to chat with ${currentMobile}?`
: "Starting a conversation will use 1 chat count. Are you ready to proceed?"}
</p>
<div className="flex gap-4">
<button
onClick={() => setIsChatConfirmModalOpen(false)}
className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500 hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={_handleCreateChat}
className={`flex-1 py-4 text-white rounded-xl font-bold transition-all shadow-lg ${isCreatingChat ? "bg-gray-400" : "bg-[#DF1D46] hover:bg-red-700 shadow-red-200"}`}
>
{isCreatingChat ? "Starting..." : "Proceed"}
</button>
</div>
</motion.div>
</div>
);
})()}
</AnimatePresence>
<div
onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200 bg-white cursor-pointer hover:shadow-2xl transition-all duration-300"
>
<div className="relative">
{/* Premium Badge */} {/* Premium Badge */}
{profile.isPremium && ( {profile.isPremium && (
<motion.div <motion.div
@ -35,16 +425,16 @@ export default function ProfileCardUI({ profile }) {
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
onClick={(e) => { onClick={handleShortlistClick}
e.stopPropagation();
// Shortlist logic here
}}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors" className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
> >
<Bookmark className="w-4 h-4 text-gray-600" /> <Bookmark className={`w-4 h-4 transition-colors ${isShortlisted ? "text-black fill-black" : "text-gray-600"}`} />
<span className="text-[12px] font-medium text-gray-700">Shortlist</span> <span className="text-[12px] font-medium text-gray-700">
{isShortlisted ? "Shortlisted" : "Shortlist"}
</span>
</motion.button> </motion.button>
<div className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"> <div className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]">
<img <img
src={imageSrc} src={imageSrc}
@ -66,77 +456,129 @@ export default function ProfileCardUI({ profile }) {
</div> </div>
</div> </div>
<div className="px-4 pt-2 pb-4 flex flex-col gap-3 bg-white"> <div className="px-5 py-5 -mt-8 bg-white rounded-t-[32px] relative z-[2] shadow-[0_-12px_40px_rgba(0,0,0,0.1)] min-h-[280px] flex flex-col">
<div className="flex items-center gap-2 text-gray-600">
<VisibilityIcon sx={{ fontSize: 18 }} /> <div className="text-left mb-4">
<span className="text-[13px] font-medium">Last seen: {profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"}</span>
</div>
<div className="grid grid-cols-2 gap-y-2 gap-x-4"> <h2 className="text-[20px] font-bold text-gray-900 leading-tight">
<div className="flex items-center gap-2"> {profile.name}
<CakeIcon sx={{ fontSize: 18, color: "#374151" }} /> </h2>
<span className="text-[14px] font-semibold text-gray-900">{profile.age ? `${profile.age} yrs` : "-"}</span> <div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
</div> <span>ID: {profile.member_id || profile.id}</span>
<div className="flex items-center gap-2"> <span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<AccessibilityNewIcon sx={{ fontSize: 18, color: "#374151" }} /> <span className="flex items-center gap-1">
<span className="text-[14px] font-semibold text-gray-900">{profile.height ? `${profile.height} cm` : "-"}</span> <VisibilityIcon sx={{ fontSize: 14 }} />
</div> {profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"}
<div className="flex items-center gap-2 col-span-2"> </span>
<Receipt className="w-4 h-4 text-gray-700" />
<span className="text-[14px] font-semibold text-gray-900 truncate">{profile.annual_income_name || "N/A"}</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4 text-gray-700"> <div className="space-y-3 mt-2 flex-1">
<div className="flex items-center gap-1.5" title="Raasi"> {(profile.age || profile.height) && (
<MoonStar className="w-4 h-4" /> <div className="flex items-center gap-3">
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.raasi_name || "-"}</span> <div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
</div> <img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
<div className="flex items-center gap-1.5" title="Star"> </div>
<Sparkles className="w-4 h-4" /> <span className="text-[13px] font-bold text-gray-700">
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.star_name || "-"}</span> {profile.age ? `${profile.age} yrs` : ""}
</div> {profile.age && profile.height ? ", " : ""}
<div className="flex items-center gap-1.5" title="Caste"> {profile.height ? `${profile.height} cm` : ""}
<IdCard className="w-4 h-4" /> </span>
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.caste_name || "-"}</span> </div>
</div> )}
{(profile.religion_name || profile.caste_name) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-pink-50 flex items-center justify-center flex-shrink-0">
<img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{profile.religion_name}
{profile.religion_name && profile.caste_name ? " / " : ""}
{profile.caste_name}
</span>
</div>
)}
{(profile.annual_income_name || profile.income) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-green-50 flex items-center justify-center flex-shrink-0">
<img src={cashIcon} alt="Cash" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{profile.annual_income_name || profile.income}
</span>
</div>
)}
{(profile.district_name || profile.location) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0">
<img src={locationIcon} alt="Location" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700 truncate">
{profile.district_name || profile.location}
</span>
</div>
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex gap-3 mt-6">
<LocationOnIcon sx={{ fontSize: 18, color: "#DC2626" }} /> {profile.is_send_interest_received && profile.statusReceived?.toLowerCase() === 'pending' ? (
<span className="text-[14px] font-semibold text-gray-900 truncate"> <>
{profile.district_name || profile.location || "-"} <button
{profile.state_name ? `, ${profile.state_name}` : ""} onClick={handleInterest}
</span> className="flex-1 flex items-center justify-center gap-2 py-3 bg-[#0C8626] text-white rounded-2xl font-bold text-[13px] hover:bg-green-700 transition-all active:scale-[0.98]"
>
<Heart size={16} fill="white" /> Accept
</button>
<button
onClick={handleDecline}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-white border border-red-200 text-red-600 rounded-2xl font-bold text-[13px] hover:bg-red-50 transition-all active:scale-[0.98]"
>
<X size={16} /> Reject
</button>
</>
) : (!(profile.is_send_interest || isLiked) && !profile.is_send_interest_received) ? (
<>
<button
onClick={handleInterest}
className={`flex-[1.2] flex items-center justify-center gap-2 py-3 rounded-2xl font-bold text-[13px] transition-all active:scale-[0.98] shadow-lg shadow-green-900/10 ${isLiked ? "bg-green-100 text-green-700 border border-green-200" : "bg-[#034E08] text-white hover:bg-green-800"}`}
>
<Heart size={16} fill={isLiked ? "#034E08" : "white"} />
{isLiked ? "Sent" : "Interest"}
</button>
<button
onClick={(e) => { e.stopPropagation(); navigate(`/profile-details/${profile.id}`); }}
className="flex-1 py-3 bg-gray-50 border border-gray-100 text-gray-600 rounded-2xl font-bold text-[13px] flex items-center justify-center gap-2 hover:bg-gray-100 transition-all active:scale-[0.98]"
>
<Eye size={16} /> Detail
</button>
</>
) : (
<>
<button
onClick={handleCall}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-white border border-green-200 text-green-700 rounded-2xl font-bold text-[13px] hover:bg-green-50 transition-all active:scale-[0.98]"
>
<Phone size={16} /> Call
</button>
<button
onClick={handleMessage}
className="flex-1 flex items-center justify-center gap-2 py-3 bg-white border border-red-200 text-red-600 rounded-2xl font-bold text-[13px] hover:bg-red-50 transition-all active:scale-[0.98]"
>
<MessageSquare size={16} /> Message
</button>
</>
)}
</div> </div>
<div className="flex gap-3 mt-2">
<button
onClick={(e) => { e.stopPropagation(); }}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-red-50 border border-red-200 text-red-700 rounded-full font-medium text-sm hover:bg-red-100 transition-colors active:scale-95"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round"/></svg>
Decline
</button>
<button
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-full font-medium text-sm border transition-colors active:scale-95 ${isLiked ? "bg-green-100 border-green-300 text-green-700" : "bg-green-50 border-green-200 text-green-700 hover:bg-green-100"}`}
onClick={(e) =>{ e.stopPropagation(); setIsLiked(!isLiked); }}
>
{isLiked ? (
<>
<svg className="w-4 h-4 text-red-500 fill-current" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
Sent
</>
) : (
<>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/></svg>
Interest
</>
)}
</button>
</div>
</div> </div>
</div> </div>
); </>
} );
}

View File

@ -0,0 +1,75 @@
import { useState, useEffect } from 'react';
const TinderCard = ({ children, onSwipe, onCardLeftScreen, preventSwipe, className }) => {
const [pos, setPos] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [gone, setGone] = useState(false);
useEffect(() => {
setPos({ x: 0, y: 0 });
setDragging(false);
setGone(false);
}, []);
const handleStart = (clientX, clientY) => {
if (gone) return;
setDragging(true);
setStartPos({ x: clientX - pos.x, y: clientY - pos.y });
};
const handleMove = (clientX, clientY) => {
if (!dragging || gone) return;
setPos({ x: clientX - startPos.x, y: clientY - startPos.y });
};
const handleEnd = () => {
if (gone) return;
setDragging(false);
if (Math.abs(pos.x) > 120) {
const dir = pos.x > 0 ? 'right' : 'left';
if (onSwipe) onSwipe(dir);
setGone(true);
setTimeout(() => onCardLeftScreen && onCardLeftScreen(dir), 300);
} else {
setPos({ x: 0, y: 0 });
}
};
if (gone) return null;
const rotation = pos.x / 20;
const opacity = Math.min(Math.abs(pos.x) / 100, 1);
return (
<div
className={className}
style={{
transform: `translate(${pos.x}px, ${pos.y}px) rotate(${rotation}deg)`,
transition: dragging ? 'none' : 'transform 0.3s ease-out',
touchAction: 'none'
}}
onMouseDown={(e) => handleStart(e.clientX, e.clientY)}
onMouseMove={(e) => handleMove(e.clientX, e.clientY)}
onMouseUp={handleEnd}
onMouseLeave={() => dragging && handleEnd()}
onTouchStart={(e) => handleStart(e.touches[0].clientX, e.touches[0].clientY)}
onTouchMove={(e) => handleMove(e.touches[0].clientX, e.touches[0].clientY)}
onTouchEnd={handleEnd}
>
{pos.x > 50 && (
<div className="absolute top-8 left-8 z-10 border-4 border-green-500 text-green-500 px-4 py-2 rounded-lg text-2xl font-bold rotate-[-20deg]" style={{ opacity }}>
LIKE
</div>
)}
{pos.x < -50 && (
<div className="absolute top-8 right-8 z-10 border-4 border-red-500 text-red-500 px-4 py-2 rounded-lg text-2xl font-bold rotate-[20deg]" style={{ opacity }}>
NOPE
</div>
)}
{children}
</div>
);
};
export default TinderCard;

View File

@ -0,0 +1,62 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
const UpgradeModal = ({ isOpen, onClose }) => {
const navigate = useNavigate();
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
/>
{/* Modal Content */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative bg-white rounded-[32px] shadow-2xl w-full max-w-sm overflow-hidden p-10 flex flex-col items-center text-center"
>
<h4 className="text-[22px] font-bold text-gray-900 mb-6">Note</h4>
<h3 className="text-[20px] font-bold text-gray-900 mb-4 leading-tight">
Membership Upgrade Required
</h3>
<p className="text-[#888] text-[15px] leading-relaxed mb-10 px-2 font-medium">
You are currently a Free Member. Upgrade to a Premium Plan to start sending interests and connecting with your perfect match.
</p>
<div className="flex w-full gap-4">
<button
onClick={onClose}
className="flex-1 py-4 border border-gray-200 text-gray-500 rounded-2xl font-bold text-[16px] hover:bg-gray-50 transition-all active:scale-[0.98]"
>
Later
</button>
<button
onClick={() => {
navigate('/subscription-plan');
onClose();
}}
className="flex-1 py-4 bg-[#e91e4a] text-white rounded-2xl font-bold text-[16px] shadow-lg shadow-red-200 hover:bg-[#d81b45] transition-all active:scale-[0.98]"
>
Upgrade Now
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
};
export default UpgradeModal;

View File

@ -92,68 +92,69 @@ useEffect(() => {
{ {
id: "all_matches", id: "all_matches",
icon: <PersonIcon className="w-6 h-6" />, icon: <PersonIcon className="w-6 h-6" />,
title: "Your Matches", title: "All Matches",
description: "View all the profiles that match your preferences", description: "Profiles matching your preferences",
category: "All Matches", category: "Primary Matches",
},
{
id: "newly_joined",
icon: <RockingChair className="w-6 h-6" />,
title: "Newly Joined",
description: "Joined within the last 30 days",
category: "Primary Matches",
}, },
{ {
id: "shorlisted_by_you", id: "shorlisted_by_you",
icon: <StarIcon className="w-6 h-6" />, icon: <StarIcon className="w-6 h-6" />,
title: "Shortlisted by you", title: "Shortlisted by you",
description: "Matches you have shortlisted", description: "Matches you have shortlisted",
category: "Based on activity", category: "Your Activity",
}, },
{ {
id: "viewed_you", id: "viewed_you",
icon: <VisibilityIcon className="w-6 h-6" />, icon: <VisibilityIcon className="w-6 h-6" />,
title: "Viewed you", title: "Viewed you",
description: "Matches who have viewed your profile", description: "Profiles who viewed you",
category: "Based on activity", category: "Your Activity",
}, },
{ {
id: "shorlisted_you", id: "shorlisted_you",
icon: <PersonAddIcon className="w-6 h-6" />, icon: <PersonAddIcon className="w-6 h-6" />,
title: "Shortlisted you", title: "Shortlisted you",
description: "Matches who have shortlisted your profile", description: "Profiles who shortlisted you",
category: "Based on activity", category: "Your Activity",
}, },
{ {
id: "viewed_by_you", id: "viewed_by_you",
icon: <VisibilityIcon className="w-6 h-6" />, icon: <VisibilityIcon className="w-6 h-6" />,
title: "Viewed by you", title: "Viewed by you",
description: "Matches you have viewed", description: "Profiles you have viewed",
category: "Based on activity", category: "Your Activity",
}, },
{ {
id: "newly_joined",
icon: <RockingChair className="w-6 h-6" />,
title: "Newly Joined",
description: "Matches who Joined within the last 30 days",
category: "Based on activity",
},
{
id: "location_matches", id: "location_matches",
icon: <LocateFixed className="w-6 h-6" />, icon: <LocateFixed className="w-6 h-6" />,
title: "Location matches", title: "Location matches",
description: "Matches near your location", description: "Matches near your location",
category: "Based on activity", category: "Smart Matches",
}, },
{ {
id: "education_matches", id: "education_matches",
icon: <School className="w-6 h-6" />, icon: <School className="w-6 h-6" />,
title: "Education matches", title: "Education matches",
description: "Matches near your education match", description: "Profiles with similar education",
category: "Based on activity", category: "Smart Matches",
}, },
{ {
id: "job_matches", id: "job_matches",
icon: <WorkflowIcon className="w-6 h-6" />, icon: <WorkflowIcon className="w-6 h-6" />,
title: "Job matches", title: "Job matches",
description: "Matches near your job", description: "Profiles with similar profession",
category: "Based on activity", category: "Smart Matches",
}, },
]; ];
let currentCategory = ""; let currentCategory = "";
return ( return (
@ -168,10 +169,11 @@ useEffect(() => {
<div className="w-full md:w-80"> <div className="w-full md:w-80">
<div <div
className="relative rounded-[10px] border border-gray-200 bg-white my-6 className="relative rounded-[15px] border border-gray-200 bg-white my-6
shadow-lg h-[400px] md:h-[600px] overflow-y-auto md:sticky md:top-[150px]" shadow-xl h-[450px] md:h-[calc(100vh-200px)] overflow-y-auto md:sticky md:top-[120px] custom-sidebar-scrollbar"
> >
<div className="py-2 px-4 sticky top-0 bg-[#fff] "> <div className="py-2 px-4 sticky top-0 bg-[#fff] ">
<h2 className="text-xl font-bold text-green-900 mb-4 mt-6 first:mt-0"> <h2 className="text-xl font-bold text-green-900 mb-4 mt-6 first:mt-0">
Filter Matches </h2> Filter Matches </h2>
@ -188,14 +190,17 @@ useEffect(() => {
return ( return (
<div key={tab.id}> <div key={tab.id}>
{showCategory && ( {showCategory && (
<h2 className="text-xl font-bold text-gray-900 mb-4 mt-6 first:mt-0"> <h2 className="text-xs font-bold text-green-700 uppercase tracking-widest mb-3 mt-6 first:mt-0 px-2 opacity-80">
{tab.category} {tab.category}
</h2> </h2>
)} )}
<div <div
onClick={() => { onClick={() => {
dispatch(updateFilter({ filter_type: tab.id })); const finalFilterType = tab.id === "all_matches" ? "" : tab.id;
dispatch(updateFilter({ filter_type: finalFilterType }));
}} }}
className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${ className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${
selectedTab === tab.id selectedTab === tab.id
? "bg-green-50 border-l-4 border-green-600" ? "bg-green-50 border-l-4 border-green-600"
@ -331,10 +336,22 @@ useEffect(() => {
<style>{`
.custom-sidebar-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-sidebar-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-sidebar-scrollbar::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 10px;
}
.custom-sidebar-scrollbar::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}
`}</style>
</> </>
); );
} }

View File

@ -1,126 +1,202 @@
import { useRef, useState } from 'react'; import { useRef, useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Swiper, SwiperSlide } from 'swiper/react'; import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules'; import { Navigation, Pagination, Autoplay, EffectCoverflow } from 'swiper/modules';
import { Crown, Bookmark, User, Briefcase, MapPin, X, Send, ChevronLeft, ChevronRight } from 'lucide-react'; import { Crown, Bookmark, X, ChevronLeft, ChevronRight, RotateCcw, Heart, Timer } from 'lucide-react';
import CakeIcon from "@mui/icons-material/Cake"; import { useNavigate } from 'react-router-dom';
import HeightIcon from "@mui/icons-material/Height"; import { toast } from 'react-hot-toast';
import GroupsIcon from "@mui/icons-material/Groups"; import axiosInstance from "../../api/axiosInstance";
import TempleHinduIcon from "@mui/icons-material/TempleHindu"; import { API_ENDPOINTS } from "../../api/apiEndpoints";
import UpgradeModal from '../common/UpgradeModal';
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import SchoolIcon from "@mui/icons-material/School"; import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
// Import Swiper styles // Import Swiper styles
import 'swiper/css'; import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import 'swiper/css/pagination'; import 'swiper/css/pagination';
import 'swiper/css/effect-coverflow'; import 'swiper/css/effect-coverflow';
import { useNavigate } from 'react-router-dom';
const DailyRecommendedCard = () => { /*
Countdown Timer Component (Popup Style)
*/
const CountdownTimer = ({ onContinue }) => {
const [timeLeft, setTimeLeft] = useState({ hours: 0, minutes: 0, seconds: 0 });
useEffect(() => {
const calculateTimeLeft = () => {
const now = new Date();
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const diff = endOfDay - now;
if (diff <= 0) return { hours: 0, minutes: 0, seconds: 0 };
return {
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / 1000 / 60) % 60),
seconds: Math.floor((diff / 1000) % 60)
};
};
setTimeLeft(calculateTimeLeft());
const timer = setInterval(() => {
setTimeLeft(calculateTimeLeft());
}, 1000);
return () => clearInterval(timer);
}, []);
const fmt = n => String(n).padStart(2, '0');
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-md p-4 overflow-y-auto">
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 40 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="bg-white rounded-[40px] shadow-2xl w-full max-w-lg overflow-hidden p-10 flex flex-col items-center text-center my-auto"
>
<div className="w-24 h-24 bg-blue-50 rounded-full flex items-center justify-center mb-8">
<div className="w-16 h-16 rounded-full border-2 border-blue-100 flex items-center justify-center">
<Timer className="w-10 h-10 text-blue-900" strokeWidth={1.5} />
</div>
</div>
<h3 className="text-[28px] font-bold text-[#1e3a8a] mb-4">Daily Limit Exceeded</h3>
<p className="text-gray-500 text-[16px] leading-relaxed mb-10 px-4">
You've viewed all recommended profiles for today. Your daily limit will reset in:
</p>
<div className="flex items-start justify-center gap-4 mb-12">
{[
{ label: 'HOURS', value: timeLeft.hours },
{ label: 'MINS', value: timeLeft.minutes },
{ label: 'SECS', value: timeLeft.seconds }
].map((u, i) => (
<div key={i} className="flex flex-col items-center">
<div className="w-20 h-20 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.06)] border border-gray-50 flex items-center justify-center mb-3">
<span className="text-[32px] font-bold text-[#1e3a8a]">{fmt(u.value)}</span>
</div>
<span className="text-[10px] font-bold text-gray-400 tracking-widest">{u.label}</span>
</div>
))}
</div>
<button
onClick={onContinue}
className="w-full py-5 bg-[#009944] text-white rounded-2xl font-bold text-[18px] shadow-lg shadow-green-900/20 hover:bg-[#00883d] transition-all active:scale-[0.98]"
>
Continue to Dashboard
</button>
</motion.div>
</div>
);
};
import { useQuery } from '@tanstack/react-query';
import { getHeaderDetails } from "../../api/preview.api";
const DailyRecommendedCard = ({ profiles: initialProfiles = [] }) => {
const swiperRef = useRef(null); const swiperRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [activeProfiles, setActiveProfiles] = useState(initialProfiles);
const [isHidden, setIsHidden] = useState(() => {
const hiddenDate = localStorage.getItem('daily_limit_hidden_date');
return hiddenDate === new Date().toDateString();
});
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
// Sample profile data const { data: headerData } = useQuery({
const profiles = [ queryKey: ["headerDetails"],
{ queryFn: getHeaderDetails,
id: 1, staleTime: 60000,
name: 'Selva Kumar . R', });
userId: 'TK52586A',
lastSeen: '14 Nov 25', const isUserPaid = headerData?.myDetails?.is_paid_member === true;
age: 23,
height: '5\'2"', useEffect(() => {
religion: 'Hindu / Agamudayar / Thuluva Vellal', setActiveProfiles(initialProfiles);
education: 'BCA, Data Analyst', }, [initialProfiles]);
location: 'Vellore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=500&fit=crop',
isPremium: true const handleContinueToDashboard = () => {
}, localStorage.setItem('daily_limit_hidden_date', new Date().toDateString());
{ setIsHidden(true);
id: 2, };
name: 'Priya Sharma',
userId: 'TK52587B', const handleInterest = async (e, profileId, name) => {
lastSeen: '15 Nov 25', e.stopPropagation();
age: 25, if (!isUserPaid) {
height: '5\'4"', setIsUpgradeModalOpen(true);
religion: 'Hindu / Brahmin / Iyer', return;
education: 'MBA, Marketing Manager',
location: 'Chennai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 3,
name: 'Rahul Venkat',
userId: 'TK52588C',
lastSeen: '16 Nov 25',
age: 28,
height: '5\'10"',
religion: 'Hindu / Mudaliar / Arcot',
education: 'B.Tech, Software Engineer',
location: 'Bangalore, Karnataka',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 4,
name: 'Aishwarya Reddy',
userId: 'TK52589D',
lastSeen: '17 Nov 25',
age: 26,
height: '5\'5"',
religion: 'Hindu / Reddy / Telangana',
education: 'CA, Chartered Accountant',
location: 'Hyderabad, Telangana',
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=500&fit=crop',
isPremium: true
},
{
id: 5,
name: 'Karthik Mohan',
userId: 'TK52590E',
lastSeen: '18 Nov 25',
age: 27,
height: '5\'8"',
religion: 'Hindu / Nadar / Tamil',
education: 'M.Tech, Civil Engineer',
location: 'Madurai, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&h=500&fit=crop',
isPremium: false
},
{
id: 6,
name: 'Divya Lakshmi',
userId: 'TK52591F',
lastSeen: '19 Nov 25',
age: 24,
height: '5\'3"',
religion: 'Hindu / Pillai / Tamil',
education: 'B.Com, HR Executive',
location: 'Coimbatore, Tamil Nadu',
image: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&h=500&fit=crop',
isPremium: true
} }
]; try {
await axiosInstance.post(`${API_ENDPOINTS.INTEREST_SEND}?profile_id=${profileId}`);
toast.success(`Interest sent to ${name}!`, {
icon: '❤️',
style: { borderRadius: '10px', background: '#333', color: '#fff' }
});
setActiveProfiles(prev => prev.filter(p => p.id !== profileId));
} catch (error) {
toast.error("Failed to send interest. Please try again.");
}
};
const handleDecline = async (e, profileId) => {
e.stopPropagation();
try {
await axiosInstance.post(`${API_ENDPOINTS.DAILY_RECOMMENDED_DONT_SHOW}?profile_id=${profileId}`);
setActiveProfiles(prev => prev.filter(p => p.id !== profileId));
toast.success("Profile hidden");
} catch (error) {
toast.error("Failed to hide profile.");
}
};
const ProfileCard = ({ profile }) => { const ProfileCard = ({ profile }) => {
const [isLiked, setIsLiked] = useState(false); const image = profile.photo || profile.image;
const memberId = profile.member_id || profile.userId;
const religion = profile.religion || (profile.religion_name ? `${profile.religion_name}${profile.caste_name ? ' / ' + profile.caste_name : ''}` : null);
const education = profile.education || profile.education_name;
const occupation = profile.occupation || profile.occupation_name;
const location = profile.location || profile.district_name || profile.city_name;
const income = profile.income || profile.annual_income || profile.salary;
const [isShortlisted, setIsShortlisted] = useState(profile.is_shortlisted === 1);
const handleShortlistClick = async (e) => {
e.stopPropagation();
try {
const res = await axiosInstance.post(`${API_ENDPOINTS.SHORTLIST_API}?profile_id=${profile.id}`);
if (res.data?.status === "success") {
setIsShortlisted(!isShortlisted);
toast.success(res.data.message || "Updated shortlist status");
}
} catch (error) {
toast.error("Failed to update shortlist");
}
};
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
onClick={() => navigate(`/profile-details/${profile.id}`)} onClick={() => navigate(`/profile-details/${profile.id}`)}
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border-1 border-green-200" className="w-full max-w-sm rounded-[24px] shadow-xl overflow-hidden border border-green-100 bg-white group hover:shadow-2xl transition-all"
> >
<div className="relative"> <div className="relative overflow-hidden">
{profile.isPremium && ( {profile.isPremium && (
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
transition={{ delay: 0.2, type: 'spring' }}
className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg" className="absolute top-4 left-4 z-10 bg-orange-500 rounded-full p-2 shadow-lg"
> >
<Crown className="w-5 h-5 text-white" /> <Crown className="w-5 h-5 text-white" />
@ -128,150 +204,103 @@ const DailyRecommendedCard = () => {
)} )}
<motion.button <motion.button
whileHover={{ scale: 1 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors" className="absolute top-4 right-4 z-10 bg-white/90 backdrop-blur-sm rounded-full px-4 py-2 shadow-md flex items-center space-x-2 hover:bg-white transition-colors"
onClick={(e) => { onClick={handleShortlistClick}
e.stopPropagation();
}}
> >
<Bookmark className="w-4 h-4" /> <Bookmark
<span className="text-[12px] font-medium">Shortlist</span> className={`w-4 h-4 transition-colors ${isShortlisted ? "text-black fill-black" : "text-green-700"}`}
/>
<span className="text-[12px] font-bold text-gray-700">
{isShortlisted ? "Shortlisted" : "Shortlist"}
</span>
</motion.button> </motion.button>
<div <div className="bg-gray-200 overflow-hidden w-full h-[300px]">
className=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
style={{ height: "300px" }}
>
<img <img
src={profile.image} src={image}
alt={profile.name} alt={profile.name}
className="w-full h-full object-cover" className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/> />
</div> </div>
<div <div
className="absolute bottom-0 left-0 right-0 h-35 pointer-events-none" className="absolute bottom-0 left-0 right-0 h-32 pointer-events-none"
style={{ style={{
background: background: "linear-gradient(to top, white 0%, rgba(255,255,255,0.8) 40%, transparent 100%)",
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
}} }}
></div> ></div>
<div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900"> <div className="absolute bottom-0 left-0 right-0 p-6 pb-2">
<h1 className="text-[18px] text-green-900 font-bold mb-2"> <h1 className="text-[20px] text-black-900 font-bold mb-1 truncate">
{profile.name} {profile.name}
</h1> </h1>
<p className="text-[14px] text-gray-700 leading-relaxed"> <p className="text-[13px] text-gray-500 font-medium">
Matrimony ID: {profile.userId} ID: {memberId}
</p> </p>
</div> </div>
</div> </div>
<div <div className="px-6 pb-6 pt-2 space-y-3 bg-white min-h-[160px]">
className="px-4 pt-[-2px] pb-4 flex flex-col gap-2" {(profile.age || profile.height) && (
style={{ background: "rgb(255, 255, 255)" }}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CakeIcon className="w-4 h-4 text-gray-700" /> <img src={personIcon} alt="Person" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-medium text-gray-700">
Age : {profile.age} {profile.age ? `${profile.age} Yrs` : ''}
{profile.age && profile.height ? ', ' : ''}
{profile.height || ''}
</span> </span>
</div> </div>
)}
{religion && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" /> <img src={religionIcon} alt="Religion" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-medium text-gray-700 truncate">
Height: {profile.height} {religion}
</span> </span>
</div> </div>
</div> )}
<div className="flex items-center gap-4"> {(education || occupation) && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GroupsIcon className="w-4 h-4 text-gray-700" /> <SchoolIcon sx={{ fontSize: 18 }} className="text-gray-400" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-medium text-gray-700 truncate">
{profile.religion} {education || occupation}
</span> </span>
</div> </div>
</div> )}
<div className="flex items-center gap-4"> {location && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SchoolIcon className="w-4 h-4 text-gray-700" /> <img src={locationIcon} alt="Location" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-medium text-gray-700 truncate">
{profile.education} {location}
</span> </span>
</div> </div>
</div> )}
<div className="flex items-center gap-4"> {income && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LocationOnIcon className="w-4 h-4 text-gray-700" /> <img src={cashIcon} alt="Income" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-600 text-gray-900"> <span className="text-[14px] font-medium text-gray-700 truncate">
{profile.location} {income}
</span> </span>
</div> </div>
</div> )}
<div className="flex gap-3 my-2 justify-between w-full px-[0px]"> <div className="flex gap-3 pt-4 justify-between">
<button <button
onClick={(e) => { onClick={(e) => handleDecline(e, profile.id)}
e.stopPropagation(); className="flex-1 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-600 font-bold text-sm rounded-full transition-all flex items-center justify-center gap-2 active:scale-95"
}}
className="gap-2 px-3 w-[fit-content] bg-[#A70710] hover:bg-red-600 text-white
font-semibold text-base py-2 rounded-[20px] shadow-md
hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95"
> >
<svg <X size={16} /> Decline
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path
d="M18 6L6 18M6 6l12 12"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Decline
</button> </button>
<button <button
className="w-[fit-content] bg-[#034E08] hover:bg-green-700 text-white font-semibold text-base className="flex-1 px-4 py-2 bg-[#034E08] hover:bg-green-800 text-white font-bold text-sm rounded-full transition-all flex items-center justify-center gap-2 shadow-lg shadow-green-900/20 active:scale-95"
rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300 onClick={(e) => handleInterest(e, profile.id, profile.name)}
transform hover:scale-105 flex items-center justify-center"
onClick={(e) => {
e.stopPropagation();
setIsLiked(!isLiked);
}}
> >
{isLiked ? ( <Heart size={16} fill="white" /> Interest
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
fill="#EF4444"
/>
</svg>
) : (
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
Interest
</button> </button>
</div> </div>
</div> </div>
@ -279,127 +308,70 @@ const DailyRecommendedCard = () => {
); );
}; };
if (isHidden) return null;
return ( return (
<div className=" py-10"> <div className="py-12">
<div className="w-[100%] max-w-[1400px] mx-auto"> <UpgradeModal
{/* Header */} isOpen={isUpgradeModalOpen}
<motion.div onClose={() => setIsUpgradeModalOpen(false)}
initial={{ opacity: 0, y: -20 }} />
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12" {activeProfiles.length === 0 ? (
> <CountdownTimer onContinue={handleContinueToDashboard} />
<h1 className="text-[20px] text-[#00000] sm:text-[22px] lg:text-[24px] font-semibold mb-3"> ) : (
Daily Recommended <div className="w-full max-w-[1400px] mx-auto px-4">
</h1> <motion.div
<p className="text-gray-900 text-[12px]">Find your perfect match today</p> initial={{ opacity: 0, y: -20 }}
</motion.div> animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
{/* Swiper Container */}
<div className="relative px-0 sm:px-0">
<Swiper
ref={swiperRef}
modules={[Navigation, Pagination, Autoplay, EffectCoverflow]}
spaceBetween={10}
slidesPerView={1}
autoplay={{
delay: 5000,
disableOnInteraction: false,
pauseOnMouseEnter: true
}}
// pagination={{
// clickable: true,
// dynamicBullets: true
// }}
loop={true}
speed={800}
breakpoints={{
640: {
slidesPerView: 2,
spaceBetween: 10
},
1024: {
slidesPerView: 3,
spaceBetween: 10
},
1280: {
slidesPerView: 4,
spaceBetween: 5
}
}}
className="pb-16"
> >
{profiles.map((profile) => ( <h2 className="text-3xl font-extrabold text-gray-900 mb-2">
<SwiperSlide key={profile.id}> Daily <span className="text-green-700">Recommended</span>
<ProfileCard profile={profile} /> </h2>
</SwiperSlide> <p className="text-gray-500 font-medium">Handpicked matches just for you</p>
))} </motion.div>
</Swiper>
<div className="relative group">
<Swiper
ref={swiperRef}
modules={[Navigation, Pagination, Autoplay, EffectCoverflow]}
spaceBetween={24}
slidesPerView={1}
autoplay={{ delay: 4000, disableOnInteraction: false }}
breakpoints={{
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 },
1280: { slidesPerView: 4 }
}}
className="!pb-12"
>
{activeProfiles.map((profile) => (
<SwiperSlide key={profile.id}>
<ProfileCard profile={profile} />
</SwiperSlide>
))}
</Swiper>
<button
onClick={() => swiperRef.current?.swiper.slidePrev()}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 -translate-x-4 bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all hidden lg:flex border border-gray-100"
>
<ChevronLeft className="w-6 h-6 text-green-800" />
</button>
<button
onClick={() => swiperRef.current?.swiper.slideNext()}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 translate-x-4 bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all hidden lg:flex border border-gray-100"
>
<ChevronRight className="w-6 h-6 text-green-800" />
</button>
</div>
{/* Custom Navigation Buttons */}
<motion.button
whileHover={{ scale: 1.1, x: -5 }}
whileTap={{ scale: 0.9 }}
onClick={() => swiperRef.current?.swiper.slidePrev()}
className="hidden sm:flex absolute left-0 top-1/2 -translate-y-1/2 z-10
bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all
w-12 h-12 rounded-full shadow-xl hover:shadow-2xl items-center justify-center "
>
<ChevronLeft className="w-6 h-6 text-gray-700" />
</motion.button>
<motion.button
whileHover={{ scale: 1.1, x: 5 }}
whileTap={{ scale: 0.9 }}
onClick={() => swiperRef.current?.swiper.slideNext()}
className="hidden sm:flex absolute right-0 top-1/2 -translate-y-1/2 z-10
bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all
w-12 h-12 rounded-full shadow-xl hover:shadow-2xl items-center justify-center transition-all"
>
<ChevronRight className="w-6 h-6 text-gray-700" />
</motion.button>
</div> </div>
)}
{/* View All Button */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-center mt-12"
>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-6 py-3 bg-[#034E08] text-white rounded-full font-semibold text-lg shadow-lg hover:shadow-xl transition-shadow"
onClick={() => navigate("/matches")}
>
View All Recommendations
</motion.button>
</motion.div>
</div>
{/* Custom Swiper Styles */}
<style>{`
.swiper-pagination-bullet {
width: 10px;
height: 10px;
background: #A70710;
opacity: 0.5;
}
.swiper-pagination-bullet-active {
opacity: 1;
width: 30px;
border-radius: 5px;
}
// .swiper-slide {
// // height: auto;
// // display: flex;
// }
`}</style>
</div> </div>
); );
}; };

View File

@ -16,6 +16,13 @@ import {
Heart, Heart,
Eye, Eye,
} from "lucide-react"; } from "lucide-react";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
// Import Swiper styles // Import Swiper styles
import "swiper/css"; import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
@ -124,67 +131,63 @@ const ProfileCard = ({ profile }) => {
} }
}} }}
> >
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} /> <Bookmark size={14} fill={isShortlisted ? "#000" : "none"} className={isShortlisted ? "text-black" : ""} />
{shortlistMutation.isPending ? "..." : "Shortlist"} {shortlistMutation.isPending ? "..." : (isShortlisted ? "Shortlisted" : "Shortlist")}
</div> </div>
</div> </div>
{/* CONTENT */} {/* CONTENT */}
<div className="px-4 py-4 -mt-[60px] bg-white/65 backdrop-blur-[25px] rounded-t-[15px] shadow-[0_-10px_30px_rgba(0,0,0,0.15)] relative z-[2]"> <div className="px-5 py-5 -mt-8 bg-white rounded-t-[32px] relative z-[2] shadow-[0_-12px_40px_rgba(0,0,0,0.1)] h-[240px] flex flex-col">
<h2 className="text-center text-[22px] font-semibold mb-1"> <div className="text-left mb-4">
{name} <h2 className="text-[20px] font-bold text-gray-900 leading-tight">
</h2> {name}
</h2>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8"> <div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
<p>ID: {idNumber}</p> <span>ID: {idNumber}</span>
<p className="flex items-center gap-0.5"> <span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<Eye size={12} /> {lastSeen} <span className="flex items-center gap-1">
</p> <Eye size={12} /> {lastSeen}
</span>
</div>
</div> </div>
<div className="flex flex-wrap justify-center gap-2"> <div className="space-y-3 mt-2">
{[ {(age || height) && (
age, <div className="flex items-center gap-3">
height, <div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
salary, <img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
location, </div>
caste, <span className="text-[13px] font-bold text-gray-700">
zodiac1, {age || ""}
zodiac2, {age && height ? ", " : ""}
] {height || ""}
.filter(Boolean)
.map((v, i) => (
<span
key={i}
className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
>
{v}
</span> </span>
))} </div>
</div> )}
<div className="flex gap-4 mt-[15px] justify-center"> {(caste || zodiac1) && (
<button <div className="flex items-center gap-3">
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" : ""}`} <div className="w-8 h-8 rounded-full bg-pink-50 flex items-center justify-center flex-shrink-0">
onClick={(e) => { <img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-80" />
e.stopPropagation(); </div>
if (!declineMutation.isPending) declineMutation.mutate(id); <span className="text-[13px] font-bold text-gray-700 truncate">
}} {caste}
disabled={declineMutation.isPending} {caste && zodiac1 ? ` (${zodiac1})` : zodiac1 || ""}
> </span>
<X size={18} /> {declineMutation.isPending ? "..." : "Decline"} </div>
</button> )}
<button {location && (
className={`px-2 py-1 rounded-[20px] border border-green-200 bg-green-50 text-green-900 flex items-center gap-1.5 font-semibold hover:bg-green-100 transition-colors ${interestMutation.isPending ? "opacity-50 cursor-wait" : ""}`} <div className="flex items-center gap-3">
onClick={(e) => { <div className="w-8 h-8 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0">
e.stopPropagation(); <img src={locationIcon} alt="Location" className="w-4 h-4 opacity-80" />
if (!interestMutation.isPending) interestMutation.mutate(id); </div>
}} <span className="text-[13px] font-bold text-gray-700 truncate">
disabled={interestMutation.isPending} {location}
> </span>
<Heart size={18} /> {interestMutation.isPending ? "..." : "Interest"} </div>
</button> )}
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -210,16 +213,19 @@ const MatchingList = ({ matches }) => {
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-center mb-12" className="text-center mb-10"
> >
<h1 className="text-[20px] text-[#00000] sm:text-[22px] lg:text-[24px] font-semibold mb-3"> <h2 className="text-[28px] font-extrabold text-gray-900 mb-1">
Your Matching List All Matches <span className="text-green-700">List</span>
</h1> </h2>
<p className="text-gray-900 text-[12px]">
Find your perfect match today <p className="text-gray-500 font-medium">
We've found these profiles that match your preferences
</p> </p>
</motion.div> </motion.div>
{/* Swiper Container */} {/* Swiper Container */}
<div className="relative"> <div className="relative">
<Swiper <Swiper

View File

@ -11,6 +11,13 @@ import {
Heart, Heart,
Eye, Eye,
} from 'lucide-react'; } from 'lucide-react';
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
// Import Swiper styles // Import Swiper styles
import 'swiper/css'; import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
@ -119,67 +126,63 @@ const ProfileCard = ({ profile }) => {
} }
}} }}
> >
<Bookmark size={14} fill={isShortlisted ? "#000" : "none"} /> <Bookmark size={14} fill={isShortlisted ? "#000" : "none"} className={isShortlisted ? "text-black" : ""} />
{shortlistMutation.isPending ? "..." : "Shortlist"} {shortlistMutation.isPending ? "..." : (isShortlisted ? "Shortlisted" : "Shortlist")}
</div> </div>
</div> </div>
{/* CONTENT */} {/* CONTENT */}
<div className="px-4 py-4 -mt-[60px] bg-white/65 backdrop-blur-[25px] rounded-t-[15px] shadow-[0_-10px_30px_rgba(0,0,0,0.15)] relative z-[2]"> <div className="px-5 py-5 -mt-8 bg-white rounded-t-[32px] relative z-[2] shadow-[0_-12px_40px_rgba(0,0,0,0.1)] h-[240px] flex flex-col">
<h2 className="text-center text-[22px] font-semibold mb-1"> <div className="text-left mb-4">
{name} <h2 className="text-[20px] font-bold text-gray-900 leading-tight">
</h2> {name}
</h2>
<div className="flex justify-between items-center mb-2 text-[11px] text-gray-600 px-8"> <div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
<p>ID: {idNumber}</p> <span>ID: {idNumber}</span>
<p className="flex items-center gap-0.5"> <span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<Eye size={12} /> {lastSeen} <span className="flex items-center gap-1">
</p> <Eye size={12} /> {lastSeen}
</span>
</div>
</div> </div>
<div className="flex flex-wrap justify-center gap-2"> <div className="space-y-3 mt-2">
{[ {(age || height) && (
age, <div className="flex items-center gap-3">
height, <div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
salary, <img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
location, </div>
caste, <span className="text-[13px] font-bold text-gray-700">
zodiac1, {age || ""}
zodiac2, {age && height ? ", " : ""}
] {height || ""}
.filter(Boolean)
.map((v, i) => (
<span
key={i}
className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
>
{v}
</span> </span>
))} </div>
</div> )}
<div className="flex gap-4 mt-[15px] justify-center"> {(caste || zodiac1) && (
{/* <button <div className="flex items-center gap-3">
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" : ""}`} <div className="w-8 h-8 rounded-full bg-pink-50 flex items-center justify-center flex-shrink-0">
onClick={(e) => { <img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-80" />
e.stopPropagation(); </div>
if (!declineMutation.isPending) declineMutation.mutate(id); <span className="text-[13px] font-bold text-gray-700 truncate">
}} {caste}
disabled={declineMutation.isPending} {caste && zodiac1 ? ` (${zodiac1})` : zodiac1 || ""}
> </span>
<X size={18} /> {declineMutation.isPending ? "..." : "Decline"} </div>
</button> */} )}
<button {location && (
className={`px-2 py-1 rounded-[20px] border border-green-200 bg-green-50 text-green-900 flex items-center gap-1.5 font-semibold hover:bg-green-100 transition-colors ${interestMutation.isPending ? "opacity-50 cursor-wait" : ""}`} <div className="flex items-center gap-3">
onClick={(e) => { <div className="w-8 h-8 rounded-full bg-orange-50 flex items-center justify-center flex-shrink-0">
e.stopPropagation(); <img src={locationIcon} alt="Location" className="w-4 h-4 opacity-80" />
if (!interestMutation.isPending) interestMutation.mutate(id); </div>
}} <span className="text-[13px] font-bold text-gray-700 truncate">
disabled={interestMutation.isPending} {location}
> </span>
<Heart size={18} /> {interestMutation.isPending ? "..." : "Interest"} </div>
</button> )}
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -208,14 +211,16 @@ const NewJoinedProfile = ({ profiles }) => {
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-center mb-12" className="text-center mb-10"
> >
<h1 className="text-[20px] text-[#000000] sm:text-[22px] lg:text-[24px] font-semibold mb-3"> <h2 className="text-[28px] font-extrabold text-gray-900 mb-1">
New Joined Newly <span className="text-green-700">Joined</span>
</h1> </h2>
<p className="text-gray-900 text-[12px]">Find your perfect match today</p> <p className="text-gray-500 font-medium">Be the first to connect with our newest members</p>
</motion.div> </motion.div>
{/* Swiper Container */} {/* Swiper Container */}
<div className="relative"> <div className="relative">
<Swiper <Swiper

View File

@ -1,47 +1,75 @@
import { useState } from "react"; import React, { useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Crown, Bookmark } from "lucide-react"; import { Crown, Bookmark } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import CakeIcon from "@mui/icons-material/Cake"; // Custom Icons
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew"; import personIcon from "../../assets/images/personicon.svg";
import GroupsIcon from "@mui/icons-material/Groups"; import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import VisibilityIcon from "@mui/icons-material/Visibility";
import SchoolIcon from "@mui/icons-material/School"; import SchoolIcon from "@mui/icons-material/School";
import LocationOnIcon from "@mui/icons-material/LocationOn";
const buildDefaultRows = (profile) => [ const buildDefaultRows = (profile) => {
[ const rows = [];
{
icon: <CakeIcon className="w-4 h-4 text-gray-700" />, // Row 1: Age & Height
text: `Age: ${profile?.age ?? "-"}`, if (profile?.age || profile?.height) {
}, const row = [];
{ if (profile?.age && profile?.age !== "-") {
icon: <AccessibilityNewIcon className="w-4 h-4 text-gray-700" />, row.push({
text: `Height: ${profile?.height ?? "-"}`, icon: <img src={personIcon} alt="Person" className="w-4 h-4 opacity-70" />,
}, text: `${profile.age} Yrs`,
], });
[ }
{ if (profile?.height && profile?.height !== "-") {
icon: <GroupsIcon className="w-4 h-4 text-gray-700" />, row.push({
text: icon: row.length === 0 ? <img src={personIcon} alt="Person" className="w-4 h-4 opacity-70" /> : null,
profile?.religion || text: profile.height,
profile?.caste || });
profile?.community || }
"-", if (row.length > 0) rows.push(row);
}, }
],
[ // Row 2: Religion / Caste
{ const religionText = profile?.religion || (profile?.religion_name ? `${profile.religion_name}${profile.caste_name ? ' / ' + profile.caste_name : ''}` : null);
icon: <SchoolIcon className="w-4 h-4 text-gray-700" />, if (religionText && religionText !== "-") {
text: profile?.education || profile?.qualification || "-", rows.push([{
}, icon: <img src={religionIcon} alt="Religion" className="w-4 h-4 opacity-70" />,
], text: religionText,
[ }]);
{ }
icon: <LocationOnIcon className="w-4 h-4 text-gray-700" />,
text: profile?.location || "-", // Row 3: Education
}, const eduText = profile?.education || profile?.qualification || profile?.education_name;
], if (eduText && eduText !== "-") {
]; rows.push([{
icon: <SchoolIcon sx={{ fontSize: 16 }} className="text-gray-400" />,
text: eduText,
}]);
}
// Row 4: Income
const incomeText = profile?.income || profile?.annual_income || profile?.annual_income_name;
if (incomeText && incomeText !== "-") {
rows.push([{
icon: <img src={cashIcon} alt="Cash" className="w-4 h-4 opacity-70" />,
text: incomeText,
}]);
}
// Row 5: Location
const locText = profile?.location || profile?.district_name;
if (locText && locText !== "-") {
rows.push([{
icon: <img src={locationIcon} alt="Location" className="w-4 h-4 opacity-70" />,
text: locText,
}]);
}
return rows;
};
const getProfileIdText = (profile) => const getProfileIdText = (profile) =>
profile?.userId || profile?.userId ||

View File

@ -1,12 +1,13 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import AstroChatUI from "./AstroChatUI";
import LazyImage from "../common/LazyImage"; import LazyImage from "../common/LazyImage";
import ProfileIcon from "../../assets/images/profileicon.png"; import ProfileIcon from "../../assets/images/profileicon.png";
import HoroscodeIcon from "../../assets/images/horoscopericon.png"; import HoroscodeIcon from "../../assets/images/horoscopericon.png";
import FamilyIcon from "../../assets/images/homeicon.png"; import FamilyIcon from "../../assets/images/homeicon.png";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import AstroChatUI from "./AstroChatUI";
import MembershipCard from "./MembershipCard"; import MembershipCard from "./MembershipCard";
const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember }) => { const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember }) => {
@ -59,7 +60,9 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
return ( return (
<> <>
<div className="max-w-[1400px] w-[100%] mx-auto my-10 px-2 sm:p-2 lg:p-2"> <div className="max-w-[1400px] w-full mx-auto my-10 px-4">
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -111,66 +114,59 @@ const ProfileCompletion = ({ percentage = 0, missingDetails,becomePaidMember })
</div> </div>
{/* Cards Section */} {/* Cards Section */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 "> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* Left Side: Astro Chat */}
<div className="w-full">
<AstroChatUI />
</div>
{/* Desktop Logo */} {/* Right Side: Cards & Membership */}
{/* <Box sx={{ display: { xs: "none", md: "flex" }, mr: 1 }}> <div className="w-full">
<LazyImage <div className="rounded-2xl bg-green-50 border border-green-200 p-6">
src={Logo} <motion.div
className="w-full h-[50px] rounded-lg object-cover" variants={container}
/> initial="hidden"
</Box> */} animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
<AstroChatUI/>
<div className="my-4 rounded-2xl bg-green-50 border border-1 border-green-200 p-4 ">
<motion.div
variants={container}
initial="hidden"
animate="show"
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6"
>
{/* NOTE: The cards are static for now. To make them dynamic,
the `missingDetails` prop should be an array of objects to map over. */}
{cards.map((card, index) => (
<div
key={card.id}
onClick={() => navigate(card.url)}
className=" border border-1 border-red-50 bg-white rounded-3xl hover:bg-red-50 hover:border-2
flex flex-col items-center space-x-2 h-32 justify-center transition-colors duration-500
cursor-pointer "
> >
{/* Icon Container */} {cards.map((card, index) => (
<motion.div <div
initial={{ rotate: -180, opacity: 0 }} key={card.id}
animate={{ rotate: 0, opacity: 1 }} onClick={() => navigate(card.url)}
transition={{ delay: 0.9 + index * 0.1, duration: 0.5 }} className="border border-red-50 bg-white rounded-3xl hover:bg-red-50 hover:border-2 flex flex-col items-center space-x-2 h-32 justify-center transition-colors duration-500 cursor-pointer"
// className={`${card.bgColor} p-0 rounded-xl border ${card.borderColor}`} >
> <motion.div
<LazyImage initial={{ rotate: -180, opacity: 0 }}
src={card.icon} animate={{ rotate: 0, opacity: 1 }}
alt={card.title} transition={{ delay: 0.9 + index * 0.1, duration: 0.5 }}
className="w-ful h-full " >
/> <LazyImage
</motion.div> src={card.icon}
alt={card.title}
className="w-full h-full"
/>
</motion.div>
{/* Text */} <motion.h3
<motion.h3 initial={{ opacity: 0, x: -10 }}
initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }}
animate={{ opacity: 1, x: 0 }} transition={{ delay: 1 + index * 0.1, duration: 0.4 }}
transition={{ delay: 1 + index * 0.1, duration: 0.4 }} className="text-[14px] sm:text-[16px] font-semibold text-gray-900"
className="text-[14px] sm:text-[16px] font-semibold text-gray-900" >
> {card.title}
{card.title} </motion.h3>
</motion.h3> </div>
))}
</motion.div>
<div className="mt-6">
<MembershipCard becomePaidMember={becomePaidMember} />
</div> </div>
))} </div>
</motion.div> </div>
<MembershipCard becomePaidMember={becomePaidMember} />
</div> </div>
</div>
{/* Additional Info Section */} {/* Additional Info Section */}
{/* <motion.div {/* <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}

View File

@ -1,5 +1,12 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Heart, X, Crown, Bookmark, Eye, Clock, ChevronLeft, ChevronRight } from "lucide-react"; import { Heart, X, Crown, Bookmark, Eye, Clock, ChevronLeft, ChevronRight } from "lucide-react";
// Custom Icons
import personIcon from "../../assets/images/personicon.svg";
import religionIcon from "../../assets/images/religonicon.svg";
import locationIcon from "../../assets/images/locationicon.svg";
import cashIcon from "../../assets/images/cashicon.svg";
import SchoolIcon from "@mui/icons-material/School";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules"; import { Navigation, Pagination } from "swiper/modules";
import "swiper/css"; import "swiper/css";
@ -11,6 +18,7 @@ import toast from "react-hot-toast";
import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi"; import { shortlistProfile, sendInterest, declineProfile } from "../../services/shortlistapi";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
const ProfileCardItem = ({ profile }) => { const ProfileCardItem = ({ profile }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -118,27 +126,48 @@ const ProfileCardItem = ({ profile }) => {
</p> </p>
</div> </div>
<div className="flex flex-wrap justify-center gap-2"> <div className="flex flex-col gap-2 mt-4 min-h-[140px]">
{[ {(age || height) && (
age, <div className="flex items-center gap-2">
height, <img src={personIcon} alt="Person" className="w-5 h-5 opacity-70" />
salary, <span className="text-[14px] font-medium text-gray-700">
location, {age || ""}
caste, {age && height ? ", " : ""}
zodiac1, {height || ""}
zodiac2,
]
.filter(Boolean)
.map((v, i) => (
<span
key={i}
className="px-1.5 py-1.5 rounded-[20px] bg-white/70 border border-black/8 text-[13px]"
>
{v}
</span> </span>
))} </div>
)}
{(caste || zodiac1) && (
<div className="flex items-center gap-2">
<img src={religionIcon} alt="Religion" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{caste}
{caste && zodiac1 ? ` (${zodiac1})` : zodiac1 || ""}
</span>
</div>
)}
{salary && (
<div className="flex items-center gap-2">
<img src={cashIcon} alt="Cash" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{salary}
</span>
</div>
)}
{location && (
<div className="flex items-center gap-2">
<img src={locationIcon} alt="Location" className="w-5 h-5 opacity-70" />
<span className="text-[14px] font-medium text-gray-700 truncate">
{location}
</span>
</div>
)}
</div> </div>
<div className="flex gap-4 mt-[15px] justify-center"> <div className="flex gap-4 mt-[15px] justify-center">
<button <button
className={`px-2 py-1 rounded-[20px] border border-red-200 bg-red-50 flex items-center gap-1.5 font-semibold text-red-900 hover:bg-red-100 transition-colors ${declineMutation.isPending ? "opacity-50 cursor-wait" : ""}`} className={`px-2 py-1 rounded-[20px] border border-red-200 bg-red-50 flex items-center gap-1.5 font-semibold text-red-900 hover:bg-red-100 transition-colors ${declineMutation.isPending ? "opacity-50 cursor-wait" : ""}`}

View File

@ -73,35 +73,31 @@ const EducationalDetailsForm = ({
dispatch(updateEducationalDetails(updates)); dispatch(updateEducationalDetails(updates));
setLocalErrors((prev) => ({ ...prev, [field]: "" })); setLocalErrors((prev) => ({ ...prev, [field]: "" }));
if (!isEditMode) {
dispatch(clearAllStepsFrom(3));
}
}; };
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!data.study_field) newErrors.study_field = "Required"; if (!data.study_field) newErrors.study_field = "Field of study is required";
if (!data.education) newErrors.education = "Required"; if (!data.education) newErrors.education = "Highest qualification is required";
if (!data.education_detail) newErrors.education_detail = "Required"; if (!data.education_detail) newErrors.education_detail = "Education detail is required";
if (!data.employee_type) newErrors.employee_type = "Required"; if (!data.employee_type) newErrors.employee_type = "Employee type is required";
if (!isUnemployed) { if (!isUnemployed) {
if (!data.occupation) newErrors.occupation = "Required"; if (!data.occupation) newErrors.occupation = "Occupation is required";
if (!data.occupation_detail) newErrors.occupation_detail = "Required"; if (!data.occupation_detail) newErrors.occupation_detail = "Occupation detail is required";
if (!data.income_currency) newErrors.income_currency = "Required"; if (!data.income_currency) newErrors.income_currency = "Income currency is required";
if (!data.annual_income) newErrors.annual_income = "Required"; if (!data.annual_income) newErrors.annual_income = "Annual income is required";
if (!data.work_country) newErrors.work_country = "Required"; if (!data.work_country) newErrors.work_country = "Work country is required";
if (isIndia) { if (isIndia) {
if (!data.work_state) newErrors.work_state = "Required"; if (!data.work_state) newErrors.work_state = "Work state is required";
if (!data.work_district) newErrors.work_district = "Required"; if (!data.work_district) newErrors.work_district = "Work city is required";
} else { } else {
if (!data.work_city) newErrors.work_city = "Required"; if (!data.work_city) newErrors.work_city = "Work city is required";
} }
} }
if (!data.address) newErrors.address = "Required"; if (!data.address) newErrors.address = "Work address is required";
setLocalErrors(newErrors); setLocalErrors(newErrors);
return newErrors; return newErrors;

View File

@ -119,10 +119,6 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
dispatch(updateFamilyDetails(updates)); dispatch(updateFamilyDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear); if (onFieldChange) onFieldChange(fieldsToClear);
if (!isEditMode) {
dispatch(clearAllStepsFrom(4));
}
}; };
const handleSiblingChange = (type, index, field, value) => { const handleSiblingChange = (type, index, field, value) => {
@ -131,10 +127,6 @@ const FamilyDetailsForm = ({ onSubmitStep, onSkipStep, errors, onFieldChange, is
list[index] = { ...list[index], [field]: value }; list[index] = { ...list[index], [field]: value };
dispatch(updateFamilyDetails({ [type]: list })); dispatch(updateFamilyDetails({ [type]: list }));
if (onFieldChange) onFieldChange(type); if (onFieldChange) onFieldChange(type);
if (!isEditMode) {
dispatch(clearAllStepsFrom(4));
}
}; };
const scrollToError = (errorMap) => { const scrollToError = (errorMap) => {

View File

@ -18,11 +18,11 @@ import {
DialogActions, DialogActions,
Tooltip, Tooltip,
} from "@mui/material"; } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers"; import { LocalizationProvider, DatePicker, TimePicker } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { format } from "date-fns";
import { useLifestyleMasters } from "../hooks/useMasters"; import { useLifestyleMasters } from "../hooks/useMasters";
import { useStarMasters, usePathamMasters } from "../hooks/useDependentMasters";
import horoscopeImg from "../assets/images/horoscopeimg.png"; import horoscopeImg from "../assets/images/horoscopeimg.png";
const LifestyleDetailsForm = ({ const LifestyleDetailsForm = ({
@ -34,30 +34,47 @@ const LifestyleDetailsForm = ({
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.registerform.lifestyleDetails); const data = useSelector((state) => state.registerform.lifestyleDetails);
if (!data) return null;
const inputRef = useRef(null); const inputRef = useRef(null);
const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>; const requiredMark = <span style={{ color: "#d32f2f" }}> *</span>;
const { data: lifestyleMasters, isLoading: isLifestyleMastersLoading } = const { data: lifestyleMasters, isLoading: isLifestyleMastersLoading } =
useLifestyleMasters(); useLifestyleMasters();
const dietOptions = useMemo(() => { const raasiOptions = useMemo(() => {
const raw = lifestyleMasters; const raw = lifestyleMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.raasis || raw.raasi || raw.rasi || [];
return raw.diet || raw.diets || [];
}, [lifestyleMasters]); }, [lifestyleMasters]);
const hobbyOptions = useMemo(() => { const panjangamOptions = useMemo(() => {
const raw = lifestyleMasters; const raw = lifestyleMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.panjangam_types || raw.panjangamTypes || [];
return raw.hobbies || raw.hobby || [];
}, [lifestyleMasters]); }, [lifestyleMasters]);
const { data: starData } = useStarMasters(data.raasi);
const starOptions = useMemo(() => {
if (!starData) return [];
return starData.stars || starData.star || starData.data || (Array.isArray(starData) ? starData : []);
}, [starData]);
const { data: pathamData } = usePathamMasters(data.star);
const pathamOptions = useMemo(() => {
if (!pathamData) return [];
return (
pathamData.pathams ||
pathamData.patham ||
pathamData.data ||
(Array.isArray(pathamData) ? pathamData : [])
);
}, [pathamData]);
const grahaOptions = useMemo(() => { const grahaOptions = useMemo(() => {
const raw = lifestyleMasters; const raw = lifestyleMasters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw;
return raw.grahas || raw.graha || []; return raw.grahas || raw.graha || [];
}, [lifestyleMasters]); }, [lifestyleMasters]);
@ -74,13 +91,21 @@ const LifestyleDetailsForm = ({
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
useEffect(() => {
if (data.dob) {
const date = new Date(data.dob);
if (!isNaN(date.getTime())) {
const day = format(date, "EEEE");
if (data.dayOfBirth !== day) {
handleChange("dayOfBirth", day);
}
}
}
}, [data.dob]);
const handleChange = (field, value) => { const handleChange = (field, value) => {
dispatch(updateLifestyleDetails({ [field]: value })); dispatch(updateLifestyleDetails({ [field]: value }));
if (onFieldChange) onFieldChange(field); if (onFieldChange) onFieldChange(field);
if (!isEditMode) {
dispatch(clearAllStepsFrom(5));
}
}; };
const handleMultiChange = (field, value) => { const handleMultiChange = (field, value) => {
@ -189,7 +214,7 @@ const LifestyleDetailsForm = ({
return `${h}:${m}`; return `${h}:${m}`;
}; };
const renderChartCell = (label, value, onChange) => ( const renderChartCell = (label, value = [], onChange) => (
<Tooltip title={value && value.length > 0 ? value.join(", ") : ""} arrow placement="top"> <Tooltip title={value && value.length > 0 ? value.join(", ") : ""} arrow placement="top">
<div className="bg-white border border-gray-300 rounded-lg p-2 flex flex-col items-center justify-center min-h-[70px]"> <div className="bg-white border border-gray-300 rounded-lg p-2 flex flex-col items-center justify-center min-h-[70px]">
<span className="text-[10px] font-medium text-gray-700 text-center"> <span className="text-[10px] font-medium text-gray-700 text-center">
@ -221,7 +246,7 @@ const LifestyleDetailsForm = ({
> >
{grahaOptions.map((opt) => ( {grahaOptions.map((opt) => (
<MenuItem key={opt} value={opt}> <MenuItem key={opt} value={opt}>
<Checkbox checked={value.indexOf(opt) > -1} /> <Checkbox checked={Array.isArray(value) && value.indexOf(opt) > -1} />
<ListItemText primary={opt} /> <ListItemText primary={opt} />
</MenuItem> </MenuItem>
))} ))}
@ -267,120 +292,13 @@ const LifestyleDetailsForm = ({
return ( return (
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Diet{requiredMark}
</label>
<FormControl fullWidth variant="outlined" error={Boolean(errors.diets)}>
<InputLabel id="diets-label">Select Diet</InputLabel>
<Select
labelId="diets-label"
label="Select Diet"
name="diets"
value={data.diets}
onChange={(e) => handleChange("diets", e.target.value)}
inputRef={inputRef}
disabled={isLifestyleMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{dietOptions.map((opt) => {
const value = opt.id ?? opt;
const label = opt.diet_name || opt.name || String(opt);
return (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
);
})}
</Select>
{errors.diets && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.diets}
</p>
)}
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Hobbies & Interests (Multi-select){requiredMark}
</label>
<FormControl
fullWidth
variant="outlined"
error={Boolean(errors.hobbies)}
>
<InputLabel id="hobbies-label">
Select Hobbies & Interests
</InputLabel>
<Select
labelId="hobbies-label"
label="Select Hobbies & Interests"
name="hobbies"
multiple
value={data.hobbies}
onChange={(e) => handleMultiChange("hobbies", e.target.value)}
disabled={isLifestyleMastersLoading}
renderValue={(selected) =>
selected
.map((id) => {
const item = hobbyOptions.find(
(opt) => (opt.id ?? opt) === id
);
return item?.hobby_name || item?.name || id;
})
.join(", ")
}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{hobbyOptions.map((opt) => {
const value = opt.id ?? opt;
const label = opt.hobby_name || opt.name || String(opt);
return (
<MenuItem key={value} value={value}>
<Checkbox checked={data.hobbies.indexOf(value) > -1} />
<ListItemText primary={label} />
</MenuItem>
);
})}
</Select>
{errors.hobbies && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.hobbies}
</p>
)}
</FormControl>
</div>
</div>
<div className="text-center py-4"> <div className="text-center py-4">
<h2 className="text-[18px] font-semibold text-gray-800"> <h2 className="text-[18px] font-semibold text-gray-800">
Astrology / Horoscope Astrology / Horoscope
</h2> </h2>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[900px] mx-auto"> <div className="grid grid-cols-1 md:grid-cols-4 gap-6 w-full max-w-[1200px] mx-auto">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Date of Birth{requiredMark} Date of Birth{requiredMark}
@ -403,6 +321,19 @@ const LifestyleDetailsForm = ({
</LocalizationProvider> </LocalizationProvider>
</div> </div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Day of Birth</label>
<TextField
fullWidth
name="dayOfBirth"
value={data.dayOfBirth}
readOnly
variant="outlined"
placeholder="Day of Week"
InputProps={{ readOnly: true }}
/>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">
Time of Birth{requiredMark} Time of Birth{requiredMark}
@ -424,7 +355,7 @@ const LifestyleDetailsForm = ({
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Place of Birth</label> <label className="text-gray-900 text-[15px]">Place of Birth{requiredMark}</label>
<TextField <TextField
fullWidth fullWidth
name="placeOfBirth" name="placeOfBirth"
@ -438,32 +369,186 @@ const LifestyleDetailsForm = ({
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-[950px] mx-auto mt-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[1200px] mx-auto mt-6">
<div> <div className="flex flex-col gap-2">
<div className="py-4"> <label className="text-gray-900 text-[15px]">Select Raasi{requiredMark}</label>
<h3 className="text-base font-semibold text-gray-800 mb-3"> <FormControl fullWidth variant="outlined" error={Boolean(errors.raasi)} id="raasi">
Add Rasi <InputLabel id="raasi-label">Select Raasi</InputLabel>
</h3> <Select
{renderChartGrid("graha")} labelId="raasi-label"
{errors.graha_duplicate && ( label="Select Raasi"
<p style={{ color: "#d32f2f", fontSize: "0.75rem", marginTop: "4px" }}> name="raasi"
{errors.graha_duplicate} value={data.raasi}
</p> onChange={(e) => {
)} handleChange("raasi", e.target.value);
</div> handleChange("star", "");
handleChange("patham", "");
}}
>
{raasiOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.raasi_name || opt.rasi_name}
</MenuItem>
))}
</Select>
</FormControl>
</div> </div>
<div> <div className="flex flex-col gap-2">
<div className="py-4"> <label className="text-gray-900 text-[15px]">Select Birth Star{requiredMark}</label>
<h3 className="text-base font-semibold text-gray-800 mb-3"> <FormControl fullWidth variant="outlined" error={Boolean(errors.star)} id="star">
Add Navamsam <InputLabel id="star-label">Select Birth Star</InputLabel>
</h3> <Select
{renderChartGrid("amsam")} labelId="star-label"
{errors.amsam_duplicate && ( label="Select Birth Star"
<p style={{ color: "#d32f2f", fontSize: "0.75rem", marginTop: "4px" }}> name="star"
{errors.amsam_duplicate} value={data.star}
</p> onChange={(e) => {
)} handleChange("star", e.target.value);
handleChange("patham", "");
}}
>
{starOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.star_name || opt.name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Select Padham</label>
<FormControl fullWidth variant="outlined" id="patham">
<InputLabel id="patham-label">Select Padham</InputLabel>
<Select
labelId="patham-label"
label="Select Padham"
name="patham"
value={data.patham}
onChange={(e) => handleChange("patham", e.target.value)}
>
{pathamOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Select Lagnam</label>
<FormControl fullWidth variant="outlined" id="lagnam">
<InputLabel id="lagnam-label">Select Lagnam</InputLabel>
<Select
labelId="lagnam-label"
label="Select Lagnam"
name="lagnam"
value={data.lagnam}
onChange={(e) => handleChange("lagnam", e.target.value)}
>
{raasiOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.raasi_name || opt.rasi_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Panjangam Type</label>
<FormControl fullWidth variant="outlined" id="panjangam_type">
<InputLabel id="panjangam-label">Select Panjangam Type</InputLabel>
<Select
labelId="panjangam-label"
label="Select Panjangam Type"
name="panjangam_type"
value={data.panjangam_type}
onChange={(e) => handleChange("panjangam_type", e.target.value)}
>
{panjangamOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-[1200px] mx-auto mt-6">
<div className="flex flex-col gap-2">
<h3 className="text-base font-semibold text-gray-800 mb-3">Add Rasi</h3>
{renderChartGrid("graha")}
</div>
<div className="flex flex-col gap-2">
<h3 className="text-base font-semibold text-gray-800 mb-3">Add Navamsam</h3>
{renderChartGrid("amsam")}
</div>
</div>
<div className="mt-8 w-full max-w-[1200px] mx-auto p-6 border border-gray-200 rounded-lg bg-gray-50">
<h3 className="text-base font-semibold text-gray-800 mb-4">Dasa Details</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Type</label>
<FormControl fullWidth variant="outlined" id="dasa_balance">
<InputLabel id="dasa-label">Select Dasa Type</InputLabel>
<Select
labelId="dasa-label"
label="Select Dasa Type"
name="dasa_balance"
value={data.dasa_balance}
onChange={(e) => handleChange("dasa_balance", e.target.value)}
>
{grahaOptions.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Year</label>
<TextField
id="dasa_years"
fullWidth
name="dasa_years"
value={data.dasa_years}
onChange={(e) => handleChange("dasa_years", e.target.value)}
placeholder="Enter Year"
variant="outlined"
type="number"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Month</label>
<TextField
id="dasa_months"
fullWidth
name="dasa_months"
value={data.dasa_months}
onChange={(e) => handleChange("dasa_months", e.target.value)}
placeholder="Enter Month"
variant="outlined"
type="number"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Dasa Day</label>
<TextField
id="dasa_days"
fullWidth
name="dasa_days"
value={data.dasa_days}
onChange={(e) => handleChange("dasa_days", e.target.value)}
placeholder="Enter Day"
variant="outlined"
type="number"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@ import {
Button, Button,
Checkbox, Checkbox,
ListItemText, ListItemText,
TextField,
} from "@mui/material"; } from "@mui/material";
import { usePartnerPreferenceMasters } from "../hooks/useMasters"; import { usePartnerPreferenceMasters } from "../hooks/useMasters";
import { useCityMasters, useSubCasteMasters } from "../hooks/useDependentMasters"; import { useCityMasters, useSubCasteMasters } from "../hooks/useDependentMasters";
@ -30,11 +31,30 @@ const PartnerPreferencesForm = ({
const subCasteQuery = useSubCasteMasters(data.castes); const subCasteQuery = useSubCasteMasters(data.castes);
const cityQuery = useCityMasters(data.states); const cityQuery = useCityMasters(data.states);
const ageRangeOptions = useMemo(() => { const ageOptions = useMemo(() => Array.from({ length: 53 }, (_, i) => i + 18), []);
const heightOptions = useMemo(() => {
const raw = masters; const raw = masters;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; return raw.heights || raw.height || [];
return raw.ageRange || raw.age_range || []; }, [masters]);
const maritalStatusOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
return raw.maritalStatus || raw.marital_status || [];
}, [masters]);
const starOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
return raw.stars || raw.star || [];
}, [masters]);
const employeeTypeOptions = useMemo(() => {
const raw = masters;
if (!raw) return [];
return raw.employeeTypes || raw.employee_type || [];
}, [masters]); }, [masters]);
const casteOptions = useMemo(() => { const casteOptions = useMemo(() => {
@ -72,6 +92,8 @@ const PartnerPreferencesForm = ({
return raw.annual_income || raw.annualIncome || []; return raw.annual_income || raw.annualIncome || [];
}, [masters]); }, [masters]);
const currencyOptions = useMemo(() => ["INR", "USD"], []);
const stateOptions = useMemo(() => { const stateOptions = useMemo(() => {
const raw = masters; const raw = masters;
if (!raw) return []; if (!raw) return [];
@ -96,8 +118,15 @@ const PartnerPreferencesForm = ({
const cityOptions = useMemo(() => { const cityOptions = useMemo(() => {
const raw = cityQuery.data; const raw = cityQuery.data;
if (!raw) return []; if (!raw) return [];
if (Array.isArray(raw)) return raw; if (Array.isArray(raw)) {
return raw.subCaste || raw.district || raw.data || []; const merged = raw.flatMap((entry) => {
if (!entry) return [];
if (Array.isArray(entry)) return entry;
return entry.district || entry.districts || entry.data || [];
});
return merged;
}
return raw.district || raw.districts || raw.data || [];
}, [cityQuery.data]); }, [cityQuery.data]);
useEffect(() => { useEffect(() => {
@ -107,12 +136,15 @@ const PartnerPreferencesForm = ({
const handleChange = (field, value) => { const handleChange = (field, value) => {
const arrayFields = new Set([ const arrayFields = new Set([
"castes", "castes",
"subCastes", "sub_castes",
"occupations", "occupations",
"educations", "educations",
"hobbies",
"states", "states",
"districts", "districts",
"marital_statuses",
"birth_stars",
"employee_types",
"currencies",
]); ]);
const nextValue = const nextValue =
arrayFields.has(field) && typeof value === "string" arrayFields.has(field) && typeof value === "string"
@ -122,8 +154,8 @@ const PartnerPreferencesForm = ({
const fieldsToClear = [field]; const fieldsToClear = [field];
if (field === "castes") { if (field === "castes") {
updates.subCastes = []; updates.sub_castes = [];
fieldsToClear.push("subCastes"); fieldsToClear.push("sub_castes");
} }
if (field === "states") { if (field === "states") {
updates.districts = []; updates.districts = [];
@ -201,215 +233,297 @@ const PartnerPreferencesForm = ({
<div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8"> <div className="w-full max-w-[1200px] mx-auto py-6 md:px-2 rounded-8">
<form noValidate autoComplete="off" style={{ padding: 16 }}> <form noValidate autoComplete="off" style={{ padding: 16 }}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-20 gap-y-10 mb-6">
{/* Age Range */} {/* 1. Age Range */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Age Range (From - To)</label>
Age Range{requiredMark} <div className="flex gap-2">
</label> <FormControl fullWidth variant="outlined" error={Boolean(errors.age_from)}>
<FormControl <InputLabel>From Age</InputLabel>
fullWidth <Select
variant="outlined" id="age_from"
error={Boolean(errors.ageRange)} name="age_from"
> value={data.age_from}
<InputLabel id="ageRange-label">Select Age Range</InputLabel> label="From Age"
<Select inputRef={inputRef}
labelId="ageRange-label" onChange={(e) => handleChange("age_from", e.target.value)}
label="Select Age Range"
name="ageRange"
value={data.ageRange}
onChange={(e) => handleChange("ageRange", e.target.value)}
inputRef={inputRef}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{ageRangeOptions.map((opt) => (
<MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
{opt.name || opt}
</MenuItem>
))}
</Select>
{errors.ageRange && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
> >
{errors.ageRange} {ageOptions.map((age) => (
</p> <MenuItem key={age} value={age}>{age}</MenuItem>
)} ))}
</FormControl> </Select>
{errors.age_from && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.age_from}
</p>
)}
</FormControl>
<FormControl fullWidth variant="outlined" error={Boolean(errors.age_to)}>
<InputLabel>To Age</InputLabel>
<Select
id="age_to"
name="age_to"
value={data.age_to}
label="To Age"
onChange={(e) => handleChange("age_to", e.target.value)}
>
{ageOptions.map((age) => (
<MenuItem key={age} value={age}>{age}</MenuItem>
))}
</Select>
{errors.age_to && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.age_to}
</p>
)}
</FormControl>
</div>
</div> </div>
{/* Caste */} {/* 2. Height Range */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Height Range (From - To)</label>
Caste{requiredMark} <div className="flex gap-2">
</label> <FormControl fullWidth variant="outlined" error={Boolean(errors.height_from)}>
<InputLabel>From Height</InputLabel>
<Select
id="height_from"
name="height_from"
value={data.height_from}
label="From Height"
onChange={(e) => handleChange("height_from", e.target.value)}
>
{heightOptions.map((h) => (
<MenuItem key={h.id} value={h.id}>{h.height_text}</MenuItem>
))}
</Select>
{errors.height_from && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.height_from}
</p>
)}
</FormControl>
<FormControl fullWidth variant="outlined" error={Boolean(errors.height_to)}>
<InputLabel>To Height</InputLabel>
<Select
id="height_to"
name="height_to"
value={data.height_to}
label="To Height"
onChange={(e) => handleChange("height_to", e.target.value)}
>
{heightOptions.map((h) => (
<MenuItem key={h.id} value={h.id}>{h.height_text}</MenuItem>
))}
</Select>
{errors.height_to && (
<p style={{ color: "#d32f2f", margin: "3px 14px 0 14px", fontSize: "0.75rem" }}>
{errors.height_to}
</p>
)}
</FormControl>
</div>
</div>
{/* 3. Marital Status */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Marital Status</label>
{renderMultiSelect({
name: "marital_statuses",
label: "Marital Status",
options: maritalStatusOptions,
value: data.marital_statuses,
getLabel: (opt) => opt.marital_status_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* 4. Birth Star */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Birth Star</label>
{renderMultiSelect({
name: "birth_stars",
label: "Birth Star",
options: starOptions,
value: data.birth_stars,
getLabel: (opt) => opt.star_name || opt.name,
getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading,
})}
</div>
{/* 5. Caste */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Caste</label>
{renderMultiSelect({ {renderMultiSelect({
name: "castes", name: "castes",
label: "Caste", label: "Caste",
options: casteOptions, options: casteOptions,
value: data.castes, value: data.castes,
getLabel: (opt) => opt.caste_name || opt.name || String(opt), getLabel: (opt) => opt.caste_name || opt.name,
getValue: (opt) => opt.id ?? opt, getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading, disabled: isPartnerMastersLoading,
})} })}
</div> </div>
{/* Sub Caste */} {/* 6. Sub-Sect */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Sub-Sect</label>
Sub Caste{requiredMark}
</label>
{renderMultiSelect({ {renderMultiSelect({
name: "subCastes", name: "sub_castes",
label: "Sub Caste", label: "Sub-Sect",
options: subCasteOptions, options: subCasteOptions,
value: data.subCastes, value: data.sub_castes,
getLabel: (opt) => getLabel: (opt) => opt.sub_caste_name || opt.name,
opt.sub_caste_name || opt.subCaste_name || opt.name || String(opt), getValue: (opt) => opt.id,
getValue: (opt) => opt.id ?? opt,
disabled: data.castes.length === 0 || subCasteQuery.isLoading, disabled: data.castes.length === 0 || subCasteQuery.isLoading,
})} })}
</div> </div>
{/* Occupation */} {/* 7. Qualification */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Qualification</label>
Occupation{requiredMark}
</label>
{renderMultiSelect({
name: "occupations",
label: "Occupation",
options: occupationOptions,
value: data.occupations,
getLabel: (opt) => opt.occupation_name || opt.name || String(opt),
getValue: (opt) => opt.id ?? opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* Qualification */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">
Qualification{requiredMark}
</label>
{renderMultiSelect({ {renderMultiSelect({
name: "educations", name: "educations",
label: "Qualification", label: "Qualification",
options: educationOptions, options: educationOptions,
value: data.educations, value: data.educations,
getLabel: (opt) => opt.education_name || opt.name || String(opt), getLabel: (opt) => opt.education_name || opt.name,
getValue: (opt) => opt.id ?? opt, getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading, disabled: isPartnerMastersLoading,
})} })}
</div> </div>
{/* Lifestyle and Hobbies */} {/* 8. Occupation */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Occupation</label>
Lifestyle & Hobbies{requiredMark}
</label>
{renderMultiSelect({ {renderMultiSelect({
name: "hobbies", name: "occupations",
label: "Lifestyle & Hobbies", label: "Occupation",
options: hobbyOptions, options: occupationOptions,
value: data.hobbies, value: data.occupations,
getLabel: (opt) => opt.hobby_name || opt.name || String(opt), getLabel: (opt) => opt.occupation_name || opt.name,
getValue: (opt) => opt.id ?? opt, getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading, disabled: isPartnerMastersLoading,
})} })}
</div> </div>
{/* Annual Income */} {/* 9. Employee Type */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Employee Type</label>
Annual Income{requiredMark} {renderMultiSelect({
</label> name: "employee_types",
<FormControl label: "Employee Type",
fullWidth options: employeeTypeOptions,
variant="outlined" value: data.employee_types,
error={Boolean(errors.annualIncome)} getLabel: (opt) => opt.employee_type_name || opt.name,
> getValue: (opt) => opt.id,
<InputLabel id="annualIncome-label"> disabled: isPartnerMastersLoading,
Select Annual Income })}
</InputLabel>
<Select
labelId="annualIncome-label"
label="Select Annual Income"
name="annualIncome"
value={data.annualIncome}
onChange={(e) => handleChange("annualIncome", e.target.value)}
disabled={isPartnerMastersLoading}
sx={{
"& .MuiSelect-select.Mui-disabled": {
cursor: "not-allowed",
},
}}
>
{annualIncomeOptions.map((opt) => (
<MenuItem key={opt.id ?? opt} value={opt.id ?? opt}>
{opt.annual_income_name || opt.name || opt}
</MenuItem>
))}
</Select>
{errors.annualIncome && (
<p
style={{
color: "#d32f2f",
margin: "3px 14px 0 14px",
fontSize: "0.75rem",
}}
>
{errors.annualIncome}
</p>
)}
</FormControl>
</div> </div>
{/* State */} {/* 10. Currency Type */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">Income Currency Type</label>
State{requiredMark} {renderMultiSelect({
</label> name: "currencies",
label: "Currency",
options: currencyOptions,
value: data.currencies,
getLabel: (opt) => opt,
getValue: (opt) => opt,
disabled: isPartnerMastersLoading,
})}
</div>
{/* 11. Income Range */}
{data.currencies.includes("INR") && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Annual Income (INR Range)</label>
<div className="flex gap-2">
<TextField
id="inr_from"
name="inr_from"
fullWidth
label="From (INR)"
value={data.inr_from}
error={Boolean(errors.inr_from)}
helperText={errors.inr_from}
onChange={(e) => handleChange("inr_from", e.target.value)}
/>
<TextField
id="inr_to"
name="inr_to"
fullWidth
label="To (INR)"
value={data.inr_to}
error={Boolean(errors.inr_to)}
helperText={errors.inr_to}
onChange={(e) => handleChange("inr_to", e.target.value)}
/>
</div>
</div>
)}
{data.currencies.includes("USD") && (
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Annual Income (USD Range)</label>
<div className="flex gap-2">
<TextField
id="usd_from"
name="usd_from"
fullWidth
label="From (USD)"
value={data.usd_from}
error={Boolean(errors.usd_from)}
helperText={errors.usd_from}
onChange={(e) => handleChange("usd_from", e.target.value)}
/>
<TextField
id="usd_to"
name="usd_to"
fullWidth
label="To (USD)"
value={data.usd_to}
error={Boolean(errors.usd_to)}
helperText={errors.usd_to}
onChange={(e) => handleChange("usd_to", e.target.value)}
/>
</div>
</div>
)}
{/* 12. State */}
<div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">State</label>
{renderMultiSelect({ {renderMultiSelect({
name: "states", name: "states",
label: "State", label: "State",
options: stateOptions, options: stateOptions,
value: data.states, value: data.states,
getLabel: (opt) => opt.state_name || opt.name || String(opt), getLabel: (opt) => opt.state_name || opt.name,
getValue: (opt) => opt.id ?? opt, getValue: (opt) => opt.id,
disabled: isPartnerMastersLoading, disabled: isPartnerMastersLoading,
})} })}
</div> </div>
{/* City */} {/* 13. City */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]"> <label className="text-gray-900 text-[15px]">City</label>
City{requiredMark}
</label>
{renderMultiSelect({ {renderMultiSelect({
name: "districts", name: "districts",
label: "City", label: "City",
options: cityOptions, options: cityOptions,
value: data.districts, value: data.districts,
getLabel: (opt) => getLabel: (opt) => opt.district_name || opt.city_name || opt.name,
opt.district_name || opt.city_name || opt.name || String(opt), getValue: (opt) => opt.id,
getValue: (opt) => opt.id ?? opt,
disabled: data.states.length === 0 || cityQuery.isLoading, disabled: data.states.length === 0 || cityQuery.isLoading,
})} })}
</div> </div>
</div> </div>
<Grid <Grid
item size={12}
xs={12}
style={{ style={{
marginTop: 40, marginTop: 40,
display: "flex", display: "flex",

View File

@ -126,13 +126,20 @@ const PersonalDetailsForm = ({ onSubmitStep, errors: externalErrors, onFieldChan
dispatch(updatePersonalDetails(updates)); dispatch(updatePersonalDetails(updates));
if (onFieldChange) onFieldChange(fieldsToClear); if (onFieldChange) onFieldChange(fieldsToClear);
if (!isEditMode) {
dispatch(clearAllStepsFrom(2));
}
if (localErrors[field]) { if (localErrors[field]) {
setLocalErrors(prev => ({ ...prev, [field]: "" })); setLocalErrors(prev => ({ ...prev, [field]: "" }));
} }
// Real-time password match validation
if (field === "confirmPassword" || field === "password") {
const p = field === "password" ? value : data.password;
const cp = field === "confirmPassword" ? value : data.confirmPassword;
if (p && cp && p !== cp) {
setLocalErrors(prev => ({ ...prev, confirmPassword: "Passwords do not match" }));
} else if (p && cp && p === cp) {
setLocalErrors(prev => ({ ...prev, confirmPassword: "" }));
}
}
}; };
const handleMobileSubmit = async () => { const handleMobileSubmit = async () => {
@ -198,21 +205,27 @@ const PersonalDetailsForm = ({ onSubmitStep, errors: externalErrors, onFieldChan
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!data.profile_for) newErrors.profile_for = "Required"; if (!data.profile_for) newErrors.profile_for = "Profile created for is required";
if (!data.gender) newErrors.gender = "Required"; if (!data.gender) newErrors.gender = "Gender is required";
if (!data.name) newErrors.name = "Required"; if (!data.name) newErrors.name = "Name is required";
if (!data.mobile) newErrors.mobile = "Required"; if (!data.mobile) newErrors.mobile = "Mobile number is required";
if (!data.email) newErrors.email = "Required"; if (!data.email) newErrors.email = "Email is required";
if (!isEditMode && !data.password) newErrors.password = "Required"; if (!isEditMode && !data.password) newErrors.password = "Password is required";
if (!isEditMode && data.password !== data.confirmPassword) newErrors.confirmPassword = "Passwords do not match"; if (!isEditMode && !data.confirmPassword) newErrors.confirmPassword = "Confirm Password is required";
if (!data.marital_status) newErrors.marital_status = "Required"; if (!isEditMode && data.password && data.confirmPassword && data.password !== data.confirmPassword) {
if (!data.height) newErrors.height = "Required"; newErrors.confirmPassword = "Passwords do not match";
if (!data.complexion) newErrors.complexion = "Required"; }
if (!data.physical_status) newErrors.physical_status = "Required"; if (!data.caste) newErrors.caste = "Caste is required";
if (!data.mother_language) newErrors.mother_language = "Required"; if (!data.sub_caste) newErrors.sub_caste = "Sub-Sect is required";
if (!data.do_you_speak_telugu && data.do_you_speak_telugu !== 0) newErrors.do_you_speak_telugu = "Required";
if (!data.inter_caste_parents && data.inter_caste_parents !== 0) newErrors.inter_caste_parents = "Required"; if (!data.marital_status) newErrors.marital_status = "Marital status is required";
if (data.known_languages.length === 0) newErrors.known_languages = "Required"; if (!data.height) newErrors.height = "Height is required";
if (!data.complexion) newErrors.complexion = "Complexion is required";
if (!data.physical_status) newErrors.physical_status = "Physical status is required";
if (!data.mother_language) newErrors.mother_language = "Mother tongue is required";
if (!data.do_you_speak_telugu && data.do_you_speak_telugu !== 0) newErrors.do_you_speak_telugu = "Telugu speaking status is required";
if (!data.inter_caste_parents && data.inter_caste_parents !== 0) newErrors.inter_caste_parents = "Inter-caste parents status is required";
if (data.known_languages.length === 0) newErrors.known_languages = "Known languages are required";
setLocalErrors(newErrors); setLocalErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
@ -260,21 +273,27 @@ const PersonalDetailsForm = ({ onSubmitStep, errors: externalErrors, onFieldChan
// 2. Perform general form validation // 2. Perform general form validation
if (!validateForm()) { if (!validateForm()) {
const newErrors = {}; const newErrors = {};
if (!data.profile_for) newErrors.profile_for = "Required"; if (!data.profile_for) newErrors.profile_for = "Profile created for is required";
if (!data.gender) newErrors.gender = "Required"; if (!data.gender) newErrors.gender = "Gender is required";
if (!data.name) newErrors.name = "Required"; if (!data.name) newErrors.name = "Name is required";
if (!data.mobile) newErrors.mobile = "Required"; if (!data.mobile) newErrors.mobile = "Mobile number is required";
if (!data.email) newErrors.email = "Required"; if (!data.email) newErrors.email = "Email is required";
if (!isEditMode && !data.password) newErrors.password = "Required"; if (!isEditMode && !data.password) newErrors.password = "Password is required";
if (!isEditMode && data.password !== data.confirmPassword) newErrors.confirmPassword = "Passwords do not match"; if (!isEditMode && !data.confirmPassword) newErrors.confirmPassword = "Confirm Password is required";
if (!data.marital_status) newErrors.marital_status = "Required"; if (!isEditMode && data.password && data.confirmPassword && data.password !== data.confirmPassword) {
if (!data.height) newErrors.height = "Required"; newErrors.confirmPassword = "Passwords do not match";
if (!data.complexion) newErrors.complexion = "Required"; }
if (!data.physical_status) newErrors.physical_status = "Required"; if (!data.caste) newErrors.caste = "Caste is required";
if (!data.mother_language) newErrors.mother_language = "Required"; if (!data.sub_caste) newErrors.sub_caste = "Sub-Sect is required";
if (!data.do_you_speak_telugu && data.do_you_speak_telugu !== 0) newErrors.do_you_speak_telugu = "Required";
if (!data.inter_caste_parents && data.inter_caste_parents !== 0) newErrors.inter_caste_parents = "Required"; if (!data.marital_status) newErrors.marital_status = "Marital status is required";
if (data.known_languages.length === 0) newErrors.known_languages = "Required"; if (!data.height) newErrors.height = "Height is required";
if (!data.complexion) newErrors.complexion = "Complexion is required";
if (!data.physical_status) newErrors.physical_status = "Physical status is required";
if (!data.mother_language) newErrors.mother_language = "Mother tongue is required";
if (!data.do_you_speak_telugu && data.do_you_speak_telugu !== 0) newErrors.do_you_speak_telugu = "Telugu speaking status is required";
if (!data.inter_caste_parents && data.inter_caste_parents !== 0) newErrors.inter_caste_parents = "Inter-caste parents status is required";
if (data.known_languages.length === 0) newErrors.known_languages = "Known languages are required";
toast.error("Please fill all mandatory fields"); toast.error("Please fill all mandatory fields");
scrollToError(newErrors); scrollToError(newErrors);
@ -581,8 +600,8 @@ const PersonalDetailsForm = ({ onSubmitStep, errors: externalErrors, onFieldChan
{/* 14. Caste / Community */} {/* 14. Caste / Community */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Caste / Community</label> <label className="text-gray-900 text-[15px]">Caste / Community{requiredMark}</label>
<FormControl fullWidth disabled={casteQuery.isLoading}> <FormControl fullWidth disabled={casteQuery.isLoading} error={Boolean(errors.caste)} id="caste">
<InputLabel>Select Caste</InputLabel> <InputLabel>Select Caste</InputLabel>
<Select <Select
value={data.caste} value={data.caste}
@ -593,13 +612,14 @@ const PersonalDetailsForm = ({ onSubmitStep, errors: externalErrors, onFieldChan
<MenuItem key={opt.id} value={opt.id}>{opt.caste_name}</MenuItem> <MenuItem key={opt.id} value={opt.id}>{opt.caste_name}</MenuItem>
))} ))}
</Select> </Select>
{errors.caste && <FormHelperText>{errors.caste}</FormHelperText>}
</FormControl> </FormControl>
</div> </div>
{/* 15. Sub-Sect */} {/* 15. Sub-Sect */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-gray-900 text-[15px]">Sub-Sect{requiredMark}</label> <label className="text-gray-900 text-[15px]">Sub-Sect{requiredMark}</label>
<FormControl fullWidth disabled={!data.caste || subCasteQuery.isLoading} id="sub_caste"> <FormControl fullWidth disabled={!data.caste || subCasteQuery.isLoading} error={Boolean(errors.sub_caste)} id="sub_caste">
<InputLabel>Select Sub-Sect</InputLabel> <InputLabel>Select Sub-Sect</InputLabel>
<Select <Select
value={data.sub_caste} value={data.sub_caste}
@ -610,6 +630,7 @@ const PersonalDetailsForm = ({ onSubmitStep, errors: externalErrors, onFieldChan
<MenuItem key={opt.id} value={opt.id}>{opt.sub_caste_name}</MenuItem> <MenuItem key={opt.id} value={opt.id}>{opt.sub_caste_name}</MenuItem>
))} ))}
</Select> </Select>
{errors.sub_caste && <FormHelperText>{errors.sub_caste}</FormHelperText>}
</FormControl> </FormControl>
</div> </div>

View File

@ -5,7 +5,7 @@ import { Navigation, Pagination } from "swiper/modules";
import "swiper/css"; import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
import { Edit2,Info } from 'lucide-react'; import { Edit2, Info } from 'lucide-react';
import { import {
Card, Card,
CardContent, CardContent,
@ -25,6 +25,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { usePreviewDetails } from "../hooks/usePreview"; import { usePreviewDetails } from "../hooks/usePreview";
import { isAuthenticated } from "../utills/auth"; import { isAuthenticated } from "../utills/auth";
import { usePersonalMasters, usePartnerPreferenceMasters } from "../hooks/useMasters";
import horoscopeImg from "../assets/images/horoscopeimg.png"; import horoscopeImg from "../assets/images/horoscopeimg.png";
import { updatePersonalDetails } from "../redux/registrationFormSlice"; import { updatePersonalDetails } from "../redux/registrationFormSlice";
@ -42,8 +43,7 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
if (pd) { if (pd) {
const images = pd.images || pd.profile_images; const images = pd.images || pd.profile_images;
if (images && images.length > 0) { if (images && images.length > 0) {
// Map images to ensure they have a consistent format for Redux const formattedImages = images.map(img =>
const formattedImages = images.map(img =>
typeof img === 'string' ? { url: img, preview: img } : img typeof img === 'string' ? { url: img, preview: img } : img
); );
dispatch(updatePersonalDetails({ profiles: formattedImages })); dispatch(updatePersonalDetails({ profiles: formattedImages }));
@ -66,124 +66,50 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
setOpenConfirmDialog(false); setOpenConfirmDialog(false);
}; };
const sections = previewData?.personal_details const renderValue = (value) => {
? [ if (value === null || value === undefined || value === "") return "-";
{
title: "Personal Details",
step: 1,
data: previewData.personal_details,
},
{
title: "Educational & Professional Details",
step: 2,
data: previewData.educational_details,
},
{
title: "Family Details",
step: 3,
data: previewData.family_details,
},
{
title: "Lifestyle & Habits",
step: 4,
data: previewData.lifestyle_details,
},
{
title: "Partner Preferences",
step: 5,
data: previewData.partner_preferences,
},
]
: [
{
title: 'Personal Details',
step: 1,
data: formData.personalDetails,
},
{
title: 'Educational & Professional Details',
step: 2,
data: formData.educationalDetails,
},
{
title: 'Family Details',
step: 3,
data: formData.familyDetails,
},
{
title: 'Lifestyle & Habits',
step: 4,
data: formData.lifestyleDetails,
},
{
title: 'Partner Preferences',
step: 5,
data: formData.partnerPreferences,
},
];
const renderValue = (key, value) => {
if (key === "profiles" || key === "profile" || key === "profile_images" || key === "images") {
const list = Array.isArray(value) ? value : (value ? [value] : []);
return (
<Box display="flex" gap={1} flexWrap="wrap">
{list.length > 0 ? (
list.map((imgObj, index) => {
const src =
typeof imgObj === "string"
? imgObj
: imgObj?.preview ||
imgObj?.url ||
(imgObj?.file ? URL.createObjectURL(imgObj.file) : null);
if (!src) return null;
return (
<img
key={index}
src={src}
alt="Profile"
style={{
width: "90px",
height: "90px",
objectFit: "cover",
borderRadius: "50%",
border: "1px solid #ccc",
// marginLeft: "-20px",
}}
/>
);
})
) : (
<Box display="flex" alignItems="center" gap={1} sx={{ color: "#888" }}>
<Info size={16} />
<Typography>No photos uploaded</Typography>
</Box>
)}
</Box>
);
}
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length === 0) return "-"; if (value.length === 0) return "-";
if (typeof value[0] === "object") {
return value.map((item, idx) => (
<div key={idx}>{JSON.stringify(item)}</div>
));
}
return value.join(", "); return value.join(", ");
} }
if (typeof value === "boolean" || value === 1 || value === "1" || value === 0 || value === "0") {
if (value && typeof value === "object") { if (value === true || value === 1 || value === "1") return "Yes";
return "-"; if (value === false || value === 0 || value === "0") return "No";
} }
return value;
return value || "-";
}; };
const DetailRow = ({ label, value }) => (
<Box display="grid" gridTemplateColumns="45% 55%" gap={1} mb={0.8} alignItems="start">
<Typography color="text.secondary" sx={{ fontWeight: 500, fontSize: '0.875rem' }}>
{label}:
</Typography>
<Typography variant="body2" fontWeight={600} sx={{ wordBreak: 'break-word' }}>
{renderValue(value)}
</Typography>
</Box>
);
const SectionTitle = ({ title, mt = 2 }) => (
<Typography
variant="subtitle1"
sx={{
fontWeight: 'bold',
fontSize: '18px',
color: '#111827',
mt: mt,
mb: 2
}}
>
{title}
</Typography>
);
const renderChartGrid = (getDataForCell, title) => { const renderChartGrid = (getDataForCell, title) => {
const renderCell = (i) => { const renderCell = (i) => {
const items = getDataForCell(i); const items = getDataForCell(i);
const label = Array.isArray(items) ? items.join(', ') : ''; const label = Array.isArray(items) ? items.join(', ') : '';
return ( return (
<Tooltip key={i} title={label} arrow placement="top" disableHoverListener={!items || items.length === 0}> <Tooltip key={i} title={label} arrow placement="top" disableHoverListener={!items || items.length === 0}>
<Box <Box
@ -204,12 +130,12 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
cursor: items && items.length > 0 ? 'help' : 'default' cursor: items && items.length > 0 ? 'help' : 'default'
}} }}
> >
{items && items.length > 2 ? ( {items && items.length > 2 ? (
<Box> <Box>
{items.slice(0, 2).join(', ')} {items.slice(0, 2).join(', ')}
<Box component="span" sx={{ color: 'primary.main', display: 'block', fontSize:'0.6rem' }}>+{items.length - 2}</Box> <Box component="span" sx={{ color: 'primary.main', display: 'block', fontSize: '0.6rem' }}>+{items.length - 2}</Box>
</Box> </Box>
) : label} ) : label}
</Box> </Box>
</Tooltip> </Tooltip>
); );
@ -217,315 +143,299 @@ const PreviewScreen = ({ onEdit, onSubmit }) => {
return ( return (
<Box> <Box>
<Typography variant="subtitle2" align="center" gutterBottom sx={{ fontWeight: 600 }}>{title}</Typography> <Typography variant="subtitle2" align="center" gutterBottom sx={{ fontWeight: 600, color: '#CC1F1F' }}>{title}</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0.5, maxWidth: '300px', mx: 'auto', p: 1, bgcolor: '#fff3e0', borderRadius: 2 }}> <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0.5, maxWidth: '300px', mx: 'auto', p: 1, bgcolor: '#fff3e0', borderRadius: 2 }}>
{renderCell(1)} {renderCell(2)} {renderCell(3)} {renderCell(4)} {renderCell(1)} {renderCell(2)} {renderCell(3)} {renderCell(4)}
{renderCell(5)} {renderCell(5)}
<Box sx={{ gridColumn: 'span 2', gridRow: 'span 2', bgcolor: '#fff', border: '1px solid #eee', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'0.7rem', fontWeight:'bold', color:'#aaa', overflow: 'hidden' }}> <Box sx={{ gridColumn: 'span 2', gridRow: 'span 2', bgcolor: '#fff', border: '1px solid #eee', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', fontWeight: 'bold', color: '#aaa', overflow: 'hidden' }}>
<img src={horoscopeImg} alt={title} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> <img src={horoscopeImg} alt={title} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
</Box>
{renderCell(6)}
{renderCell(7)} {renderCell(8)}
{renderCell(9)} {renderCell(10)} {renderCell(11)} {renderCell(12)}
</Box> </Box>
{renderCell(6)}
{renderCell(7)} {renderCell(8)}
{renderCell(9)} {renderCell(10)} {renderCell(11)} {renderCell(12)}
</Box>
</Box> </Box>
); );
}; };
return ( const { data: pm } = usePersonalMasters();
<Box p={3} sx={{maxWidth:"1400px", width:"100%"}} mx="auto" > const { data: ppm } = usePartnerPreferenceMasters();
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 my-10'>
const getNamesFromIds = (ids, options, labelKey = 'name', valueKey = 'id') => {
if (!ids || (Array.isArray(ids) && ids.length === 0)) return null;
if (!options) return Array.isArray(ids) ? ids.join(", ") : ids;
const idArray = Array.isArray(ids) ? ids : String(ids).split(",").map(v => v.trim());
if (idArray.length === 0) return null;
// Check if the first element is already a name
if (idArray[0] && typeof idArray[0] === 'string' && isNaN(Number(idArray[0]))) {
return idArray.join(", ");
}
const names = idArray.map(id => {
const found = options.find(opt => String(opt[valueKey]) === String(id));
if (found) return found[labelKey];
return id;
});
return names.filter(Boolean).join(", ");
};
const pInfo = previewData?.personal_details || formData.personalDetails;
const eInfo = previewData?.educational_details || formData.educationalDetails;
const fInfo = previewData?.family_details || formData.familyDetails;
const lInfo = previewData?.lifestyle_details || formData.lifestyleDetails;
// Helper to check if partner preferences from API are actually empty
const isPartnerApiDataEmpty = (pp) => {
if (!pp) return true;
// Check for common fields that should have values if data was saved
const hasData = pp.preferred_age_from || pp.age_from || pp.preferred_marital_status_ids?.length > 0 || pp.marital_statuses?.length > 0 || pp.preferred_castes_ids?.length > 0 || pp.castes?.length > 0;
return !hasData;
};
const prInfo = (!isPartnerApiDataEmpty(previewData?.partner_preferences)) ? previewData.partner_preferences :
(!isPartnerApiDataEmpty(previewData?.preferred_details)) ? previewData.preferred_details :
(!isPartnerApiDataEmpty(previewData?.partner_details)) ? previewData.partner_details :
(formData.partnerPreferences || {});
return (
<Box p={3} sx={{ maxWidth: "1400px", width: "100%" }} mx="auto">
<div className='grid grid-cols-1 md:grid-cols-2 gap-6 my-10'>
{isLoading && ( {isLoading && (
<Box <Box py={4} display="flex" justifyContent="center" sx={{ color: "#666", gridColumn: 'span 2' }}>
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#666" }}
>
Loading preview... Loading preview...
</Box> </Box>
)} )}
{isError && ( {isError && (
<Box <Box py={4} display="flex" justifyContent="center" sx={{ color: "#d32f2f", gridColumn: 'span 2' }}>
py={4}
display="flex"
justifyContent="center"
sx={{ color: "#d32f2f" }}
>
Failed to load preview. Failed to load preview.
</Box> </Box>
)} )}
{!isLoading && {!isLoading && (
sections.map((section) => ( <>
{/* 1. Personal Details */}
<Card key={section.title} variant="outlined" <Card variant="outlined" sx={{ borderRadius: 2 }}>
sx={{ borderRadius: 2,
// background:"#fff5ed"
}}>
<CardHeader <CardHeader
title={ title={<Typography variant="h6" fontWeight="bold">Personal Details</Typography>}
<Typography variant="h6" fontWeight="bold"> action={<IconButton color="primary" onClick={() => onEdit(1)} size="large"><Edit2 size={20} /></IconButton>}
{section.title} sx={{ padding: "15px 15px", background: "#f5fbff" }}
</Typography>
}
action={
<IconButton
aria-label="edit"
color="primary"
onClick={() => onEdit(section.step)}
size="large"
>
<Edit2 size={20} />
</IconButton>
}
sx={{padding:"15px 15px", background:"#f5fbff" }}
/> />
{/* <Divider /> */} <CardContent>
<CardContent sx={{ pt: 1, }}> <Box mb={2}>
{Object.entries(section.data || {}).map(([key, value]) => { {/* Photos Preview */}
// Filter out ID fields <Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1, fontSize: '0.75rem' }}>Profile Photos:</Typography>
const hiddenFields = [ <Box display="flex" gap={1} flexWrap="wrap">
"id", {(pInfo.images || pInfo.profile_images || pInfo.profiles || []).map((imgObj, index) => {
"created_at", const src = typeof imgObj === "string" ? imgObj : imgObj?.preview || imgObj?.url;
"updated_at", return src ? (
"phone_number_visibility", <img key={index} src={src} alt="Profile" style={{ width: "80px", height: "80px", objectFit: "cover", borderRadius: "50%", border: "1px solid #ccc" }} />
"chat_alert_notification", ) : null;
"chat_protection", })}
"profile_photo_protect", {!(pInfo.images || pInfo.profile_images || pInfo.profiles || []).length && <Typography variant="caption">No photos</Typography>}
"call_protection", </Box>
"match_alert_preference", </Box>
"who_can_message", <DetailRow label="Profile Created For" value={pInfo.profile_for} />
"who_can_message_categories", <DetailRow label="Gender" value={pInfo.gender} />
"user_status", <DetailRow label="Name" value={pInfo.name} />
]; <DetailRow label="Mobile Number" value={pInfo.mobile || pInfo.mobileNumber || pInfo.mobile_number} />
if (hiddenFields.includes(key) || key.endsWith('_id') || key.endsWith('Id') || key.endsWith('_ids') || key.endsWith('Ids')) { <DetailRow label="Email Id" value={pInfo.email || pInfo.emailId || pInfo.email_id} />
return null; <DetailRow label="Marital Status" value={getNamesFromIds(pInfo.marital_status, pm?.marital_status, 'marital_status_name')} />
} <DetailRow label="Height" value={getNamesFromIds(pInfo.height, pm?.heights, 'height_text')} />
<DetailRow label="Weight" value={pInfo.weight} />
// Handle Horoscope Chart (Server Data) <DetailRow label="Complexion" value={getNamesFromIds(pInfo.complexion, pm?.complexion, 'complexion_name')} />
if (key === 'horoscope' && value && typeof value === 'object') { <DetailRow label="Physical Status" value={getNamesFromIds(pInfo.physical_status, pm?.physicalStatus, 'physical_status_name')} />
return ( <DetailRow label="Religion" value={getNamesFromIds(pInfo.religion, pm?.religion, 'religion_name')} />
<Box key={key} sx={{ width: '100%', mt: 2, mb: 2, borderTop: '1px solid #e0e0e0', pt: 2, gridColumn: '1 / -1' }}> <DetailRow label="Caste / Community" value={getNamesFromIds(pInfo.caste, pm?.caste, 'caste_name')} />
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>Horoscope Details</Typography> <DetailRow label="Sub-Sect" value={getNamesFromIds(pInfo.sub_caste, pm?.subCaste, 'sub_caste_name')} />
<Grid container spacing={4} justifyContent="center"> <DetailRow label="Willing to marry from same sub-sect" value={pInfo.willing_to_marry} />
<Grid item xs={12} md={6}> <DetailRow label="Inter-Caste Parents" value={pInfo.inter_caste_parents === 1 || pInfo.inter_caste_parents === "1" ? "Yes" : "No"} />
{renderChartGrid((i) => { { (pInfo.inter_caste_parents === 1 || pInfo.inter_caste_parents === "1") && <DetailRow label="Inter-Caste Parents Details" value={pInfo.inter_caste_parents_details} />}
const val = value[`graha_${i}`]; <DetailRow label="Gothram" value={pInfo.gothram} />
return val ? val.split(',') : []; <DetailRow label="Do you speak Telugu" value={pInfo.do_you_speak_telugu === 1 || pInfo.do_you_speak_telugu === "1" ? "Yes" : "No"} />
}, 'Rasi')} <DetailRow label="About" value={pInfo.about_us} />
</Grid> <DetailRow label="Language Spoken" value={pInfo.known_languages} />
<Grid item xs={12} md={6}> <DetailRow label="Mother Tongue" value={getNamesFromIds(pInfo.mother_language, pm?.languages, 'language')} />
{renderChartGrid((i) => {
const val = value[`amsam_${i}`];
return val ? val.split(',') : [];
}, 'Navamsam')}
</Grid>
</Grid>
</Box>
);
}
// Handle Horoscope Chart (Redux Data)
if (key === 'graha' && value) {
return (
<Box key={key} sx={{ width: '100%', mt: 2, mb: 2, gridColumn: '1 / -1' }}>
{renderChartGrid((i) => value[i] || [], 'Rasi')}
</Box>
);
}
if (key === 'amsam' && value) {
return (
<Box key={key} sx={{ width: '100%', mt: 2, mb: 2, gridColumn: '1 / -1' }}>
{renderChartGrid((i) => value[i] || [], 'Navamsam')}
</Box>
);
}
// Handle brothers and sisters arrays specifically
if ((key === 'brothers' || key === 'sisters') && Array.isArray(value) && value.length > 0) {
return (
<Box key={key} sx={{ py: 2, borderBottom: "1px solid #e0e0e0" }}>
<Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1 }}>
{key === 'brothers' ? 'Brothers Details' : 'Sisters Details'}
</Typography>
<style>
{`
.custom-swiper-${key} .swiper-button-next,
.custom-swiper-${key} .swiper-button-prev {
color: #d32f2f;
width: 25px;
height: 25px;
overflow: 'hidden';
padding:5px;
display: 'flex';
align-items: 'center';
justify-content: 'center';
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.custom-swiper-${key} .swiper-button-next::after,
.custom-swiper-${key} .swiper-button-prev::after {
font-size: 8px;
font-weight: bold;
width:20px;
}
.custom-swiper-${key} .swiper-pagination-bullet-active {
background-color: #d32f2f;
}
`}
</style>
<Swiper
className={`custom-swiper-${key}`}
modules={[Navigation, Pagination]}
spaceBetween={15}
slidesPerView={1}
navigation
pagination={{ clickable: true }}
style={{ padding: "4px 4px 30px 4px" }}
>
{value.map((sibling, idx) => (
<SwiperSlide key={idx}>
<Card variant="outlined"
sx={{ bgcolor: '#fff', height: '100%' }}>
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
{Object.entries(sibling).map(([sKey, sVal]) => {
if (sKey === 'id' || sKey.endsWith('_id') || sKey.endsWith('Id') || sKey === 'created_at' || sKey === 'updated_at') return null;
let displayValue = sVal;
if (sKey === 'haveChildrens' || sKey === 'have_childrens' || sKey === 'hasChildren' || sKey === 'has_children') {
if (sVal === true || sVal === 1 || sVal === '1' || sVal === 'Yes') {
displayValue = 'Yes';
} else if (sVal === false || sVal === 0 || sVal === '0' || sVal === 'No') {
displayValue = 'No';
}
}
return (
<Box key={sKey} display="grid" gridTemplateColumns="45% 55%" gap={1} mb={0.8} alignItems="start">
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 500 }}>
{sKey
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase())
.trim()}
</Typography>
<Typography variant="body2" fontWeight={600} sx={{ wordBreak: 'break-word' }}>
{displayValue ?? '-'}
</Typography>
</Box>
)})}
</CardContent>
</Card>
</SwiperSlide>
))}
</Swiper>
</Box>
);
}
// Convert camelCase or camel_Snake_case to readable words
const formattedKey = key
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.replace(/\b\w/g, (l) => l.toUpperCase())
.trim();
const content = renderValue(key, value);
return (
<Box
key={key}
py={0.7}
// borderBottom="1px solid #e0e0e0"
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr",
sm: "1fr 1fr",
},
gap: "10px",
alignItems: "center",
}}
>
<Typography color="text.secondary" sx={{ fontWeight: 500 }}>
{formattedKey}:
</Typography>
{/* Special Case: Profiles Image Preview */}
{key === "profiles" || key === "profile" || key === "profile_images" || key === "images" ? (
content
) : value ? (
<Typography
sx={{
fontWeight: 600,
wordBreak: "break-word",
textAlign: "left",
}}
>
{content}
</Typography>
) : (
<Box
display="flex"
alignItems="center"
gap={1}
sx={{ color: "#888", fontStyle: "italic" }}
>
<Info size={16} />
<Typography>No data available</Typography>
</Box>
)}
</Box>
);
})}
</CardContent> </CardContent>
</Card> </Card>
))}
</div> {/* 2. Educational & Professional Details */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Educational & Professional Details</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(2)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Highest Qualification" value={getNamesFromIds(eInfo.education, ppm?.education || ppm?.educations, 'education_name')} />
<DetailRow label="Field of Study" value={getNamesFromIds(eInfo.study_field, ppm?.study_fields || ppm?.studyFields, 'study_field_name')} />
<DetailRow label="About" value={eInfo.education_detail || eInfo.educationDetail || eInfo.about} />
<DetailRow label="College Name" value={eInfo.college_name} />
<DetailRow label="Employee type" value={getNamesFromIds(eInfo.employee_type, ppm?.employeeTypes || ppm?.employee_type, 'employee_type_name')} />
<DetailRow label="Occupation" value={getNamesFromIds(eInfo.occupation, ppm?.occupation || ppm?.occupations, 'occupation_name')} />
<DetailRow label="Occupation Details" value={eInfo.occupation_detail || eInfo.occupationDetail} />
<DetailRow label="Organization Name" value={eInfo.company_name || eInfo.organizationName || eInfo.companyName} />
<DetailRow label="Income Currency Type" value={eInfo.income_currency} />
<DetailRow label="Annual Income" value={eInfo.annual_income} />
<SectionTitle title="Work Location" />
<DetailRow label="Country" value={eInfo.work_country_name || eInfo.work_country} />
<DetailRow label="State" value={eInfo.work_state_name || eInfo.work_state} />
<DetailRow label="City" value={eInfo.work_district_name || eInfo.work_city} />
<DetailRow label="Address" value={eInfo.work_location || eInfo.address} />
</CardContent>
</Card>
<Grid item xs={12} sx={{display:"flex", justifyContent:"center"}}> {/* 3. Family Details */}
<Button <Card variant="outlined" sx={{ borderRadius: 2 }}>
variant="contained" <CardHeader
color="success" title={<Typography variant="h6" fontWeight="bold">Family Details</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(3)} size="large"><Edit2 size={20} /></IconButton>}
size="large" sx={{ padding: "15px 15px", background: "#f5fbff" }}
onClick={handleConfirmSubmit} />
sx={{ borderRadius: 2 }} <CardContent>
> <DetailRow label="Father Name" value={fInfo.father_name || fInfo.fatherName} />
Submit full Completed Data <DetailRow label="Father Occupation" value={fInfo.father_occupation || fInfo.father_occupational || fInfo.fatherOccupation || fInfo.fatherOccupational || fInfo.father_occupation_name} />
</Button> <DetailRow label="Mother Name" value={fInfo.mother_name || fInfo.motherName} />
</Grid> <DetailRow label="Mother Occupation" value={fInfo.mother_occupation || fInfo.mother_occupational || fInfo.motherOccupation || fInfo.motherOccupational || fInfo.mother_occupation_name} />
<Dialog {/* Siblings */}
open={openConfirmDialog} {['brothers', 'sisters'].map(type => (
onClose={handleCancelSubmit} (fInfo[type] && fInfo[type].length > 0) && (
aria-labelledby="alert-dialog-title" <Box key={type} mt={2} mb={2}>
aria-describedby="alert-dialog-description" <Typography color="text.secondary" sx={{ fontWeight: 600, mb: 1, textTransform: 'capitalize' }}>{type} Details:</Typography>
<Swiper modules={[Navigation, Pagination]} spaceBetween={10} slidesPerView={1} navigation pagination={{ clickable: true }} style={{ paddingBottom: '30px' }}>
{fInfo[type].map((sib, i) => (
<SwiperSlide key={i}>
<Card variant="outlined" sx={{ p: 2, bgcolor: '#f9f9f9' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>{type.slice(0, -1)} {i + 1}</Typography>
<DetailRow label="Name" value={sib.name} />
<DetailRow label={type === 'brothers' ? 'Brother Type' : 'Sister Type'} value={sib.type} />
<DetailRow label="Occupation" value={sib.occupation || sib.occupational || sib.occupation_name} />
<DetailRow label="Marital Status" value={sib.marital_status || sib.maritalStatus || sib.marital_status_id} />
<DetailRow label="Has Children" value={sib.has_children || sib.hasChildren || sib.thereHaveChildren} />
<DetailRow label="Additional Details" value={sib.details || sib.additionalDetails || sib.additional_details} />
</Card>
</SwiperSlide>
))}
</Swiper>
</Box>
)
))}
<DetailRow label="Family Status" value={fInfo.family_status || fInfo.familyStatus} />
<DetailRow label="Native Place" value={fInfo.native_place || fInfo.nativePlace} />
<SectionTitle title="Location Living" />
<DetailRow label="Country Living" value={fInfo.family_country_name || fInfo.living_country_name} />
<DetailRow label="Residing State" value={fInfo.family_state_name || fInfo.living_state_name} />
<DetailRow label="Residing City" value={fInfo.family_district_name || fInfo.family_city || fInfo.living_city_name} />
<DetailRow label="Address" value={fInfo.address} />
<DetailRow label="Expectations / Requirements Details" value={fInfo.expectation_details || fInfo.expectationDetails} />
<DetailRow label="Willing to go abroad" value={fInfo.willing_to_go_abroad || fInfo.willingToGoAbroad} />
</CardContent>
</Card>
{/* 4. Lifestyle & Horoscope */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">Lifestyle & Horoscope</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(4)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Time of Birth" value={lInfo.time_of_birth_formated || lInfo.tob} />
<DetailRow label="Place of Birth" value={lInfo.place_of_birth || lInfo.placeOfBirth} />
<DetailRow label="Birth Star" value={lInfo.star_name || lInfo.star} />
<DetailRow label="Padham" value={lInfo.patham} />
<DetailRow label="Rasi" value={lInfo.raasi_name || lInfo.raasi} />
<DetailRow label="Lagnam" value={lInfo.lagnam_name || lInfo.lagnam} />
<DetailRow label="Panjangam" value={lInfo.panjangam_type} />
<DetailRow label="Date of Birth" value={lInfo.date_of_birth_formated || lInfo.dob} />
<DetailRow label="Dasa Balance" value={lInfo.dasa_balance} />
<DetailRow label="Dasa Duration" value={(lInfo.dasa_years || lInfo.dasa_months || lInfo.dasa_days) ? `${lInfo.dasa_years || 0} Years, ${lInfo.dasa_months || 0} Months, ${lInfo.dasa_days || 0} Days` : "-"} />
{/* Horoscope Charts */}
{lInfo.horoscope && (
<Box mt={3}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 6 }}>
{renderChartGrid((i) => lInfo.horoscope[`graha_${i}`]?.split(',') || (lInfo.graha && lInfo.graha[i]) || [], 'Rasi')}
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
{renderChartGrid((i) => lInfo.horoscope[`amsam_${i}`]?.split(',') || (lInfo.amsam && lInfo.amsam[i]) || [], 'Navamsam')}
</Grid>
</Grid>
</Box>
)}
</CardContent>
</Card>
{/* 5. What am I looking for in a Partner */}
<Card variant="outlined" sx={{ borderRadius: 2 }}>
<CardHeader
title={<Typography variant="h6" fontWeight="bold">What am I looking for in a Partner</Typography>}
action={<IconButton color="primary" onClick={() => onEdit(5)} size="large"><Edit2 size={20} /></IconButton>}
sx={{ padding: "15px 15px", background: "#f5fbff" }}
/>
<CardContent>
<DetailRow label="Age Range" value={(prInfo.preferred_age_from || prInfo.age_from) && (prInfo.preferred_age_to || prInfo.age_to) ? `${prInfo.preferred_age_from || prInfo.age_from} - ${prInfo.preferred_age_to || prInfo.age_to}` : "-"} />
<DetailRow label="Height" value={(getNamesFromIds(prInfo.preferred_height_from_id || prInfo.height_from, ppm?.heights, 'height_text') && getNamesFromIds(prInfo.preferred_height_to_id || prInfo.height_to, ppm?.heights, 'height_text')) ? `${getNamesFromIds(prInfo.preferred_height_from_id || prInfo.height_from, ppm?.heights, 'height_text')} - ${getNamesFromIds(prInfo.preferred_height_to_id || prInfo.height_to, ppm?.heights, 'height_text')}` : "-"} />
<DetailRow label="Marital Status" value={getNamesFromIds(prInfo.preferred_marital_statuses || prInfo.marital_statuses || prInfo.preferred_marital_status || prInfo.marital_status, ppm?.maritalStatus || ppm?.marital_status, 'marital_status_name')} />
<DetailRow label="Birth Star" value={getNamesFromIds(prInfo.preferred_birth_stars || prInfo.birth_stars || prInfo.preferred_birth_star || prInfo.birth_star, ppm?.stars || ppm?.star, 'star_name')} />
<DetailRow label="Caste" value={getNamesFromIds(prInfo.preferred_castes || prInfo.castes || prInfo.preferred_caste || prInfo.caste, ppm?.caste || ppm?.castes, 'caste_name')} />
<DetailRow label="Sub - Sect" value={getNamesFromIds(prInfo.preferred_sub_castes || prInfo.sub_castes || prInfo.preferred_sub_caste || prInfo.sub_caste, ppm?.sub_caste || ppm?.sub_castes, 'sub_caste_name')} />
<DetailRow label="Qualification" value={getNamesFromIds(prInfo.preferred_educations || prInfo.educations || prInfo.preferred_education || prInfo.education, ppm?.education || ppm?.educations, 'education_name')} />
<DetailRow label="Occupation" value={getNamesFromIds(prInfo.preferred_occupations || prInfo.occupations || prInfo.preferred_occupation || prInfo.occupation, ppm?.occupation || ppm?.occupations, 'occupation_name')} />
<DetailRow label="Employee Type" value={getNamesFromIds(prInfo.preferred_employee_types || prInfo.employee_types || prInfo.preferred_employee_type || prInfo.employee_type, ppm?.employeeTypes || ppm?.employee_type, 'employee_type_name')} />
<SectionTitle title="Prefer Annual Income" />
{(prInfo.preferred_currencies || prInfo.currencies || []).includes("INR") && (
<DetailRow label="Prefer Annual Income (INR)" value={(prInfo.preferred_inr_from || prInfo.inr_from || prInfo.preferred_inr_range) ? `From : ${prInfo.preferred_inr_from || prInfo.inr_from || ""} \nTo : ${prInfo.preferred_inr_to || prInfo.inr_to || ""}` : "-"} />
)}
{(prInfo.preferred_currencies || prInfo.currencies || []).includes("USD") && (
<DetailRow label="Prefer Annual Income (USD)" value={(prInfo.preferred_usd_from || prInfo.usd_from || prInfo.preferred_usd_range) ? `From : ${prInfo.preferred_usd_from || prInfo.usd_from || ""} \nTo : ${prInfo.preferred_usd_to || prInfo.usd_to || ""}` : "-"} />
)}
<DetailRow label="State" value={getNamesFromIds(prInfo.preferred_states || prInfo.states || prInfo.preferred_state || prInfo.state, ppm?.states, 'state_name')} />
<DetailRow label="City" value={getNamesFromIds(prInfo.preferred_districts || prInfo.districts || prInfo.preferred_district || prInfo.district, ppm?.districts, 'district_name')} />
</CardContent>
</Card>
</>
)}
</div>
<Grid size={12} sx={{ display: "flex", justifyContent: "center" }}>
<Button
variant="contained"
color="success"
size="large"
onClick={handleConfirmSubmit}
sx={{ borderRadius: 2 }}
> >
<DialogTitle id="alert-dialog-title"> Submit Full Completed Data
{"Confirm Submission"} </Button>
</DialogTitle> </Grid>
<DialogContent>
<DialogContentText id="alert-dialog-description"> <Dialog
Once you submit your details, you will not be able to edit the following fields: Place of Birth, Date of Birth, Rasi and Navamsam. open={openConfirmDialog}
</DialogContentText> onClose={handleCancelSubmit}
</DialogContent> aria-labelledby="alert-dialog-title"
<DialogActions> aria-describedby="alert-dialog-description"
<Button onClick={handleCancelSubmit}>Cancel</Button> >
<Button onClick={handleProceedSubmit} autoFocus> <DialogTitle id="alert-dialog-title">{"Confirm Submission"}</DialogTitle>
OK <DialogContent>
</Button> <DialogContentText id="alert-dialog-description">
</DialogActions> Once you submit your details, you will not be able to edit the following fields: Place of Birth, Date of Birth, Rasi and Navamsam.
</Dialog> </DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCancelSubmit}>Cancel</Button>
<Button onClick={handleProceedSubmit} autoFocus>OK</Button>
</DialogActions>
</Dialog>
</Box> </Box>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@ -84,15 +84,24 @@ const STEP_FIELD_ORDER = {
"familyStatus", "familyStatus",
"nativePlace", "nativePlace",
], ],
4: ["diets", "hobbies", "dob", "tob", "placeOfBirth"], 4: ["dob", "tob", "placeOfBirth", "raasi", "star", "patham", "lagnam", "panjangam_type", "dasa_balance", "dasa_years", "dasa_months", "dasa_days"],
5: [ 5: [
"ageRange", "age_from",
"age_to",
"height_from",
"height_to",
"marital_statuses",
"birth_stars",
"castes", "castes",
"subCastes", "sub_castes",
"occupations",
"educations", "educations",
"hobbies", "occupations",
"annualIncome", "employee_types",
"currencies",
"inr_from",
"inr_to",
"usd_from",
"usd_to",
"states", "states",
"districts", "districts",
], ],
@ -392,18 +401,23 @@ const [completedSteps, setCompletedSteps] = useState([]);
if (!firstKey) return; if (!firstKey) return;
setTimeout(() => { setTimeout(() => {
const element = document.getElementById(firstKey) || const element =
document.querySelector(`[name="${firstKey}"]`) || document.getElementById(firstKey) ||
document.querySelector(`[aria-labelledby~="${firstKey}-label"]`); document.querySelector(`[name="${firstKey}"]`) ||
document.querySelector(`[id^="${firstKey}"]`) ||
document.querySelector(`[name^="${firstKey}"]`) ||
document.querySelector(`[aria-labelledby~="${firstKey}-label"]`);
if (element) { if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" }); element.scrollIntoView({ behavior: "smooth", block: "center" });
// Try to find a focusable element within the container // Try to find a focusable element within the container
const focusable = element.querySelector('input:not([type="hidden"]), select, textarea, [role="combobox"], [role="button"]') || element; const focusable =
element.querySelector('input:not([type="hidden"]), select, textarea, [role="combobox"], [role="button"]') ||
element;
if (focusable && typeof focusable.focus === "function") { if (focusable && typeof focusable.focus === "function") {
focusable.focus(); setTimeout(() => focusable.focus(), 300);
} }
} }
}, 100); }, 100);
@ -460,32 +474,36 @@ const [completedSteps, setCompletedSteps] = useState([]);
useEffect(() => { useEffect(() => {
const processData = async () => { const processData = async () => {
if (personalDetailsData?.status === "success" && personalDetailsData?.personal_details) { const isSuccess = personalDetailsData?.status === "success" || personalDetailsData?.success === true;
if (isSuccess && personalDetailsData?.personal_details) {
const pd = personalDetailsData.personal_details; const pd = personalDetailsData.personal_details;
setIsStep1Update(true); setIsStep1Update(true);
const rawImages = pd.profile_images || pd.images || []; const rawImages = pd.profile_images || pd.profiles || pd.images || [];
const mappedImages = await Promise.all( const mappedImages = await Promise.all(
rawImages.map(async (url, index) => { rawImages.map(async (img, index) => {
const imageUrl = typeof url === "string" ? url : url?.url; const originalUrl = typeof img === "string" ? img : (img.url || img.preview || img.image_url);
if (!originalUrl) return null;
// Rewrite URL to use proxy in development to avoid CORS
const imageUrl = (import.meta.env.DEV && originalUrl.startsWith('https://www.thirukalyanam.amrithaa.net/backend'))
? originalUrl.replace('https://www.thirukalyanam.amrithaa.net/backend', '/backend')
: originalUrl;
const fileName = (typeof img === "object" && img.name) || `image-${index}.jpg`;
let file = null; let file = null;
let mimeType = "image/jpeg"; let mimeType = "image/jpeg";
let fileName = `image-${index}.jpg`;
try { try {
if (imageUrl) { const response = await fetch(imageUrl);
const response = await fetch(imageUrl); const blob = await response.blob();
const blob = await response.blob(); if (blob.type) mimeType = blob.type;
if (blob.type) mimeType = blob.type; file = new File([blob], fileName, { type: mimeType });
const ext = mimeType.split("/")[1] || "jpg";
fileName = `image-${index}.${ext}`;
file = new File([blob], fileName, { type: mimeType });
}
} catch (error) { } catch (error) {
console.error("Error converting image URL to File:", error); console.error("Error converting image URL to File:", error);
} }
return { return {
id: `server-${index}`, id: (typeof img === "object" && img.id) || `server-${index}`,
preview: imageUrl, preview: imageUrl,
imageUrl: imageUrl, imageUrl: imageUrl,
file: file, file: file,
@ -494,42 +512,61 @@ const [completedSteps, setCompletedSteps] = useState([]);
valid: true, valid: true,
}; };
}) })
); ).then(res => res.filter(Boolean));
const formattedDob = pd.dob ? pd.dob.split("T")[0] : ""; const dobVal = pd.dob || pd.date_of_birth || "";
const formattedDob = dobVal ? dobVal.split("T")[0] : "";
dispatch( const updates = {};
updatePersonalDetails({ if (pd.name) updates.name = pd.name;
name: pd.name || "", const mobile = pd.mobile || pd.mobileNumber || pd.mobile_number;
mobile: pd.mobile || "", if (mobile) updates.mobile = mobile;
email: pd.email || "", const email = pd.email || pd.emailId || pd.email_id;
gender: pd.gender || "", if (email) updates.email = email;
dob: formattedDob, if (pd.gender) updates.gender = pd.gender;
height: pd.height_id || "", if (formattedDob) updates.dob = formattedDob;
weight: pd.weight || "", const height = pd.height_id || pd.height;
marital_status: pd.marital_status_id || "", if (height) updates.height = height;
religion: pd.religion_id || "", if (pd.weight) updates.weight = pd.weight;
profile_for: pd.profile_for_id || "", const marital_status = pd.marital_status_id || pd.marital_status;
caste: pd.caste_id || "", if (marital_status) updates.marital_status = marital_status;
sub_caste: pd.sub_caste_id || "", const religion = pd.religion_id || pd.religion;
willing_to_marry: pd.willing_to_marry || "", if (religion) updates.religion = religion;
inter_caste_parents: pd.inter_caste_parents || 0, const profile_for = pd.profile_for_id || pd.profile_for;
inter_caste_parents_details: pd.inter_caste_parents_details || "", if (profile_for) updates.profile_for = profile_for;
gothram: pd.gothram || "", const caste = pd.caste_id || pd.caste;
do_you_speak_telugu: pd.do_you_speak_telugu || 0, if (caste) updates.caste = caste;
about_us: pd.about_us || "", const sub_caste = pd.sub_caste_id || pd.sub_caste;
known_languages: pd.known_language_ids || [], if (sub_caste) updates.sub_caste = sub_caste;
mother_language: pd.mother_language_id || "", if (pd.willing_to_marry) updates.willing_to_marry = pd.willing_to_marry;
complexion: pd.complexion_id || "", if (pd.inter_caste_parents !== undefined) updates.inter_caste_parents = pd.inter_caste_parents ?? 0;
physical_status: pd.physical_status_id || "", if (pd.inter_caste_parents_details) updates.inter_caste_parents_details = pd.inter_caste_parents_details;
raasi: pd.raasi_id || "", const gothram = pd.gothram || pd.gothram_id;
star: pd.star_id || "", if (gothram) updates.gothram = gothram;
state: pd.state_id || "", if (pd.do_you_speak_telugu !== undefined) updates.do_you_speak_telugu = pd.do_you_speak_telugu ?? 0;
city: pd.district_id || "", if (pd.about_us) updates.about_us = pd.about_us;
pincode: pd.pincode || "", const known_languages = pd.known_language_ids || pd.known_languages;
profiles: mappedImages, if (known_languages) updates.known_languages = known_languages;
}) const mother_language = pd.mother_language_id || pd.mother_language;
); if (mother_language) updates.mother_language = mother_language;
const complexion = pd.complexion_id || pd.complexion;
if (complexion) updates.complexion = complexion;
const physical_status = pd.physical_status_id || pd.physical_status;
if (physical_status) updates.physical_status = physical_status;
const raasi = pd.raasi_id || pd.raasi;
if (raasi) updates.raasi = raasi;
const star = pd.star_id || pd.star;
if (star) updates.star = star;
const state = pd.state_id || pd.state;
if (state) updates.state = state;
const city = pd.district_id || pd.city;
if (city) updates.city = city;
if (pd.pincode) updates.pincode = pd.pincode;
if (mappedImages && mappedImages.length > 0) updates.profiles = mappedImages;
if (Object.keys(updates).length > 0) {
dispatch(updatePersonalDetails(updates));
}
} }
}; };
processData(); processData();
@ -546,31 +583,64 @@ const [completedSteps, setCompletedSteps] = useState([]);
enabled: isAuth || shouldHideStepper, enabled: isAuth || shouldHideStepper,
retry: false, retry: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: true,
}); });
useEffect(() => { useEffect(() => {
if (educationalData?.status === "success" && educationalData?.educational_details) { const isSuccess = educationalData?.status === "success" || educationalData?.success === true;
if (isSuccess && educationalData?.educational_details) {
const ed = educationalData.educational_details; const ed = educationalData.educational_details;
dispatch( const updates = {};
updateEducationalDetails({
study_field: ed.study_field_id || "", const study_field = ed.study_field_id || ed.study_field || ed.education_id;
education: ed.education_id || "", if (study_field) updates.study_field = study_field;
education_detail: ed.education_detail || "",
college_name: ed.college_name || "", const education = ed.education_id || ed.education || ed.qualification_id;
employee_type: ed.employee_type_id || "", if (education) updates.education = education;
occupation: ed.occupation_id || "",
occupation_detail: ed.occupation_detail || "", const education_detail = ed.education_detail || ed.education_details || ed.educationDetail;
company_name: ed.company_name || "", if (education_detail) updates.education_detail = education_detail;
income_currency: ed.income_currency || "INR",
annual_income: ed.annual_income || "", const college_name = ed.college_name || ed.collegeName || ed.college;
work_country: ed.work_country_id || "", if (college_name) updates.college_name = college_name;
work_state: ed.work_state_id || "",
work_district: ed.work_district_id || "", const employee_type = ed.employee_type_id || ed.employee_type || ed.employeeType;
work_city: ed.work_city || "", if (employee_type) updates.employee_type = employee_type;
address: ed.address || ed.work_location || "",
}) const occupation = ed.occupation_id || ed.occupation || ed.occupational;
); if (occupation) updates.occupation = occupation;
const occupation_detail = ed.occupation_detail || ed.occupationDetail || ed.occupation_details;
if (occupation_detail) updates.occupation_detail = occupation_detail;
const company_name = ed.company_name || ed.companyName || ed.organization_name;
if (company_name) updates.company_name = company_name;
const income_currency = ed.income_currency || ed.currency || "INR";
if (income_currency) updates.income_currency = income_currency;
const annual_income = ed.annual_income || ed.annual_income_id || ed.income;
if (annual_income) updates.annual_income = annual_income;
const work_country = ed.work_country_id || ed.work_country || ed.country_id;
if (work_country) updates.work_country = work_country;
const work_state = ed.work_state_id || ed.work_state || ed.state_id;
if (work_state) updates.work_state = work_state;
const work_district = ed.work_district_id || ed.work_district || ed.district_id;
if (work_district) updates.work_district = work_district;
const work_city = ed.work_city || ed.city || ed.work_location;
if (work_city) updates.work_city = work_city;
const address = ed.address || ed.work_location || "";
if (address) updates.address = address;
if (Object.keys(updates).length > 0) {
dispatch(updateEducationalDetails(updates));
}
} }
}, [educationalData, dispatch]); }, [educationalData, dispatch]);
@ -586,49 +656,59 @@ const {data:familyData} = useQuery({
enabled: isAuth || shouldHideStepper, enabled: isAuth || shouldHideStepper,
retry:false, retry:false,
refetchOnWindowFocus:false, refetchOnWindowFocus:false,
refetchOnMount: true,
}); });
useEffect(() => { useEffect(() => {
if (familyData?.status === "success" && familyData?.family_details) { const isSuccess = familyData?.status === "success" || familyData?.success === true;
const fd = familyData.family_details; if (isSuccess && familyData?.family_details) {
dispatch( const fd = familyData.family_details;
updateFamilyDetails({ const updates = {};
fatherName: fd.father_name || "",
fatherOccupation: fd.father_occupation || "", if (fd.father_name || fd.fatherName) updates.fatherName = fd.father_name || fd.fatherName || "";
motherName: fd.mother_name || "", if (fd.father_occupation || fd.fatherOccupation || fd.father_occupation_id) updates.fatherOccupation = fd.father_occupation ?? fd.fatherOccupation ?? fd.father_occupation_id ?? "";
motherOccupation: fd.mother_occupation || "", if (fd.mother_name || fd.motherName) updates.motherName = fd.mother_name || fd.motherName || "";
familyStatus: fd.family_status_id || "", if (fd.mother_occupation || fd.motherOccupation || fd.mother_occupation_id) updates.motherOccupation = fd.mother_occupation ?? fd.motherOccupation ?? fd.mother_occupation_id ?? "";
nativePlace: fd.native_place || "", if (fd.family_status_id || fd.familyStatus || fd.family_status) updates.familyStatus = fd.family_status_id || fd.familyStatus || fd.family_status || "";
familyCountry: fd.family_country_id || "", if (fd.native_place || fd.nativePlace) updates.nativePlace = fd.native_place || fd.nativePlace || "";
familyState: fd.family_state_id || "", if (fd.brother_count !== undefined) updates.brotherCount = fd.brother_count || 0;
familyDistrict: fd.family_district_id || "", if (fd.sister_count !== undefined) updates.sisterCount = fd.sister_count || 0;
familyCity: fd.family_city || "", if (fd.family_country_id || fd.family_country_name || fd.familyCountry) updates.familyCountry = fd.family_country_id || fd.familyCountry || "";
address: fd.address || "", if (fd.family_state_id || fd.family_state_name || fd.familyState) updates.familyState = fd.family_state_id || fd.familyState || "";
expectationDetails: fd.expectation_details || "", if (fd.family_district_id || fd.family_district_name || fd.familyDistrict) updates.familyDistrict = fd.family_district_id || fd.familyDistrict || "";
willingToGoAbroad: fd.willing_to_go_abroad || "", if (fd.family_city || fd.familyCity) updates.familyCity = fd.family_city || fd.familyCity || "";
brotherCount: fd.brother_count || 0, if (fd.address) updates.address = fd.address || "";
sisterCount: fd.sister_count || 0, if (fd.expectation_details || fd.expectationDetails) updates.expectationDetails = fd.expectation_details || fd.expectationDetails || "";
brothers: (fd.brothers || []).map((b) => ({ if (fd.willing_to_go_abroad !== undefined) updates.willingToGoAbroad = fd.willing_to_go_abroad || "";
if (fd.brothers || fd.brother_details) {
updates.brothers = (fd.brothers || fd.brother_details || []).map((b) => ({
name: b.name || "", name: b.name || "",
occupation: b.occupation_name || "", occupation: b.occupation || b.occupational || b.occupation_name || "",
maritalStatus: b.marital_status || "", maritalStatus: b.marital_status || b.maritalStatus || b.marital_status_id || "",
type: b.type || "", type: b.type || "",
hasChildren: b.has_children || "", hasChildren: b.has_children || b.hasChildren || b.thereHaveChildren || "",
details: b.additional_details || "" details: b.details || b.additional_details || b.additionalDetails || "",
})), }));
sisters: (fd.sisters || []).map((s) => ({ }
if (fd.sisters || fd.sister_details) {
updates.sisters = (fd.sisters || fd.sister_details || []).map((s) => ({
name: s.name || "", name: s.name || "",
occupation: s.occupation_name || "", occupation: s.occupation || s.occupational || s.occupation_name || "",
maritalStatus: s.marital_status || "", maritalStatus: s.marital_status || s.maritalStatus || s.marital_status_id || "",
type: s.type || "", type: s.type || "",
hasChildren: s.has_children || "", hasChildren: s.has_children || s.hasChildren || s.thereHaveChildren || "",
details: s.additional_details || "" details: s.details || s.additional_details || s.additionalDetails || "",
})), }));
}) }
);
} if (Object.keys(updates).length > 0) {
}, [familyData, dispatch]); dispatch(updateFamilyDetails(updates));
}
}
}, [familyData, dispatch]);
// Fetch Lifestyle Details // Fetch Lifestyle Details
const { data: lifestyleData } = useQuery({ const { data: lifestyleData } = useQuery({
@ -644,7 +724,8 @@ useEffect(() => {
}); });
useEffect(() => { useEffect(() => {
if (lifestyleData?.status === "success" && lifestyleData?.lifestyle_details) { const isSuccess = lifestyleData?.status === "success" || lifestyleData?.success === true;
if (isSuccess && lifestyleData?.lifestyle_details) {
const ld = lifestyleData.lifestyle_details; const ld = lifestyleData.lifestyle_details;
const horoscope = ld.horoscope || {}; const horoscope = ld.horoscope || {};
@ -653,18 +734,35 @@ useEffect(() => {
for (let i = 1; i <= 12; i++) { for (let i = 1; i <= 12; i++) {
const key = `${prefix}_${i}`; const key = `${prefix}_${i}`;
const val = horoscope[key]; const val = horoscope[key];
chart[i] = val ? val.split(",") : []; chart[i] = val ? (Array.isArray(val) ? val : val.split(",")) : [];
} }
return chart; return chart;
}; };
const dobVal = ld.dob || ld.date_of_birth || "";
const formattedDob = dobVal ? dobVal.split("T")[0] : "";
let formattedTob = ld.tob || ld.time_of_birth || "";
if (!formattedTob && ld.time_of_birth_formated) {
if (ld.time_of_birth_formated.includes(":")) {
formattedTob = ld.time_of_birth_formated.substring(0, 5);
}
}
dispatch( dispatch(
updateLifestyleDetails({ updateLifestyleDetails({
diets: ld.diet_id || "", dob: formattedDob,
hobbies: ld.hobbies_ids || [], tob: formattedTob,
dob: ld.time_of_birth ? ld.time_of_birth.split("T")[0] : "", placeOfBirth: ld.place_of_birth || ld.placeOfBirth || "",
tob: ld.time_of_birth_formated ? ld.time_of_birth_formated.substring(0, 5) : "", raasi: ld.raasi_id || horoscope.raasi_id || ld.raasi || "",
placeOfBirth: ld.place_of_birth || "", star: ld.star_id || horoscope.star_id || ld.star || "",
patham: ld.patham || horoscope.patham || "",
lagnam: ld.lagnam_id || ld.lagnam || horoscope.lagnam || "",
panjangam_type: ld.panjangam_type || horoscope.panjangam_type || "",
dasa_balance: ld.dasa_balance || horoscope.dasa_balance || "",
dasa_years: ld.dasa_years || horoscope.dasa_years || "",
dasa_months: ld.dasa_months || horoscope.dasa_months || "",
dasa_days: ld.dasa_days || horoscope.dasa_days || "",
graha: mapChart("graha"), graha: mapChart("graha"),
amsam: mapChart("amsam"), amsam: mapChart("amsam"),
}) })
@ -686,21 +784,86 @@ useEffect(() => {
}); });
useEffect(() => { useEffect(() => {
if (partnerData?.status === "success" && partnerData?.partner_preferences) { const isSuccess = partnerData?.status === "success" || partnerData?.success === true;
if (isSuccess && partnerData?.partner_preferences) {
const pp = partnerData.partner_preferences; const pp = partnerData.partner_preferences;
dispatch( const updates = {};
updatePartnerPreferences({
ageRange: pp.preferred_age_range_id || "", const age_from = pp.preferred_age_from ?? pp.age_from ?? pp.preferred_age_from_id ?? pp.age_from_id;
annualIncome: pp.preferred_annual_income_id || "", if (age_from !== undefined && age_from !== null) updates.age_from = age_from;
castes: pp.preferred_castes_ids || [],
subCastes: pp.preferred_sub_castes_ids || [], const age_to = pp.preferred_age_to ?? pp.age_to ?? pp.preferred_age_to_id ?? pp.age_to_id;
occupations: pp.preferred_occupations_ids || [], if (age_to !== undefined && age_to !== null) updates.age_to = age_to;
educations: pp.preferred_educations_ids || [],
hobbies: pp.preferred_hobbies_ids || [], const height_from = pp.preferred_height_from_id ?? pp.height_from_id ?? pp.preferred_height_from ?? pp.height_from;
states: pp.preferred_states_ids || [], if (height_from !== undefined && height_from !== null) updates.height_from = height_from;
districts: pp.preferred_districts_ids || [],
}) const height_to = pp.preferred_height_to_id ?? pp.height_to_id ?? pp.preferred_height_to ?? pp.height_to;
); if (height_to !== undefined && height_to !== null) updates.height_to = height_to;
const mapMulti = (val) => {
if (!val) return [];
if (Array.isArray(val)) return val;
return String(val).split(",").map(v => v.trim()).filter(Boolean);
};
if (pp.preferred_marital_status_ids || pp.marital_status_ids || pp.preferred_marital_statuses || pp.marital_status) {
const val = mapMulti(pp.preferred_marital_status_ids || pp.marital_status_ids || pp.preferred_marital_statuses || pp.marital_status);
if (val.length > 0) updates.marital_statuses = val;
}
if (pp.preferred_birth_star_ids || pp.birth_star_ids || pp.preferred_birth_stars || pp.birth_star) {
const val = mapMulti(pp.preferred_birth_star_ids || pp.birth_star_ids || pp.preferred_birth_stars || pp.birth_star);
if (val.length > 0) updates.birth_stars = val;
}
if (pp.preferred_castes_ids || pp.castes_ids || pp.preferred_castes || pp.caste) {
const val = mapMulti(pp.preferred_castes_ids || pp.castes_ids || pp.preferred_castes || pp.caste);
if (val.length > 0) updates.castes = val;
}
if (pp.preferred_sub_castes_ids || pp.sub_castes_ids || pp.preferred_sub_castes || pp.sub_caste) {
const val = mapMulti(pp.preferred_sub_castes_ids || pp.sub_castes_ids || pp.preferred_sub_castes || pp.sub_caste);
if (val.length > 0) updates.sub_castes = val;
}
if (pp.preferred_occupations_ids || pp.occupations_ids || pp.preferred_occupations || pp.occupation) {
const val = mapMulti(pp.preferred_occupations_ids || pp.occupations_ids || pp.preferred_occupations || pp.occupation);
if (val.length > 0) updates.occupations = val;
}
if (pp.preferred_educations_ids || pp.educations_ids || pp.preferred_educations || pp.education) {
const val = mapMulti(pp.preferred_educations_ids || pp.educations_ids || pp.preferred_educations || pp.education);
if (val.length > 0) updates.educations = val;
}
if (pp.preferred_employee_type_ids || pp.employee_type_ids || pp.preferred_employee_types || pp.employee_type) {
const val = mapMulti(pp.preferred_employee_type_ids || pp.employee_type_ids || pp.preferred_employee_types || pp.employee_type);
if (val.length > 0) updates.employee_types = val;
}
if (pp.preferred_currencies || pp.currencies) {
const val = mapMulti(pp.preferred_currencies || pp.currencies);
if (val.length > 0) updates.currencies = val;
}
const inr_from = pp.preferred_inr_from ?? pp.inr_from ?? pp.preferred_inr_from_id;
if (inr_from !== undefined && inr_from !== null) updates.inr_from = inr_from;
const inr_to = pp.preferred_inr_to ?? pp.inr_to ?? pp.preferred_inr_to_id;
if (inr_to !== undefined && inr_to !== null) updates.inr_to = inr_to;
const usd_from = pp.preferred_usd_from ?? pp.usd_from ?? pp.preferred_usd_from_id;
if (usd_from !== undefined && usd_from !== null) updates.usd_from = usd_from;
const usd_to = pp.preferred_usd_to ?? pp.usd_to ?? pp.preferred_usd_to_id;
if (usd_to !== undefined && usd_to !== null) updates.usd_to = usd_to;
if (pp.preferred_states_ids || pp.states_ids || pp.preferred_states || pp.state) {
const val = mapMulti(pp.preferred_states_ids || pp.states_ids || pp.preferred_states || pp.state);
if (val.length > 0) updates.states = val;
}
if (pp.preferred_districts_ids || pp.districts_ids || pp.preferred_districts || pp.district) {
const val = mapMulti(pp.preferred_districts_ids || pp.districts_ids || pp.preferred_districts || pp.district);
if (val.length > 0) updates.districts = val;
}
if (Object.keys(updates).length > 0) {
dispatch(updatePartnerPreferences(updates));
}
} }
}, [partnerData, dispatch]); }, [partnerData, dispatch]);
@ -722,6 +885,7 @@ useEffect(() => {
"marital_status", "marital_status",
"profile_for", "profile_for",
"caste", "caste",
"sub_caste",
"email", "email",
"mother_language", "mother_language",
"complexion", "complexion",
@ -795,7 +959,7 @@ useEffect(() => {
]; ];
required.forEach((field) => { required.forEach((field) => {
if (!familyDetails[field]) { if (!familyDetails[field]) {
// newErrors[field] = "This field is required"; // newErrors[field] = `${label} is required`;
const label = field const label = field
.replace(/([A-Z])/g, " $1") .replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase()); .replace(/^./, (str) => str.toUpperCase());
@ -810,7 +974,7 @@ useEffect(() => {
newErrors.motherName = "Mother Name must contain only alphabets"; newErrors.motherName = "Mother Name must contain only alphabets";
} }
} else if (step === 4) { } else if (step === 4) {
const required = ["diets", "hobbies", "dob", "tob"]; const required = ["dob", "tob", "placeOfBirth", "raasi", "star"];
required.forEach((field) => { required.forEach((field) => {
const value = lifestyleDetails[field]; const value = lifestyleDetails[field];
if (Array.isArray(value)) { if (Array.isArray(value)) {
@ -818,7 +982,10 @@ useEffect(() => {
if (field === "hobbies") { if (field === "hobbies") {
newErrors[field] = "Hobbies and Interests is required"; newErrors[field] = "Hobbies and Interests is required";
} else { } else {
newErrors[field] = "This field is required"; const label = field
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase());
newErrors[field] = `${label} is required`;
} }
} }
@ -830,8 +997,11 @@ useEffect(() => {
else if (field === "tob") { else if (field === "tob") {
newErrors[field] = "Time of Birth is required"; newErrors[field] = "Time of Birth is required";
} }
else if (field === "diets") { else if (field === "raasi") {
newErrors[field] = "Diet is required"; newErrors[field] = "Raasi is required";
}
else if (field === "star") {
newErrors[field] = "Birth Star is required";
} }
else { else {
@ -843,33 +1013,53 @@ useEffect(() => {
} }
}); });
} else if (step === 5) { } else if (step === 5) {
const required = [ // Range Validations
"ageRange", if (partnerPreferences.age_from && partnerPreferences.age_to) {
"castes", if (Number(partnerPreferences.age_from) > Number(partnerPreferences.age_to)) {
"subCastes", newErrors.age_from = "From Age cannot be greater than To Age";
"occupations", newErrors.age_to = "To Age cannot be less than From Age";
"educations", }
"hobbies", }
"annualIncome",
"states", if (partnerPreferences.height_from && partnerPreferences.height_to) {
"districts", if (Number(partnerPreferences.height_from) > Number(partnerPreferences.height_to)) {
]; newErrors.height_from = "From Height cannot be greater than To Height";
newErrors.height_to = "To Height cannot be less than From Height";
}
}
if (partnerPreferences.currencies.includes("INR")) {
const from = parseFloat(partnerPreferences.inr_from);
const to = parseFloat(partnerPreferences.inr_to);
if (!isNaN(from) && !isNaN(to) && from > to) {
newErrors.inr_from = "From Income cannot be greater than To Income";
newErrors.inr_to = "To Income cannot be less than From Income";
}
}
if (partnerPreferences.currencies.includes("USD")) {
const from = parseFloat(partnerPreferences.usd_from);
const to = parseFloat(partnerPreferences.usd_to);
if (!isNaN(from) && !isNaN(to) && from > to) {
newErrors.usd_from = "From Income cannot be greater than To Income";
newErrors.usd_to = "To Income cannot be less than From Income";
}
}
// Required fields
const required = ["age_from", "age_to", "height_from", "height_to", "castes", "educations", "states"];
required.forEach((field) => { required.forEach((field) => {
const value = partnerPreferences[field]; const value = partnerPreferences[field];
let isMissing = false;
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length === 0) { isMissing = value.length === 0;
// newErrors[field] = "This field is required"; } else {
const label = field isMissing = !value && value !== 0;
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase());
newErrors[field] = `${label} is required`;
}
return;
} }
if (!value) {
// newErrors[field] = "This field is required"; if (isMissing) {
const label = field const label = field
.replace(/([A-Z])/g, " $1") .replace(/_/g, " ")
.replace(/^./, (str) => str.toUpperCase()); .replace(/^./, (str) => str.toUpperCase());
newErrors[field] = `${label} is required`; newErrors[field] = `${label} is required`;
} }
@ -928,7 +1118,13 @@ useEffect(() => {
if (!isValidFile && item.preview && typeof item.preview === "string") { if (!isValidFile && item.preview && typeof item.preview === "string") {
try { try {
const response = await fetch(item.preview); // Rewrite URL to use proxy in development to avoid CORS
const finalUrl = (import.meta.env.DEV && item.preview.startsWith('https://www.thirukalyanam.amrithaa.net/backend'))
? item.preview.replace('https://www.thirukalyanam.amrithaa.net/backend', '/backend')
: item.preview;
const response = await fetch(finalUrl);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const blob = await response.blob(); const blob = await response.blob();
const mimeType = blob.type || "image/jpeg"; const mimeType = blob.type || "image/jpeg";
const ext = mimeType.split("/")[1] || "jpg"; const ext = mimeType.split("/")[1] || "jpg";
@ -983,12 +1179,16 @@ useEffect(() => {
formData.append("mother_name", familyDetails.motherName); formData.append("mother_name", familyDetails.motherName);
formData.append("mother_occupation", familyDetails.motherOccupation || ""); formData.append("mother_occupation", familyDetails.motherOccupation || "");
formData.append("family_status", familyDetails.familyStatus); formData.append("family_status", familyDetails.familyStatus);
formData.append("family_status_id", familyDetails.familyStatus);
formData.append("native_place", familyDetails.nativePlace || ""); formData.append("native_place", familyDetails.nativePlace || "");
formData.append("brother_count", familyDetails.brotherCount || 0); formData.append("brother_count", familyDetails.brotherCount || 0);
formData.append("sister_count", familyDetails.sisterCount || 0); formData.append("sister_count", familyDetails.sisterCount || 0);
formData.append("family_country", familyDetails.familyCountry || ""); formData.append("family_country", familyDetails.familyCountry || "");
formData.append("family_country_id", familyDetails.familyCountry || "");
formData.append("family_state", familyDetails.familyState || ""); formData.append("family_state", familyDetails.familyState || "");
formData.append("family_state_id", familyDetails.familyState || "");
formData.append("family_district", familyDetails.familyDistrict || ""); formData.append("family_district", familyDetails.familyDistrict || "");
formData.append("family_district_id", familyDetails.familyDistrict || "");
formData.append("family_city", familyDetails.familyCity || ""); formData.append("family_city", familyDetails.familyCity || "");
formData.append("address", familyDetails.address || ""); formData.append("address", familyDetails.address || "");
formData.append("expectation_details", familyDetails.expectationDetails || ""); formData.append("expectation_details", familyDetails.expectationDetails || "");
@ -1001,6 +1201,7 @@ useEffect(() => {
formData.append(`brothers[${index}][type]`, brother?.type || ""); formData.append(`brothers[${index}][type]`, brother?.type || "");
formData.append(`brothers[${index}][has_children]`, brother?.hasChildren || ""); formData.append(`brothers[${index}][has_children]`, brother?.hasChildren || "");
formData.append(`brothers[${index}][details]`, brother?.details || ""); formData.append(`brothers[${index}][details]`, brother?.details || "");
formData.append(`brothers[${index}][additional_details]`, brother?.details || "");
}); });
(familyDetails.sisters || []).forEach((sister, index) => { (familyDetails.sisters || []).forEach((sister, index) => {
@ -1010,6 +1211,7 @@ useEffect(() => {
formData.append(`sisters[${index}][type]`, sister?.type || ""); formData.append(`sisters[${index}][type]`, sister?.type || "");
formData.append(`sisters[${index}][has_children]`, sister?.hasChildren || ""); formData.append(`sisters[${index}][has_children]`, sister?.hasChildren || "");
formData.append(`sisters[${index}][details]`, sister?.details || ""); formData.append(`sisters[${index}][details]`, sister?.details || "");
formData.append(`sisters[${index}][additional_details]`, sister?.details || "");
}); });
return formData; return formData;
@ -1020,12 +1222,17 @@ useEffect(() => {
formData.append("dob", lifestyleDetails.dob || ""); formData.append("dob", lifestyleDetails.dob || "");
formData.append("tob", lifestyleDetails.tob || ""); formData.append("tob", lifestyleDetails.tob || "");
formData.append("place_of_birth", lifestyleDetails.placeOfBirth || ""); formData.append("place_of_birth", lifestyleDetails.placeOfBirth || "");
formData.append("raasi", lifestyleDetails.raasi || "");
formData.append("diets", lifestyleDetails.diets || ""); formData.append("raasi_id", lifestyleDetails.raasi || "");
formData.append("star", lifestyleDetails.star || "");
(lifestyleDetails.hobbies || []).forEach((id, index) => { formData.append("star_id", lifestyleDetails.star || "");
formData.append(`hobbies[${index}]`, id); formData.append("patham", lifestyleDetails.patham || "");
}); formData.append("lagnam", lifestyleDetails.lagnam || "");
formData.append("panjangam_type", lifestyleDetails.panjangam_type || "");
formData.append("dasa_balance", lifestyleDetails.dasa_balance || "");
formData.append("dasa_years", lifestyleDetails.dasa_years || "");
formData.append("dasa_months", lifestyleDetails.dasa_months || "");
formData.append("dasa_days", lifestyleDetails.dasa_days || "");
const graha = lifestyleDetails.graha || {}; const graha = lifestyleDetails.graha || {};
Object.keys(graha).forEach((house) => { Object.keys(graha).forEach((house) => {
@ -1048,35 +1255,54 @@ useEffect(() => {
const buildRegisterStep5Payload = () => { const buildRegisterStep5Payload = () => {
const formData = new FormData(); const formData = new FormData();
formData.append("age_range", partnerPreferences.ageRange || "");
formData.append("annual_income", partnerPreferences.annualIncome || ""); formData.append("preferred_age_from", partnerPreferences.age_from || "");
formData.append("preferred_age_to", partnerPreferences.age_to || "");
formData.append("preferred_height_from", partnerPreferences.height_from || "");
formData.append("preferred_height_to", partnerPreferences.height_to || "");
formData.append("preferred_inr_from", partnerPreferences.inr_from || "");
formData.append("preferred_inr_to", partnerPreferences.inr_to || "");
formData.append("preferred_usd_from", partnerPreferences.usd_from || "");
formData.append("preferred_usd_to", partnerPreferences.usd_to || "");
(partnerPreferences.castes || []).forEach((id, index) => { (partnerPreferences.marital_statuses || []).forEach((id, index) => {
formData.append(`castes[${index}]`, id); formData.append(`preferred_marital_status_ids[${index}]`, id);
}); });
(partnerPreferences.subCastes || []).forEach((id, index) => { (partnerPreferences.employee_types || []).forEach((id, index) => {
formData.append(`sub_castes[${index}]`, id); formData.append(`preferred_employee_type_ids[${index}]`, id);
});
(partnerPreferences.birth_stars || []).forEach((id, index) => {
formData.append(`preferred_birth_star_ids[${index}]`, id);
});
(partnerPreferences.currencies || []).forEach((curr, index) => {
formData.append(`preferred_currencies[${index}]`, curr);
});
(partnerPreferences.castes || []).forEach((id, index) => {
formData.append(`preferred_castes_ids[${index}]`, id);
});
(partnerPreferences.sub_castes || []).forEach((id, index) => {
formData.append(`preferred_sub_castes_ids[${index}]`, id);
}); });
(partnerPreferences.occupations || []).forEach((id, index) => { (partnerPreferences.occupations || []).forEach((id, index) => {
formData.append(`occupations[${index}]`, id); formData.append(`preferred_occupations_ids[${index}]`, id);
}); });
(partnerPreferences.educations || []).forEach((id, index) => { (partnerPreferences.educations || []).forEach((id, index) => {
formData.append(`educations[${index}]`, id); formData.append(`preferred_educations_ids[${index}]`, id);
});
(partnerPreferences.hobbies || []).forEach((id, index) => {
formData.append(`hobbies[${index}]`, id);
}); });
(partnerPreferences.states || []).forEach((id, index) => { (partnerPreferences.states || []).forEach((id, index) => {
formData.append(`states[${index}]`, id); formData.append(`preferred_states_ids[${index}]`, id);
}); });
(partnerPreferences.districts || []).forEach((id, index) => { (partnerPreferences.districts || []).forEach((id, index) => {
formData.append(`districts[${index}]`, id); formData.append(`preferred_districts_ids[${index}]`, id);
}); });
return formData; return formData;
@ -1256,11 +1482,6 @@ useEffect(() => {
if (!enabledSteps.includes(step)) return; if (!enabledSteps.includes(step)) return;
// If moving backwards during registration, clear the unsaved data of the step we are leaving
if (step < currentStep && !shouldHideStepper) {
dispatch(clearAllStepsFrom(currentStep));
}
setCurrentStep(step); setCurrentStep(step);
setErrors({}); setErrors({});
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -1378,7 +1599,7 @@ useEffect(() => {
return required.every(field => familyDetails[field]); return required.every(field => familyDetails[field]);
} }
if (stepNum === 4) { if (stepNum === 4) {
const required = ["diets", "hobbies", "dob", "tob"]; const required = ["dob", "tob", "placeOfBirth", "raasi", "star"];
return required.every(field => { return required.every(field => {
const val = lifestyleDetails[field]; const val = lifestyleDetails[field];
return Array.isArray(val) ? val.length > 0 : !!val; return Array.isArray(val) ? val.length > 0 : !!val;

View File

@ -5,6 +5,7 @@ import {
getSubCasteMasters, getSubCasteMasters,
getCityMasters, getCityMasters,
getStarMasters, getStarMasters,
getPathamMasters,
} from "../api/masters.api"; } from "../api/masters.api";
/** Personal details masters (gender, marital status, religion, gothram, raasi, state, etc.) */ /** Personal details masters (gender, marital status, religion, gothram, raasi, state, etc.) */
@ -43,7 +44,15 @@ export const useSubCasteMasters = (caste_id) =>
export const useCityMasters = (state_id) => export const useCityMasters = (state_id) =>
useQuery({ useQuery({
queryKey: ["city-masters", state_id], queryKey: ["city-masters", state_id],
queryFn: () => getCityMasters(state_id), queryFn: async () => {
if (Array.isArray(state_id)) {
const results = await Promise.all(
state_id.map((id) => getCityMasters(id))
);
return results;
}
return getCityMasters(state_id);
},
enabled: Array.isArray(state_id) ? state_id.length > 0 : !!state_id, enabled: Array.isArray(state_id) ? state_id.length > 0 : !!state_id,
}); });
@ -54,3 +63,11 @@ export const useStarMasters = (raasi_id) =>
queryFn: () => getStarMasters(raasi_id), queryFn: () => getStarMasters(raasi_id),
enabled: !!raasi_id, enabled: !!raasi_id,
}); });
/** Patham depends on star */
export const usePathamMasters = (star_id) =>
useQuery({
queryKey: ["patham-masters", star_id],
queryFn: () => getPathamMasters(star_id),
enabled: !!star_id,
});

View File

@ -8,6 +8,12 @@ export const useProfiles = (filters = {}) => {
// Remove empty filters // Remove empty filters
const cleanFilters = Object.entries(filters).reduce((acc, [key, value]) => { const cleanFilters = Object.entries(filters).reduce((acc, [key, value]) => {
// Skip default boundaries to prevent strict filtering on null values
if (key === "from_age" && value === 18) return acc;
if (key === "to_age" && value === 70) return acc;
if (key === "from_height" && value === 4.0) return acc;
if (key === "to_height" && value === 7.11) return acc;
if ( if (
value !== "" && value !== "" &&
value !== null && value !== null &&
@ -19,14 +25,21 @@ export const useProfiles = (filters = {}) => {
return acc; return acc;
}, {}); }, {});
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["profiles-filter-list", cleanFilters], queryKey: ["profiles-filter-list", cleanFilters],
queryFn: ({ pageParam = 1 }) => queryFn: ({ pageParam = 1 }) => {
getProfilesFilterList({ console.log("[API-REQUEST] Calling profiles/lists with filters:", {
...cleanFilters, ...cleanFilters,
page: pageParam, page: pageParam,
}), });
return getProfilesFilterList({
...cleanFilters,
page: pageParam,
});
},
staleTime: 1000 * 60 * 2, staleTime: 1000 * 60 * 2,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,

View File

@ -1,4 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2, Loader2, MessageCircle } from 'lucide-react'; import { Search, MoreVertical, Send, Phone, Video, Check, CheckCheck, ArrowLeft, Star, Share2, Flag, Ban, Trash2, Loader2, MessageCircle } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import ReportModal from '../components/common/ReportModal'; import ReportModal from '../components/common/ReportModal';
@ -9,7 +11,9 @@ import { useWebSocket } from '../hooks/useWebSocket';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
const ChatUI = () => { const ChatUI = () => {
const { chatId } = useParams();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { personalDetails } = useSelector((state) => state.registerform); const { personalDetails } = useSelector((state) => state.registerform);
const [selectedChat, setSelectedChat] = useState(null); const [selectedChat, setSelectedChat] = useState(null);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
@ -271,12 +275,20 @@ const ChatUI = () => {
fetchContacts(); fetchContacts();
}, []); }, []);
useEffect(() => {
if (chatId) {
setSelectedChat(chatId);
setShowChatOnMobile(true);
}
}, [chatId]);
useEffect(() => { useEffect(() => {
if (selectedChat) { if (selectedChat) {
fetchMessages(selectedChat); fetchMessages(selectedChat);
} }
}, [selectedChat]); }, [selectedChat]);
useEffect(() => { useEffect(() => {
if (isPaginating.current) { if (isPaginating.current) {
if (scrollContainerRef.current) { if (scrollContainerRef.current) {

View File

@ -18,6 +18,12 @@ import {
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { getInterestList, updateInterestStatus } from "../services/profileActionApi"; import { getInterestList, updateInterestStatus } from "../services/profileActionApi";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { motion, AnimatePresence } from "framer-motion";
import axiosInstance, { apiForFiles } from "../api/axiosInstance";
import { API_ENDPOINTS } from "../api/apiEndpoints";
import { sendMessage } from "../services/chatApi";
import UpgradeModal from "../components/common/UpgradeModal";
const InterestSendPage = () => { const InterestSendPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -84,18 +90,34 @@ const InterestSendPage = () => {
const handleStatusUpdate = async (profileId, status) => { const handleStatusUpdate = async (profileId, status) => {
try { try {
const response = await updateInterestStatus(profileId, status); const response = await updateInterestStatus(profileId, status);
if (response.status === "success" || response.success) { const isSuccess = response.status === "success" || response.success === true || response.status === true;
toast.success(response.message || `Request ${status}ed successfully`); const message = response.message || "";
if (isSuccess) {
toast.success(message || `Request ${status}ed successfully`);
fetchInterests(); fetchInterests();
} else { } else {
toast.error(response.message || "Failed to update status"); // Check for daily interest accept limit (from Flutter code)
if (message.toLowerCase().includes("daily interest accept limit") ||
message.toLowerCase().includes("limit has been reached")) {
// Trigger the Upgrade Modal via a state if needed, but here we can use a simpler approach
// or just find a way to open the modal from the ProfileCard.
// For now, let's assume we can trigger a global upgrade modal or just use toast.
// Actually, let's pass a function to show the modal or use a shared state.
// In this component, we can use a local state for the parent.
setIsGlobalUpgradeModalOpen(true);
} else {
toast.error(message || "Failed to update status");
}
} }
} catch (error) { } catch (error) {
console.error("Error updating status:", error); console.error("Error updating status:", error);
toast.error("An error occurred"); toast.error("An error occurred while updating status");
} }
}; };
const [isGlobalUpgradeModalOpen, setIsGlobalUpgradeModalOpen] = useState(false);
const hasSubTabs = subTabs.hasOwnProperty(selectedTabIndex); const hasSubTabs = subTabs.hasOwnProperty(selectedTabIndex);
return ( return (
@ -148,7 +170,7 @@ const InterestSendPage = () => {
)} )}
{/* Main Content */} {/* Main Content */}
<div className="max-w-7xl mx-auto px-4 py-8"> <div className="max-w-7xl mx-auto px-4 py-8 pb-32">
<h2 className="text-xl font-bold text-gray-900 mb-6"> <h2 className="text-xl font-bold text-gray-900 mb-6">
{selectedTabIndex === 0 && `Matches yet to respond (${profiles.length})`} {selectedTabIndex === 0 && `Matches yet to respond (${profiles.length})`}
{selectedTabIndex === 1 && `You request sent to others (${profiles.length})`} {selectedTabIndex === 1 && `You request sent to others (${profiles.length})`}
@ -177,18 +199,139 @@ const InterestSendPage = () => {
</div> </div>
)} )}
</div> </div>
<UpgradeModal
isOpen={isGlobalUpgradeModalOpen}
onClose={() => setIsGlobalUpgradeModalOpen(false)}
/>
</div> </div>
); );
}; };
const ProfileCard = ({ profile, tabIndex, onStatusUpdate }) => { const ProfileCard = ({ profile, tabIndex, onStatusUpdate }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const [isViewContactModalOpen, setIsViewContactModalOpen] = useState(false);
const [isChatConfirmModalOpen, setIsChatConfirmModalOpen] = useState(false);
const [isContactSuccessModalOpen, setIsContactSuccessModalOpen] = useState(false);
const [isInterestStatusModalOpen, setIsInterestStatusModalOpen] = useState(false);
const [isInterestRejectedModalOpen, setIsInterestRejectedModalOpen] = useState(false);
const [unlockedMobile, setUnlockedMobile] = useState(null);
const [isCreatingChat, setIsCreatingChat] = useState(false);
// New state for Accept/Reject confirmation
const [statusConfirm, setStatusConfirm] = useState({ open: false, status: null });
const handleCall = (e) => {
e.stopPropagation();
const interestStatus = (profile.is_send_interest_status || "").toLowerCase();
// 1. Check protection & interest status (same as handleMessage)
if (profile.chat_protection === 1 || profile.call_protection === 1) {
if (profile.is_send_interest) {
if (interestStatus === 'pending') {
setIsInterestStatusModalOpen(true);
return;
} else if (interestStatus === 'reject' || interestStatus === 'rejected') {
setIsInterestRejectedModalOpen(true);
return;
}
}
}
const mobile = (profile.mobile_number || "").toLowerCase();
if (mobile.includes("upgrade")) {
setIsUpgradeModalOpen(true);
} else if (mobile.includes("view contact")) {
setIsViewContactModalOpen(true);
} else {
setUnlockedMobile(profile.mobile_number);
setIsContactSuccessModalOpen(true);
}
};
const handleMessage = (e) => {
e.stopPropagation();
// 1. Check if chat already exists
if (profile.chat_id) {
navigate(`/chat/${profile.chat_id}`);
return;
}
const interestStatus = (profile.is_send_interest_status || "").toLowerCase();
// 2. Check protection & interest status (same as ProfileCardUI)
if (profile.chat_protection === 1 || profile.call_protection === 1) {
if (profile.is_send_interest) {
if (interestStatus === 'pending') {
setIsInterestStatusModalOpen(true);
return;
} else if (interestStatus === 'reject' || interestStatus === 'rejected') {
setIsInterestRejectedModalOpen(true);
return;
}
}
}
// 3. Show confirmation dialog
setIsChatConfirmModalOpen(true);
};
const _handleCreateChat = async () => {
if (isCreatingChat) return;
setIsChatConfirmModalOpen(false);
setIsCreatingChat(true);
try {
const response = await axiosInstance.post(API_ENDPOINTS.CHAT_CREATE, { profile_id: profile.id });
if (response.data?.status === true || response.data?.status === 'success') {
const newChatId = response.data?.chat_id;
try {
await sendMessage(newChatId, "This profile has initiated a chat with you");
} catch (err) {}
toast.success("Chat initiated!");
navigate(`/chat/${newChatId}`);
} else {
setIsUpgradeModalOpen(true);
}
} catch (error) {
toast.error("Failed to start conversation");
} finally {
setIsCreatingChat(false);
}
};
const _handleViewContact = async () => {
setIsViewContactModalOpen(false);
try {
const formData = new FormData();
formData.append("profile_id", profile.id);
const response = await apiForFiles.post(API_ENDPOINTS.VIEW_CONTACT, formData);
if (response.data?.status === 'success') {
setUnlockedMobile(response.data?.mobile_number);
setIsContactSuccessModalOpen(true);
toast.success("Contact details unlocked!");
} else {
setIsUpgradeModalOpen(true);
}
} catch (error) {
toast.error("Failed to view contact");
}
};
return ( return (
<>
<div <div
onClick={() => navigate(`/profile-details/${profile.id}`)} onClick={() => navigate(`/profile-details/${profile.id}`)}
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow cursor-pointer p-4" className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow cursor-pointer p-4"
> >
<div className="flex gap-4"> <div className="flex gap-4">
{/* Profile Image */} {/* Profile Image */}
<div className="relative w-32 h-40 flex-shrink-0"> <div className="relative w-32 h-40 flex-shrink-0">
@ -252,36 +395,152 @@ const ProfileCard = ({ profile, tabIndex, onStatusUpdate }) => {
{tabIndex === 0 && profile.statusReceived === 'pending' ? ( {tabIndex === 0 && profile.statusReceived === 'pending' ? (
<> <>
<button <button
onClick={(e) => { e.stopPropagation(); onStatusUpdate(profile.id, 'reject'); }} onClick={(e) => { e.stopPropagation(); setStatusConfirm({ open: true, status: 'reject' }); }}
className="flex-1 py-2 rounded-lg font-bold border border-[#DF1D46] text-[#DF1D46] hover:bg-[#DF1D46]/5 transition-all flex items-center justify-center gap-2" className="flex-1 py-2 rounded-lg font-bold border border-[#DF1D46] text-[#DF1D46] hover:bg-[#DF1D46]/5 transition-all flex items-center justify-center gap-2"
> >
<XCircle className="w-5 h-5" /> Reject Request <XCircle className="w-5 h-5" /> Reject Request
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onStatusUpdate(profile.id, 'accept'); }} onClick={(e) => { e.stopPropagation(); setStatusConfirm({ open: true, status: 'accept' }); }}
className="flex-1 py-2 rounded-lg font-bold bg-[#00903F] text-white hover:bg-[#00903F]/90 transition-all flex items-center justify-center gap-2" className="flex-1 py-2 rounded-lg font-bold bg-[#00903F] text-white hover:bg-[#00903F]/90 transition-all flex items-center justify-center gap-2"
> >
<CheckCircle className="w-5 h-5" /> Accept Request <CheckCircle className="w-5 h-5" /> Accept Request
</button> </button>
</> </>
) : (tabIndex === 1 || tabIndex === 2 || (tabIndex === 0 && profile.statusReceived === 'accept')) ? ( ) : (tabIndex === 1 || tabIndex === 2 || (tabIndex === 0 && profile.statusReceived !== 'pending')) ? (
<> <>
<button <button
onClick={(e) => { e.stopPropagation(); navigate("/chat"); }} onClick={handleMessage}
className="flex-1 py-2 rounded-lg font-bold border border-[#DF1D46] text-[#DF1D46] hover:bg-[#DF1D46]/5 transition-all flex items-center justify-center gap-2" className="flex-1 py-2 rounded-lg font-bold border border-[#DF1D46] text-[#DF1D46] hover:bg-[#DF1D46]/5 transition-all flex items-center justify-center gap-2"
> >
<MessageCircle className="w-5 h-5" /> Message <MessageCircle className="w-5 h-5" /> Message
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); /* Call logic */ }} onClick={handleCall}
className="flex-1 py-2 rounded-lg font-bold bg-[#DF1D46] text-white hover:bg-[#DF1D46]/90 transition-all flex items-center justify-center gap-2" className="flex-1 py-2 rounded-lg font-bold bg-[#DF1D46] text-white hover:bg-[#DF1D46]/90 transition-all flex items-center justify-center gap-2"
> >
<Phone className="w-5 h-5" /> Call <Phone className="w-5 h-5" /> Call
</button> </button>
</> </>
) : null} ) : null}
</div> </div>
</div> </div>
<UpgradeModal isOpen={isUpgradeModalOpen} onClose={() => setIsUpgradeModalOpen(false)} />
<AnimatePresence>
{/* Accept/Reject Status Confirmation Dialog */}
{statusConfirm.open && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center">
<div className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 ${statusConfirm.status === 'accept' ? 'bg-green-50' : 'bg-red-50'}`}>
{statusConfirm.status === 'accept' ? (
<CheckCircle className="w-10 h-10 text-[#00903F]" />
) : (
<XCircle className="w-10 h-10 text-[#DF1D46]" />
)}
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-6">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">
{statusConfirm.status === 'accept'
? "Are you sure you want to accept this interest request? By continuing, your photos, contact details, and chat access will be shared with this match."
: "Are you sure you want to reject this match?"}
</p>
<div className="flex gap-4">
<button
onClick={() => setStatusConfirm({ open: false, status: null })}
className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500"
>
Cancel
</button>
<button
onClick={() => {
onStatusUpdate(profile.id, statusConfirm.status);
setStatusConfirm({ open: false, status: null });
}}
className={`flex-1 py-4 text-white rounded-xl font-bold ${statusConfirm.status === 'accept' ? 'bg-[#00903F]' : 'bg-[#DF1D46]'}`}
>
OK
</button>
</div>
</motion.div>
</div>
)}
{isViewContactModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center">
<h3 className="text-2xl font-bold text-gray-900 mb-6">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">You need to view this profile's contact details. If you choose to <span className="text-[#DF1D46] font-bold">"Proceed"</span> one count will be deducted from your subscription.</p>
<div className="flex gap-4">
<button onClick={() => setIsViewContactModalOpen(false)} className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500">Cancel</button>
<button onClick={_handleViewContact} className="flex-1 py-4 bg-[#DF1D46] text-white rounded-xl font-bold">Proceed</button>
</div>
</motion.div>
</div>
)}
{isChatConfirmModalOpen && (() => {
const currentMobile = profile.mobile_number || "";
const isMobileVisible = currentMobile !== "" && !currentMobile.toLowerCase().includes("view") && !currentMobile.toLowerCase().includes("upgrade");
return (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center">
<h3 className="text-2xl font-bold text-gray-900 mb-6">{isMobileVisible ? "Ready to Chat?" : "Note"}</h3>
<p className="text-gray-600 mb-8 leading-relaxed">{isMobileVisible ? `Are you ready to chat with ${currentMobile}?` : "Starting a conversation will use 1 chat count. Are you ready to proceed?"}</p>
<div className="flex gap-4">
<button onClick={() => setIsChatConfirmModalOpen(false)} className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500">Cancel</button>
<button onClick={_handleCreateChat} className="flex-1 py-4 bg-[#DF1D46] text-white rounded-xl font-bold">{isCreatingChat ? "..." : "Proceed"}</button>
</div>
</motion.div>
</div>
);
})()}
{isContactSuccessModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center">
<div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center mx-auto mb-6"><Phone size={40} className="text-[#0C8626]" /></div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">Success!</h3>
<p className="text-gray-500 mb-6">The contact number has been unlocked.</p>
<div className="bg-gray-50 rounded-2xl p-6 mb-8 border border-gray-100 flex items-center justify-center">
<span className="text-3xl font-black text-gray-900 tracking-wider">
{unlockedMobile || (!profile.mobile_number?.toLowerCase().includes("view") ? profile.mobile_number : "Fetching...")}
</span>
</div>
<div className="flex gap-4">
<button onClick={() => setIsContactSuccessModalOpen(false)} className="flex-1 py-4 border border-gray-200 rounded-xl font-bold text-gray-500">Close</button>
<button onClick={() => { const clean = (unlockedMobile || profile.mobile_number).replace(/[^0-9+]/g, ''); window.location.href = `tel:${clean}`; }} className="flex-1 py-4 bg-[#0C8626] text-white rounded-xl font-bold">Call Now</button>
</div>
</motion.div>
</div>
)}
{isInterestStatusModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center">
<h3 className="text-2xl font-bold text-gray-900 mb-6">Note</h3>
<p className="text-gray-600 mb-8 leading-relaxed">An interest request has already been sent for this profile. Please wait for their response.</p>
<button onClick={() => setIsInterestStatusModalOpen(false)} className="w-full py-4 bg-[#0C8626] text-white rounded-xl font-bold">OK</button>
</motion.div>
</div>
)}
{isInterestRejectedModalOpen && (
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} className="bg-white rounded-[32px] p-8 max-w-md w-full shadow-2xl text-center">
<h3 className="text-2xl font-bold text-gray-900 mb-6">Sorry</h3>
<p className="text-gray-600 mb-8 leading-relaxed">Your interest request was rejected by this user. You cannot initiate a chat at this time.</p>
<button onClick={() => setIsInterestRejectedModalOpen(false)} className="w-full py-4 bg-gray-500 text-white rounded-xl font-bold">Close</button>
</motion.div>
</div>
)}
</AnimatePresence>
</>
); );
}; };

View File

@ -1,177 +1,268 @@
// src/pages/SubscriptionHistory.jsx
import React from 'react'; import React from 'react';
import { ArrowBackIosNew, CalendarToday, AccessTime, CreditCard, Person, Visibility } from '@mui/icons-material'; import { ArrowBackIosNew, CalendarToday, AccessTime, CreditCard, Person, Visibility, History, WorkspacePremium, ReceiptLong, LocalActivity } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { CrownIcon } from 'lucide-react'; import { CrownIcon, ShieldCheck, Zap, ArrowRight, CheckCircle2 } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { getSubscriptionHistory } from '../api/subscription.api';
import { CircularProgress, Box, Typography, Chip } from '@mui/material';
const subscriptions = [ const PlanCard = ({ sub, isActive = false, index = 0 }) => {
{ if (!sub) return null;
id: "SUB722HSN",
plan: "Gold Plan", return (
profileCount: 150, <motion.div
usedCount: 70, initial={{ opacity: 0, scale: 0.9, y: 20 }}
billingCycle: "Monthly", animate={{ opacity: 1, scale: 1, y: 0 }}
expireDate: "19/11/2025", whileHover={{ y: -8, transition: { duration: 0.3 } }}
startDate: "19/10/2025", transition={{ delay: index * 0.1, duration: 0.5 }}
startTime: "10:00 AM", className={`group relative rounded-[32px] overflow-hidden border transition-all duration-500 ${
paymentMethod: "UPI Mode", isActive
amount: 1800, ? 'border-amber-300 shadow-[0_20px_50px_rgba(255,193,7,0.15)] bg-white'
isActive: true : 'border-gray-100 bg-white/80 backdrop-blur-sm shadow-xl'
}, }`}
{ >
id: "SUB722HSN", {/* Dynamic Background Pattern */}
plan: "Gold Plan", <div className={`absolute inset-0 opacity-[0.03] pointer-events-none ${isActive ? 'bg-[radial-gradient(circle_at_50%_50%,#ffc107_0%,transparent_70%)]' : ''}`} />
profileCount: 150,
usedCount: 70, {/* Decorative Gradient Glow */}
billingCycle: "Monthly", {isActive && (
expireDate: "19/11/2025", <div className="absolute -right-20 -top-20 w-40 h-40 bg-amber-400/20 blur-[80px] rounded-full pointer-events-none" />
startDate: "19/10/2025", )}
startTime: "10:00 AM",
paymentMethod: "UPI Mode", {/* Header Section */}
amount: 1800, <div className={`relative px-6 pt-12 pb-6 text-center ${isActive ? 'bg-gradient-to-b from-amber-50/50 to-transparent' : ''}`}>
isActive: false {/* Status Badge */}
}, <div className="absolute top-4 left-1/2 -translate-x-1/2 flex items-center gap-2">
{isActive ? (
{ id: "SUB722HSN", <motion.div
plan: "Gold Plan", animate={{ scale: [1, 1.05, 1] }}
profileCount: 150, transition={{ repeat: Infinity, duration: 2 }}
usedCount: 70, className="bg-gradient-to-r from-amber-500 to-orange-600 text-white px-5 py-1.5 rounded-full text-[10px] font-black tracking-widest shadow-lg flex items-center gap-2"
billingCycle: "Monthly", >
expireDate: "19/11/2025", <Zap size={12} fill="white" /> ACTIVE PLAN
startDate: "19/10/2025", </motion.div>
startTime: "10:00 AM", ) : (
paymentMethod: "UPI Mode", <div className="bg-gray-100 text-gray-500 px-4 py-1 rounded-full text-[10px] font-bold tracking-widest flex items-center gap-2">
amount: 1800, <History size={12} /> COMPLETED
isActive: false </div>
} )}
]; </div>
<h3 className="text-2xl font-black text-gray-900 mb-1">{sub.plan_name}</h3>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-[0.2em]">Transaction ID: #TK{sub.id}</p>
</div>
<div className="px-6 pb-8">
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-3 mb-6">
<div className="relative group/stat p-4 rounded-2xl bg-gray-50 border border-gray-100 transition-colors hover:bg-amber-50/50 overflow-hidden">
<div className="absolute top-0 right-0 p-2 opacity-10 group-hover/stat:opacity-20 transition-opacity">
<Person />
</div>
<p className="text-2xl font-black text-gray-900 mb-0.5">
{sub.count_of_profile_view === -1 ? '∞' : sub.count_of_profile_view}
</p>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-wider">Profile Views</p>
</div>
<div className="relative group/stat p-4 rounded-2xl bg-gray-50 border border-gray-100 transition-colors hover:bg-green-50/50 overflow-hidden">
<div className="absolute top-0 right-0 p-2 opacity-10 group-hover/stat:opacity-20 transition-opacity">
<Visibility />
</div>
<p className="text-2xl font-black text-gray-900 mb-0.5">{sub.used_count_of_profile_view || 0}</p>
<p className="text-[9px] font-bold text-gray-400 uppercase tracking-wider">Used Count</p>
</div>
</div>
{/* Benefits/Details List */}
<div className="space-y-2 mb-8">
<DetailRow icon={<CalendarToday className="text-amber-500" />} label="Activated" value={new Date(sub.supscription_start_date).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })} />
<DetailRow icon={<AccessTime className="text-red-500" />} label="Expiry" value={new Date(sub.supscription_expiry_date).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })} />
<DetailRow icon={<ShieldCheck className="text-blue-500" size={16} />} label="Duration" value={sub.plan_validity_days === -1 ? 'Lifetime Access' : `${sub.plan_validity_days} Days`} />
</div>
{/* Price Tag */}
<div className={`p-4 rounded-2xl flex items-center justify-between ${isActive ? 'bg-amber-50 border border-amber-100' : 'bg-gray-50 border border-gray-100'}`}>
<div className="flex items-center gap-2">
<div className={`p-2 rounded-lg ${isActive ? 'bg-amber-500 text-white' : 'bg-gray-200 text-gray-500'}`}>
<CreditCard fontSize="small" />
</div>
<span className="text-xs font-bold text-gray-500">Total Paid</span>
</div>
<span className={`text-xl font-black ${isActive ? 'text-amber-600' : 'text-gray-900'}`}>
{sub.plan_amount?.toLocaleString()}
</span>
</div>
</div>
</motion.div>
);
};
const DetailRow = ({ icon, label, value }) => (
<div className="flex items-center justify-between py-2.5 px-4 rounded-xl hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<span className="flex-shrink-0 opacity-80">{icon}</span>
<span className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">{label}</span>
</div>
<span className="text-xs font-bold text-gray-900">{value}</span>
</div>
);
export default function SubscriptionHistory() { export default function SubscriptionHistory() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: historyData, isLoading, error } = useQuery({
queryKey: ['subscriptionHistory'],
queryFn: getSubscriptionHistory,
});
if (isLoading) {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-white">
<motion.div
animate={{ scale: [1, 1.1, 1], rotate: 360 }}
transition={{ repeat: Infinity, duration: 2 }}
className="mb-6"
>
<WorkspacePremium sx={{ fontSize: 60, color: '#A70710' }} />
</motion.div>
<div className="w-48 h-1 bg-gray-100 rounded-full overflow-hidden relative">
<motion.div
animate={{ left: ['-100%', '100%'] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
className="absolute top-0 bottom-0 w-full bg-[#A70710]"
/>
</div>
</div>
);
}
const currentSub = historyData?.current_subscription;
const history = historyData?.subscription_history || [];
return ( return (
<div className="min-h-screen "> <div className="min-h-screen bg-[#FAFAFA] text-black">
{/* Royal Header */} {/* Glass Header */}
<motion.header <motion.header
initial={{ y: -100 }} initial={{ y: -20, opacity: 0 }}
animate={{ y: 0 }} animate={{ y: 0, opacity: 1 }}
className="bg-[#034E08] text-white shadow-2xl rounded-[10px]" className="sticky top-0 z-[100] bg-white/80 backdrop-blur-xl border-b border-gray-100"
> >
<div className="flex items-center justify-between px-5 py-8 safe-area-top relative"> <div className="max-w-2xl mx-auto px-4 h-20 flex items-center justify-between">
<button
<h1 className="text-2xl font-bold">Subscription History</h1> onClick={() => navigate(-1)}
<div className="w-12" /> className="w-10 h-10 flex items-center justify-center rounded-2xl hover:bg-gray-100 transition-colors text-gray-600"
>
<ArrowBackIosNew fontSize="small" />
</button>
<div className="text-center">
<h1 className="text-lg font-black tracking-tight">Subscriptions</h1>
<p className="text-[9px] font-bold text-amber-600 tracking-[0.3em] uppercase">Thirukalyanam Premium</p>
</div>
<div className="w-10 h-10 flex items-center justify-center rounded-2xl bg-amber-50 text-amber-600">
<ReceiptLong size={20} />
</div>
</div> </div>
</motion.header> </motion.header>
{/* Current Plan Title */} <main className="max-w-7xl mx-auto px-4 py-10 pb-32">
<div className="px-6 pt-8"> {/* Hero Active Section */}
<motion.h2 <div className="mb-16">
initial={{ opacity: 0, x: -50 }} <div className="flex items-center justify-between mb-8">
animate={{ opacity: 1, x: 0 }} <div className="flex items-center gap-4">
className="text-3xl font-bold text-gray-900 flex items-center gap-3" <div className="w-14 h-14 rounded-[20px] bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white shadow-lg shadow-orange-100">
> <WorkspacePremium fontSize="large" />
<CrownIcon className="text-amber-500 text-4xl" /> </div>
Current Plan <div>
</motion.h2> <h2 className="text-xl font-black text-gray-900">Current Status</h2>
</div> <p className="text-xs font-bold text-gray-400 uppercase tracking-widest">Active Plan Details</p>
</div>
</div>
{currentSub && (
<div className="bg-green-50 text-green-600 px-4 py-2 rounded-xl hidden sm:flex items-center gap-2 border border-green-100">
<CheckCircle2 size={16} />
<span className="text-[10px] font-black uppercase tracking-wider">Verified Membership</span>
</div>
)}
</div>
<div className=''> {currentSub ? (
{/* Subscription Cards */} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="px-2 py-8 pb-32 grid grid-cols-1 md:grid-cols-3 gap-2"> <PlanCard sub={currentSub} isActive={true} />
{subscriptions.map((sub, index) => ( <div className="hidden lg:block lg:col-span-2 bg-gradient-to-br from-gray-900 to-black rounded-[40px] p-10 relative overflow-hidden text-white shadow-2xl">
<motion.div <div className="absolute top-0 right-0 p-10 opacity-20 scale-150">
key={index} <CrownIcon size={120} />
initial={{ opacity: 0, y: 50 }} </div>
animate={{ opacity: 1, y: 0 }} <div className="relative z-10 h-full flex flex-col justify-center">
transition={{ delay: index * 0.2 }} <h3 className="text-3xl font-black mb-4 tracking-tight">Premium Benefits Active</h3>
className={`relative rounded-3xl overflow-hidden border border-1 border-gray-200 w-full max-w-md ${ <p className="text-gray-400 mb-8 max-w-md leading-relaxed text-sm">
sub.isActive You are currently enjoying the full suite of Thirukalyanam premium features.
? 'border-amber-400 bg-gradient-to-br from-amber-50 to-pink-50' Your account is prioritized in searches and you have direct access to contact details.
: 'border-pink-200 bg-gradient-to-br from-pink-50 to-rose-50 opacity-90' </p>
}`} <div className="flex flex-wrap gap-3">
> {['Verified Profile', 'Priority Search', 'Direct Contact', 'Chat Enabled'].map((item) => (
{/* Crown Badge */} <div key={item} className="px-4 py-2 bg-white/10 backdrop-blur-md rounded-xl text-[10px] font-bold tracking-widest uppercase flex items-center gap-2">
<div className={` text-[12px] absolute z-[999] -top-1 left-1/2 -translate-x-1/2 px-8 py-2 rounded-b-3xl font-bold text-white shadow-xl flex items-center gap-2 ${ <CheckCircle2 size={12} className="text-green-400" /> {item}
sub.isActive ? 'bg-gradient-to-r from-amber-500 to-red-600' : 'bg-gradient-to-r from-pink-500 to-rose-600' </div>
}`}> ))}
<CrownIcon className="text-[12px]" /> </div>
{sub.plan} </div>
{sub.isActive && <span className="ml-2 text-[12px] bg-green-400 text-black px-3 py-1 rounded-full">ACTIVE</span>}
</div>
<div className="pt-14 pb-8 px-8">
{/* Subscription ID */}
<div className="text-center mb-6">
<p className="text-sm text-gray-600">Subscription ID</p>
<p className="text-[18px] font-bold text-gray-900">{sub.id}</p>
</div>
{/* Stats Row */}
<div className="grid grid-cols-2 gap-6 mb-4">
<div className="bg-white/70 backdrop-blur rounded-2xl p-5 text-center ">
<Person className="text-4xl text-amber-600 mx-auto mb-2" />
<p className="text-[18px] font-bold text-gray-900">{sub.profileCount}</p>
<p className="text-sm text-gray-600">Profile Count</p>
</div>
<div className="bg-white/70 backdrop-blur rounded-2xl p-5 text-center ">
<Visibility className="text-4xl text-green-600 mx-auto mb-2" />
<p className="text-[18px] font-bold text-gray-900">{sub.usedCount}</p>
<p className="text-sm text-gray-600">Used Count</p>
</div>
</div>
{/* Details Grid */}
<div className="space-y-2 text-lg">
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<CalendarToday className="text-amber-600" /> Billing Cycle
</span>
<span className="font-bold text-gray-900 text-[14px]">{sub.billingCycle}</span>
</div>
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<CalendarToday className="text-red-600" /> Expire Date
</span>
<span className="font-bold text-red-600 text-[14px]">{sub.expireDate}</span>
</div>
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<AccessTime className="text-green-600" /> Start Date & Time
</span>
<span className="font-bold text-gray-900 text-[14px]">{sub.startDate} | {sub.startTime}</span>
</div>
<div className="flex justify-between items-center bg-white/60 backdrop-blur rounded-xl px-5 py-3">
<span className="flex items-center gap-3 text-gray-700 text-[14px]">
<CreditCard className="text-purple-600" /> Payment Method
</span>
<span className="font-bold text-purple-700 text-[14px]">{sub.paymentMethod}</span>
</div>
</div>
{/* Total Amount */}
<div className="mt-8 text-center flex gap-4 items-center justify-center flex-wrap">
<p className="text-lg text-gray-600">Total Plan Amount</p>
<p className="text-[18px] font-bold text-gray-900 bg-white/70 backdrop-blur px-4 py-2 rounded-[10px]">
{sub.amount.toLocaleString()}
</p>
</div> </div>
</div> </div>
</motion.div> ) : (
))} <motion.div
</div> initial={{ opacity: 0, y: 20 }}
</div> animate={{ opacity: 1, y: 0 }}
{/* Final Message */} className="p-10 rounded-[40px] bg-white border-2 border-dashed border-gray-100 text-center max-w-xl mx-auto"
{/* <motion.div >
initial={{ opacity: 0 }} <div className="w-20 h-20 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-6">
animate={{ opacity: 1 }} <ShieldCheck size={40} className="text-gray-200" />
transition={{ delay: 0.6 }} </div>
className="fixed bottom-0 left-0 right-0 bg-gradient-to-r from-red-600 to-rose-700 text-white py-8 text-center shadow-2xl" <h3 className="text-lg font-black text-gray-900 mb-2">No Active Membership</h3>
> <p className="text-sm text-gray-400 mb-8 max-w-[240px] mx-auto leading-relaxed">
<p className="text-xl font-bold">Your journey to forever is fully powered</p> Unlock premium matchmaking features to find your soulmate faster.
<p className="text-green-200 mt-2">Enjoy unlimited matches with your Gold Plan</p> </p>
</motion.div> */} <button
onClick={() => navigate('/subscription-plan')}
className="group relative w-full py-4 bg-gray-900 text-white rounded-2xl font-black flex items-center justify-center gap-2 overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-amber-400 to-orange-500 translate-y-full group-hover:translate-y-0 transition-transform duration-500" />
<span className="relative z-10">UPGRADE NOW</span>
<ArrowRight size={18} className="relative z-10 transition-transform group-hover:translate-x-1" />
</button>
</motion.div>
)}
</div>
{/* Timeline History Section */}
<div className="relative">
<div className="flex items-center gap-4 mb-8">
<div className="w-14 h-14 rounded-[20px] bg-white shadow-xl flex items-center justify-center text-gray-400">
<History fontSize="large" />
</div>
<div>
<h2 className="text-xl font-black text-gray-900">Purchase History</h2>
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest">Your payment journey</p>
</div>
</div>
{history.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{history.map((sub, index) => (
<PlanCard key={sub.id} sub={sub} index={index + 1} />
))}
</div>
) : (
<div className="py-20 text-center">
<div className="inline-block p-6 rounded-full bg-gray-50 mb-4">
<LocalActivity className="text-gray-200" sx={{ fontSize: 40 }} />
</div>
<p className="text-sm font-bold text-gray-300 uppercase tracking-widest">No Previous History</p>
</div>
)}
</div>
</main>
</div> </div>
); );
} }

View File

@ -1,64 +1,177 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ArrowBackIosNew, Check, Star, AutoAwesome } from '@mui/icons-material'; import { ArrowBackIosNew, Check, Star, AutoAwesome, InfoOutlined } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getSubscriptionPlans, purchaseSubscription } from '../api/subscription.api';
import { toast } from 'react-hot-toast';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress } from '@mui/material';
const loadRazorpayScript = () => {
return new Promise((resolve) => {
if (window.Razorpay) {
resolve(true);
return;
}
const script = document.createElement("script");
script.src = "https://checkout.razorpay.com/v1/checkout.js";
script.onload = () => resolve(true);
script.onerror = () => resolve(false);
document.body.appendChild(script);
});
};
export default function SubscriptionPlan() { export default function SubscriptionPlan() {
const navigate = useNavigate(); const navigate = useNavigate();
const [isAnnual, setIsAnnual] = useState(true); const queryClient = useQueryClient();
const [purchaseLoading, setPurchaseLoading] = useState(false);
const [confirmDialog, setConfirmDialog] = useState({ open: false, plan: null });
const plans = [ const { data: plansData, isLoading, error } = useQuery({
{ queryKey: ['subscriptionPlans'],
name: "Gold Plan", queryFn: getSubscriptionPlans,
monthlyPrice: 1800, });
annualPrice: 1299,
color: "from-amber-400 to-yellow-600", const plans = plansData?.data || [];
button: "from-amber-500 to-orange-600",
features: [ const handlePurchase = async (plan) => {
"Unlimited Profile Views", setPurchaseLoading(true);
"Send Personalized Messages", try {
"Featured Profile for 30 Days", console.log("Initiating purchase for plan:", plan.id);
"Priority Customer Support", const res = await purchaseSubscription(plan.id);
"Verified Badge", console.log("Purchase Initiation Response:", res);
"Horoscope Matching",
"100+ Daily Matches" // Support both res.success (older API) and res.status === 'success' (newer API)
] if (res.success || res.status === 'success') {
}, const data = res.data;
{
name: "Diamond Plan", // Ensure Razorpay script is loaded
monthlyPrice: 2800, const isLoaded = await loadRazorpayScript();
annualPrice: 1999, if (!isLoaded || !window.Razorpay) {
color: "from-purple-500 to-pink-600", toast.error("Razorpay SDK failed to load. Please check your internet connection.");
button: "from-purple-600 to-pink-700", setPurchaseLoading(false);
popular: true, return;
features: [ }
"Everything in Gold",
"Top of Search Results", if (!data.order_id || !data.key_id) {
"Profile Highlight in Gold Border", toast.error("Invalid order details received from server.");
"Personal Relationship Manager", setPurchaseLoading(false);
"Video Call with Matches", return;
"Astro Compatibility Report", }
"200+ Premium Matches Daily"
] const options = {
}, key: data.key_id,
{ amount: data.amount,
name: "Platinum Plan", currency: "INR",
monthlyPrice: 4800, name: "Thirukalyanam",
annualPrice: 3499, description: data.plan_name || plan.plan_name,
color: "from-emerald-500 to-teal-600", order_id: data.order_id,
button: "from-emerald-600 to-teal-700", handler: async (response) => {
features: [ try {
"Everything in Diamond", toast.loading("Verifying payment...", { id: "verify-payment" });
"Guaranteed 10+ Interests/Month", const verifyRes = await purchaseSubscription(plan.id, {
"Exclusive VIP Events Invite", razorpay_payment_id: response.razorpay_payment_id,
"Profile Boost Every Week", razorpay_order_id: response.razorpay_order_id,
"Family Member Login Access", razorpay_signature: response.razorpay_signature,
"Lifetime Profile Visibility", });
"Dedicated Matchmaking Expert"
] if (verifyRes.success || verifyRes.status === 'success') {
toast.success("Subscription activated successfully!", { id: "verify-payment" });
queryClient.invalidateQueries(['subscriptionPlans']);
queryClient.invalidateQueries(['headerDetails']);
queryClient.invalidateQueries(['dashboardDetails']);
} else {
toast.error(verifyRes.message || "Verification failed", { id: "verify-payment" });
}
} catch (err) {
console.error("Verification Error:", err);
toast.error("Verification failed", { id: "verify-payment" });
} finally {
setPurchaseLoading(false);
}
},
prefill: {
name: data.name || "",
email: data.email || "",
contact: data.mobile || data.mobile_number || "",
},
theme: {
color: "#A70710",
},
modal: {
ondismiss: function() {
console.log("Payment modal closed by user");
setPurchaseLoading(false);
}
}
};
console.log("Opening Razorpay with options:", options);
const rzp = new window.Razorpay(options);
rzp.open();
} else {
toast.error(res.message || "Failed to initiate purchase");
setPurchaseLoading(false);
}
} catch (err) {
console.error("Purchase Error:", err);
toast.error(err.response?.data?.message || "Something went wrong during purchase initiation");
setPurchaseLoading(false);
} }
]; };
const getPlanFeatures = (plan) => {
const features = [
{ text: `Profile Views: ${plan.count_of_profile_view === -1 ? 'Unlimited' : plan.count_of_profile_view}`, available: true },
{ text: `Validity: ${plan.plan_validity_days === -1 ? 'Unlimited' : `${plan.plan_validity_days} Days`}`, available: true },
{ text: "View Mobile Number", available: plan.can_view_mobile_number },
{ text: "View Email ID", available: plan.can_view_email },
{ text: "View Raasi", available: plan.can_view_raasi },
{ text: "View Star", available: plan.can_view_star },
{ text: "View Father Name", available: plan.can_view_father_name },
{ text: "View Mother Name", available: plan.can_view_mother_name },
{ text: "View DOB", available: plan.can_view_dob },
{ text: "View TOB", available: plan.can_view_tob },
{ text: "Start Chat", available: plan.can_start_chat },
{ text: "Send Interest", available: plan.can_send_interest },
{ text: "Accept Interest", available: plan.can_accept_interest },
];
return features;
};
const getPlanColors = (index) => {
const colors = [
{ color: "from-amber-400 to-yellow-600", button: "bg-[#A70710]" },
{ color: "from-purple-500 to-pink-600", button: "bg-[#A70710]" },
{ color: "from-emerald-500 to-teal-600", button: "bg-[#A70710]" },
{ color: "from-blue-500 to-indigo-600", button: "bg-[#A70710]" },
];
return colors[index % colors.length];
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<CircularProgress sx={{ color: '#A70710' }} />
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center text-red-500 text-center px-4">
<div>
<p className="text-xl font-bold mb-2">Error loading plans</p>
<p>Please check your connection or try again later.</p>
</div>
</div>
);
}
return ( return (
<div className="min-h-screen mt-4 text-black overflow-hidden relative z-20"> <div className="min-h-screen mt-4 text-black overflow-hidden relative z-20">
@ -70,113 +183,130 @@ export default function SubscriptionPlan() {
> >
<div className="absolute inset-0 bg-[#034E08] rounded-[15px]" /> <div className="absolute inset-0 bg-[#034E08] rounded-[15px]" />
<div className="relative flex items-center justify-between px-5 py-8 safe-area-top"> <div className="relative flex items-center justify-between px-5 py-8 safe-area-top">
<button onClick={() => navigate(-1)} className="text-white">
<ArrowBackIosNew />
</button>
<h1 className="text-2xl font-bold text-center text-white">Subscription Plan</h1> <h1 className="text-2xl font-bold text-center text-white">Subscription Plan</h1>
<div className="w-12" /> <div className="w-12" />
</div> </div>
</motion.header> </motion.header>
{/* Annual / Monthly Toggle */}
<div className="flex justify-center -mt-6 relative z-10">
<motion.div
whileTap={{ scale: 0.95 }}
className="bg-[#A70710] backdrop-blur-xl rounded-full p-2 flex gap-2 shadow-2xl"
// style={{background:"linear-gradient(98.05deg, #FAFBFF 0%, #FCC4FF 100%)"}}
>
<button
onClick={() => setIsAnnual(true)}
className={`px-8 py-4 rounded-full font-bold text-lg transition-all ${isAnnual ? 'bg-white text-purple-900 shadow-lg' : 'text-white/80'}`}
>
ANNUAL PLAN
<span className="block text-xs font-normal mt-1 text-green-300">Save up to 40%</span>
</button>
<button
onClick={() => setIsAnnual(false)}
className={`px-8 py-4 rounded-full font-bold text-lg transition-all ${!isAnnual ? 'bg-white text-purple-900 shadow-lg' : 'text-white/80'}`}
>
MONTHLY PLAN
</button>
</motion.div>
</div>
{/* Pricing Cards */} {/* Pricing Cards */}
<div className="px-5 py-10 max-w-7xl mx-auto"> <div className="px-5 py-10 max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{plans.map((plan, index) => ( {plans.map((plan, index) => {
<motion.div const planColors = getPlanColors(index);
key={index} const features = getPlanFeatures(plan);
initial={{ opacity: 0, y: 50 }} const isBuyable = plan.is_can_buyable === 1 && !plan.is_current_plan;
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.2 }}
className={`relative backdrop-blur-xl rounded-3xl overflow-hidden border ${
plan.popular ? 'border-yellow-400 shadow-2xl shadow-yellow-500/30 scale-105' : 'border-white/30'
}`}
style={{background:"linear-gradient(98.05deg, #FAFBFF 0%, #FCC4FF 100%)"}} return (
> <motion.div
{plan.popular && ( key={plan.id}
<div className="absolute -top-1 left-1/2 -translate-x-1/2 bg-gradient-to-r from-yellow-400 to-amber-500 text-purple-900 px-8 py-2 rounded-b-2xl font-bold text-sm flex items-center gap-2"> initial={{ opacity: 0, y: 50 }}
<AutoAwesome className="text-lg" /> MOST POPULAR <AutoAwesome className="text-lg" /> animate={{ opacity: 1, y: 0 }}
</div> transition={{ delay: index * 0.1 }}
)} className={`relative backdrop-blur-xl rounded-3xl overflow-hidden border ${
plan.is_current_plan ? 'border-yellow-400 shadow-2xl scale-105' : 'border-white/30'
<div className="mt-6 p-8 text-center"> }`}
<h3 className="text-3xl font-bold mb-4">{plan.name}</h3> style={{ background: "linear-gradient(98.05deg, #FAFBFF 0%, #FCC4FF 100%)" }}
<div className="text-[25px] font-bold mb-2"> >
{isAnnual ? plan.annualPrice.toLocaleString() : plan.monthlyPrice.toLocaleString()} {plan.is_current_plan && (
<span className="text-[18px] font-normal text-black"> /{isAnnual ? 'month' : 'month'}</span> <div className="absolute -top-1 left-1/2 -translate-x-1/2 bg-gradient-to-r from-yellow-400 to-amber-500 text-purple-900 px-8 py-2 rounded-b-2xl font-bold text-sm flex items-center gap-2">
</div> <Star className="text-lg" /> CURRENT PLAN <Star className="text-lg" />
{isAnnual && ( </div>
<p className="text-[#00903F] font-bold text-lg mb-6">
Billed annually @ {(plan.annualPrice * 12).toLocaleString()}
</p>
)} )}
<motion.button <div className="mt-6 p-8 text-center">
whileHover={{ scale: 1.05 }} <h3 className="text-2xl font-bold mb-4">{plan.plan_name}</h3>
whileTap={{ scale: 0.95 }} <div className="text-[32px] font-bold mb-2">
className={`w-[fit-content] py-4 px-8 rounded-full text-white font-semibold text-[16px] shadow-xl bg-gradient-to-r ${plan.button} hover:shadow-2xl transition-all`} {plan.plan_amount}
style={{background:"#A70710"}} <span className="text-[18px] font-normal text-gray-600">
> /{plan.plan_validity_days === -1 ? 'Lifetime' : `${plan.plan_validity_days} days`}
Select Plan </span>
</motion.button> </div>
<div className="mt-8 text-left space-y-4"> <motion.button
<h4 className="text-xl font-bold text-black">Features</h4> disabled={!isBuyable || purchaseLoading}
{plan.features.map((feature, i) => ( whileHover={isBuyable ? { scale: 1.05 } : {}}
<div key={i} className="flex items-center gap-3"> whileTap={isBuyable ? { scale: 0.95 } : {}}
<Check className="text-green-400 text-2xl" /> onClick={() => setConfirmDialog({ open: true, plan })}
<span className="text-black">{feature}</span> className={`w-full py-4 px-8 rounded-xl text-white font-semibold text-[16px] shadow-xl transition-all ${
isBuyable ? planColors.button : 'bg-gray-400 cursor-not-allowed opacity-70'
}`}
>
{purchaseLoading ? <CircularProgress size={24} color="inherit" /> :
plan.is_current_plan ? "Current Plan" : "Select Plan"}
</motion.button>
<div className="mt-8 text-left space-y-3">
<h4 className="text-lg font-bold text-black border-b border-gray-200 pb-2">Features</h4>
<div className="max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
{features.map((feature, i) => (
<div key={i} className="flex items-center gap-3 py-1">
<div className={`rounded-full p-0.5 ${feature.available ? 'bg-green-100' : 'bg-red-100'}`}>
<Check className={`text-xl ${feature.available ? 'text-green-600' : 'text-red-600'}`}
sx={{ fontSize: 16, visibility: feature.available ? 'visible' : 'hidden' }} />
{!feature.available && <span className="w-4 h-4 flex items-center justify-center text-red-600 font-bold text-xs"></span>}
</div>
<span className={`text-sm ${feature.available ? 'text-black' : 'text-gray-400 line-through'}`}>
{feature.text}
</span>
</div>
))}
</div> </div>
))} </div>
</div> </div>
</div> </motion.div>
</motion.div> );
))} })}
</div> </div>
{/* Final CTA */} <div className="text-center mt-12 mb-8">
{/* <motion.div <p className="text-gray-600 max-w-2xl mx-auto italic">
initial={{ opacity: 0 }} Choose the plan that's right for you. Find your perfect life partner with our premium plans.
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
className="text-center mt-16"
>
<h2 className="text-3xl font-bold mb-4">Find Your Perfect Life Partner</h2>
<p className="text-xl text-white/80 max-w-2xl mx-auto mb-10">
Choose the plan thats right for your sacred journey. Every premium member gets closer to their soulmate.
</p> </p>
<motion.button </div>
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="bg-gradient-to-r from-red-600 to-rose-700 px-12 py-6 rounded-full text-2xl font-bold shadow-2xl hover:shadow-red-500/50 transition-all"
>
Choose This Plan
</motion.button>
</motion.div> */}
</div> </div>
<div className="h-32" /> {/* Confirmation Dialog */}
<Dialog
open={confirmDialog.open}
onClose={() => setConfirmDialog({ open: false, plan: null })}
PaperProps={{
sx: { borderRadius: '20px', padding: '10px' }
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1, color: '#1B5E20', fontWeight: 'bold' }}>
<InfoOutlined /> Note
</DialogTitle>
<DialogContent>
<p className="text-[#DF1D46] font-semibold text-center text-lg">
If you choose a new plan, your current plan will expire and the new plan will be activated.
</p>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center', gap: 2, pb: 3 }}>
<Button
onClick={() => setConfirmDialog({ open: false, plan: null })}
variant="outlined"
sx={{ borderRadius: '10px', color: 'gray', borderColor: 'gray' }}
>
Cancel
</Button>
<Button
onClick={() => {
const plan = confirmDialog.plan;
setConfirmDialog({ open: false, plan: null });
handlePurchase(plan);
}}
variant="contained"
sx={{ borderRadius: '10px', bgcolor: '#A70710', '&:hover': { bgcolor: '#8e060d' } }}
>
Ok
</Button>
</DialogActions>
</Dialog>
<div className="h-20" />
</div> </div>
); );
} }

View File

@ -18,6 +18,8 @@ const MatrimonyArticles = lazy(() => import("../components/profiledashboard/Matr
const MatchingList = lazy(() => import("../components/profiledashboard/MatchingList")); const MatchingList = lazy(() => import("../components/profiledashboard/MatchingList"));
const VideoSwiperGallery = lazy(() => import("../components/profiledashboard/VideoSwiperGallery")); const VideoSwiperGallery = lazy(() => import("../components/profiledashboard/VideoSwiperGallery"));
const Profilecardemo = lazy(() => import("../components/ui/ProfileCardDemo")); const Profilecardemo = lazy(() => import("../components/ui/ProfileCardDemo"));
const DailyRecommendedCard = lazy(() => import("../components/profiledashboard/DailyRecommendedCard"));
const images = [ const images = [
@ -130,97 +132,95 @@ const UserDashboardHome = () => {
<style>{` <style>{`
.custom-swiper-hero .swiper { .custom-swiper-hero .swiper {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
/* ================= MOBILE (≤768px) ================= */ /* ================= MOBILE (≤768px) ================= */
@media (max-width: 768px) { @media (max-width: 768px) {
.custom-swiper-hero .swiper { .custom-swiper-hero .swiper {
overflow: hidden; overflow: hidden;
} }
.custom-swiper-hero .swiper-slide { .custom-swiper-hero .swiper-slide {
width: 100% !important; width: 100% !important;
height: 200px; height: 200px;
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
} }
.custom-swiper-hero .swiper-slide-active { .custom-swiper-hero .swiper-slide-active {
width: 100% !important; width: 100% !important;
height: 200px !important; height: 200px !important;
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
} }
} }
/* ================= TABLET & DESKTOP (≥769px) ================= */ /* ================= TABLET & DESKTOP (≥769px) ================= */
@media (min-width: 769px) { @media (min-width: 769px) {
.custom-swiper-hero .swiper { .custom-swiper-hero .swiper {
overflow: visible; overflow: visible;
} }
.custom-swiper-hero .swiper-wrapper { .custom-swiper-hero .swiper-wrapper {
align-items: center; align-items: center;
} }
.custom-swiper-hero .swiper-slide { .custom-swiper-hero .swiper-slide {
width: 320px; width: 320px;
height: 320px; height: 320px;
transform: scale(1); /* ❌ removed zoom out */ transform: scale(1); /* ❌ removed zoom out */
opacity: 1; /* ❌ removed fade */ opacity: 1; /* ❌ removed fade */
transition: all 0.4s ease; transition: all 0.4s ease;
} }
.custom-swiper-hero .swiper-slide-active { .custom-swiper-hero .swiper-slide-active {
width: 630px !important; /* center slide bigger */ width: 630px !important; /* center slide bigger */
height: 320px !important; height: 320px !important;
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
z-index: 2; z-index: 2;
} }
} }
/* ================= COMMON ================= */ /* ================= COMMON ================= */
.custom-swiper-hero .swiper-slide > div { .custom-swiper-hero .swiper-slide > div {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.custom-swiper-hero .swiper-slide img { .custom-swiper-hero .swiper-slide img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
/* Desktop Styles */ /* Desktop Styles */
// @media (min-width: 1024px) { // @media (min-width: 1024px) {
// .custom-swiper-hero .swiper-slide { // .custom-swiper-hero .swiper-slide {
// width: 100%; // width: 100%;
// height: 400px; /* Adjust the height for desktop */ // height: 400px; /* Adjust the height for desktop */
// } // }
// .custom-swiper-hero .swiper-slide-active { // .custom-swiper-hero .swiper-slide-active {
// width: 100% !important; // width: 100% !important;
// height: 400px !important; // height: 400px !important;
// } // }
// } // }
`}</style> `}</style>
</div> </div>
<Suspense fallback={<SectionFallback height={320} />}> <Suspense fallback={<SectionFallback height={320} />}>
<Profilecardemo profiles={dashboardData?.daily_recommended} /> <DailyRecommendedCard profiles={dashboardData?.daily_recommended} />
</Suspense> </Suspense>
{/* <DailyRecommendedCard/> */}
<Suspense fallback={<SectionFallback height={220} />}> <Suspense fallback={<SectionFallback height={220} />}>
<ProfileCompletion <ProfileCompletion
percentage={dashboardData?.profile_complete_percentage} percentage={dashboardData?.profile_complete_percentage}
@ -228,7 +228,8 @@ const UserDashboardHome = () => {
becomePaidMember={dashboardData?.become_paid_member} becomePaidMember={dashboardData?.become_paid_member}
/> />
</Suspense> </Suspense>
{/* <DailyRecommendedCard/> */}
<Suspense fallback={<SectionFallback height={280} />}> <Suspense fallback={<SectionFallback height={280} />}>
<MatrimonyArticles articles={dashboardData?.blogs} /> <MatrimonyArticles articles={dashboardData?.blogs} />

View File

@ -18,7 +18,8 @@ const initialState = {
district: [], district: [],
diet: "", diet: "",
family_type: [], family_type: [],
filter_type: "all_matches", filter_type: "",
page: 1, page: 1,
isPaidMember: false, isPaidMember: false,
search: "", search: "",

View File

@ -73,8 +73,16 @@ const registrationformSlice = createSlice({
willingToGoAbroad: "", willingToGoAbroad: "",
}, },
lifestyleDetails: { lifestyleDetails: {
diets: [], dayOfBirth: "",
hobbies: [], raasi: "",
star: "",
patham: "",
lagnam: "",
panjangam_type: "",
dasa_balance: "",
dasa_years: "",
dasa_months: "",
dasa_days: "",
dob: "", dob: "",
tob: "", tob: "",
placeOfBirth: "", placeOfBirth: "",
@ -108,13 +116,22 @@ const registrationformSlice = createSlice({
}, },
}, },
partnerPreferences: { partnerPreferences: {
ageRange: "", age_from: "",
age_to: "",
height_from: "",
height_to: "",
marital_statuses: [],
birth_stars: [],
castes: [], castes: [],
subCastes: [], sub_castes: [],
occupations: [],
educations: [], educations: [],
hobbies: [], occupations: [],
annualIncome: "", employee_types: [],
currencies: [],
inr_from: "",
inr_to: "",
usd_from: "",
usd_to: "",
states: [], states: [],
districts: [], districts: [],
}, },
@ -158,15 +175,20 @@ const registrationformSlice = createSlice({
} }
if (step <= 4) { if (step <= 4) {
state.lifestyleDetails = { state.lifestyleDetails = {
diets: "", hobbies: [], dob: "", tob: "", placeOfBirth: "", dayOfBirth: "", raasi: "", star: "", patham: "", lagnam: "", panjangam_type: "",
dasa_balance: "", dasa_years: "", dasa_months: "", dasa_days: "",
dob: "", tob: "", placeOfBirth: "",
graha: { 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [], 8: [], 9: [], 10: [], 11: [], 12: [] }, 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: [] }, amsam: { 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 7: [], 8: [], 9: [], 10: [], 11: [], 12: [] },
}; };
} }
if (step <= 5) { if (step <= 5) {
state.partnerPreferences = { state.partnerPreferences = {
ageRange: "", castes: [], subCastes: [], occupations: [], educations: [], age_from: "", age_to: "", height_from: "", height_to: "",
hobbies: [], annualIncome: "", states: [], districts: [], marital_statuses: [], birth_stars: [], castes: [], sub_castes: [],
educations: [], occupations: [], employee_types: [],
currencies: [], inr_from: "", inr_to: "", usd_from: "", usd_to: "",
states: [], districts: [],
}; };
} }
}, },
@ -266,8 +288,16 @@ preloadDummyProfile: (state) => {
}; };
state.lifestyleDetails = { state.lifestyleDetails = {
...state.lifestyleDetails, ...state.lifestyleDetails,
diets: [1], dayOfBirth: "Monday",
hobbies: [1, 3], raasi: 1,
star: 1,
patham: "1",
lagnam: 1,
panjangam_type: "Thirukanitham",
dasa_balance: "SURIYAN",
dasa_years: "5",
dasa_months: "2",
dasa_days: "10",
dob: "1995-05-01", dob: "1995-05-01",
tob: "09:30", tob: "09:30",
placeOfBirth: "Chennai", placeOfBirth: "Chennai",
@ -302,13 +332,20 @@ preloadDummyProfile: (state) => {
}; };
state.partnerPreferences = { state.partnerPreferences = {
...state.partnerPreferences, ...state.partnerPreferences,
ageRange: 2, age_from: 25,
age_to: 30,
height_from: 1,
height_to: 10,
marital_statuses: [11],
birth_stars: [1, 2],
castes: [1], castes: [1],
subCastes: [1], sub_castes: [1],
occupations: [57], occupations: [57],
educations: [14], educations: [14],
hobbies: [1, 3], employee_types: [3],
annualIncome: 1, currencies: ["INR"],
inr_from: "100000",
inr_to: "120000",
states: [31], states: [31],
districts: [1], districts: [1],
}; };

View File

@ -28,7 +28,9 @@ export const store = configureStore({
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({
serializableCheck: { serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, "registerform/updatePersonalDetails"],
ignoredActionPaths: ["payload.profiles", "payload.file"],
ignoredPaths: ["registerform.personalDetails.profiles"],
}, },
}), }),
}); });

View File

@ -55,8 +55,11 @@ const UserRoutes = () => {
<Route element={<ProfileLayout />}> <Route element={<ProfileLayout />}>
<Route path="/chat" element={<ChatUI />} /> <Route path="/chat" element={<ChatUI />} />
<Route path="/chat/:chatId" element={<ChatUI />} />
</Route> </Route>
<Route element={<ProfileLayout />}> <Route element={<ProfileLayout />}>
<Route path="/horoscoper-generate" element={<HoroscopeGenerator />} /> <Route path="/horoscoper-generate" element={<HoroscopeGenerator />} />
</Route> </Route>

View File

@ -51,15 +51,13 @@ export const getInterestList = async (tab, type) => {
} }
}; };
export const updateInterestStatus = async (profile_id, status) => { export const updateInterestStatus = async (id, status) => {
try { try {
const response = await axiosInstance.post(API_ENDPOINTS.UPDATE_INTEREST_STATUS, { const response = await axiosInstance.post(`${API_ENDPOINTS.UPDATE_INTEREST_STATUS}?id=${id}&status=${status}`);
profile_id,
status
});
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error("Error updating interest status:", error); console.error("Error updating interest status:", error);
throw error; throw error;
} }
}; };

View File

@ -15,5 +15,14 @@ export default defineConfig({
"@": path.resolve(__dirname, "src"), "@": path.resolve(__dirname, "src"),
}, },
}, },
server: {
proxy: {
'/backend': {
target: 'https://www.thirukalyanam.amrithaa.net',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/backend/, '/backend') // keep /backend
}
}
}
}) })