thirukalyanamweb/src/components/common/ProfileCardUI.jsx

585 lines
24 KiB
JavaScript

import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
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 }) {
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 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
const imageSrc = profile.photo || profile.image || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
return (
<>
<UpgradeModal
isOpen={isUpgradeModalOpen}
onClose={() => setIsUpgradeModalOpen(false)}
/>
{/* 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 */}
{profile.isPremium && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
className="absolute top-4 left-4 z-10 bg-red-900 rounded-full p-2 shadow-lg"
>
<Crown className="w-5 h-5 text-white" />
</motion.div>
)}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleShortlistClick}
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 transition-colors ${isShortlisted ? "text-black fill-black" : "text-gray-600"}`} />
<span className="text-[12px] font-medium text-gray-700">
{isShortlisted ? "Shortlisted" : "Shortlist"}
</span>
</motion.button>
<div className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]">
<img
src={imageSrc}
alt={profile.name}
className="w-full h-full object-cover bg-gray-200"
style={{ objectPosition: "top" }}
onError={(e) => {
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
}}
/>
</div>
{/* Gradient Overlay */}
<div className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none" style={{ background: "linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%)" }}></div>
<div className="absolute bottom-0 left-0 right-0 p-6 pb-2 text-gray-900">
<h1 className="text-[18px] text-green-900 font-bold mb-1 truncate">{profile.name}</h1>
<p className="text-[14px] text-gray-700 leading-relaxed font-medium">ID: {profile.member_id || profile.id}</p>
</div>
</div>
<div className="px-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="text-left mb-4">
<h2 className="text-[20px] font-bold text-gray-900 leading-tight">
{profile.name}
</h2>
<div className="flex items-center justify-start gap-3 mt-1.5 text-[11px] font-semibold text-gray-400 uppercase tracking-wider">
<span>ID: {profile.member_id || profile.id}</span>
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
<span className="flex items-center gap-1">
<VisibilityIcon sx={{ fontSize: 14 }} />
{profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"}
</span>
</div>
</div>
<div className="space-y-3 mt-2 flex-1">
{(profile.age || profile.height) && (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-50 flex items-center justify-center flex-shrink-0">
<img src={personIcon} alt="Person" className="w-4 h-4 opacity-80" />
</div>
<span className="text-[13px] font-bold text-gray-700">
{profile.age ? `${profile.age} yrs` : ""}
{profile.age && profile.height ? ", " : ""}
{profile.height ? `${profile.height} cm` : ""}
</span>
</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 className="flex gap-3 mt-6">
{profile.is_send_interest_received && profile.statusReceived?.toLowerCase() === 'pending' ? (
<>
<button
onClick={handleInterest}
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>
</>
);
}