profiles list using virtualization react window
This commit is contained in:
parent
9427677a72
commit
ccf638f7f3
20
src/components/common/ProfileCardSkeleton.jsx
Normal file
20
src/components/common/ProfileCardSkeleton.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function ProfileCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-sm rounded-[10px] border shadow-lg p-4 animate-pulse">
|
||||||
|
|
||||||
|
<div className="w-full h-[280px] bg-gray-200 rounded mb-4"></div>
|
||||||
|
|
||||||
|
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||||
|
|
||||||
|
<div className="h-3 bg-gray-200 rounded w-1/2 mb-3"></div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="h-3 bg-gray-200 rounded"></div>
|
||||||
|
<div className="h-3 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
142
src/components/common/ProfileCardUI.jsx
Normal file
142
src/components/common/ProfileCardUI.jsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Crown, Bookmark, Receipt, Sparkles, MoonStar, IdCard } from "lucide-react";
|
||||||
|
import CakeIcon from "@mui/icons-material/Cake";
|
||||||
|
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
||||||
|
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function ProfileCardUI({ profile }) {
|
||||||
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Map API fields to UI, handling missing values
|
||||||
|
const imageSrc = profile.photo || profile.image || "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => navigate(`/profile-details/${profile.id}`)}
|
||||||
|
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200 bg-white cursor-pointer hover:shadow-2xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
{/* Premium Badge */}
|
||||||
|
{profile.isPremium && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 0.2, type: "spring" }}
|
||||||
|
className="absolute top-4 left-4 z-10 bg-red-900 rounded-full p-2 shadow-lg"
|
||||||
|
>
|
||||||
|
<Crown className="w-5 h-5 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Shortlist logic here
|
||||||
|
}}
|
||||||
|
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Bookmark className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="text-[12px] font-medium text-gray-700">Shortlist</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className="bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]">
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
alt={profile.name}
|
||||||
|
className="w-full h-full object-cover bg-gray-200"
|
||||||
|
style={{ objectPosition: "top" }}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient Overlay */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none" style={{ background: "linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%)" }}></div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-6 pb-2 text-gray-900">
|
||||||
|
<h1 className="text-[18px] text-green-900 font-bold mb-1 truncate">{profile.name}</h1>
|
||||||
|
<p className="text-[14px] text-gray-700 leading-relaxed font-medium">ID: {profile.member_id || profile.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-2 pb-4 flex flex-col gap-3 bg-white">
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<VisibilityIcon sx={{ fontSize: 18 }} />
|
||||||
|
<span className="text-[13px] font-medium">Last seen: {profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-y-2 gap-x-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CakeIcon sx={{ fontSize: 18, color: "#374151" }} />
|
||||||
|
<span className="text-[14px] font-semibold text-gray-900">{profile.age ? `${profile.age} yrs` : "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AccessibilityNewIcon sx={{ fontSize: 18, color: "#374151" }} />
|
||||||
|
<span className="text-[14px] font-semibold text-gray-900">{profile.height ? `${profile.height} cm` : "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 col-span-2">
|
||||||
|
<Receipt className="w-4 h-4 text-gray-700" />
|
||||||
|
<span className="text-[14px] font-semibold text-gray-900 truncate">{profile.annual_income_name || "N/A"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-gray-700">
|
||||||
|
<div className="flex items-center gap-1.5" title="Raasi">
|
||||||
|
<MoonStar className="w-4 h-4" />
|
||||||
|
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.raasi_name || "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5" title="Star">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.star_name || "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5" title="Caste">
|
||||||
|
<IdCard className="w-4 h-4" />
|
||||||
|
<span className="text-[13px] font-medium truncate max-w-[80px]">{profile.caste_name || "-"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LocationOnIcon sx={{ fontSize: 18, color: "#DC2626" }} />
|
||||||
|
<span className="text-[14px] font-semibold text-gray-900 truncate">
|
||||||
|
{profile.district_name || profile.location || "-"}
|
||||||
|
{profile.state_name ? `, ${profile.state_name}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); }}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-red-50 border border-red-200 text-red-700 rounded-full font-medium text-sm hover:bg-red-100 transition-colors active:scale-95"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-full font-medium text-sm border transition-colors active:scale-95 ${isLiked ? "bg-green-100 border-green-300 text-green-700" : "bg-green-50 border-green-200 text-green-700 hover:bg-green-100"}`}
|
||||||
|
onClick={(e) =>{ e.stopPropagation(); setIsLiked(!isLiked); }}
|
||||||
|
>
|
||||||
|
{isLiked ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4 text-red-500 fill-current" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
||||||
|
Sent
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||||
|
Interest
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import ReactWindow from "react-window";
|
|
||||||
|
|
||||||
const FixedSizeList = ReactWindow?.FixedSizeList || ReactWindow?.default?.FixedSizeList;
|
|
||||||
|
|
||||||
const LISTBOX_PADDING = 8; // px
|
|
||||||
|
|
||||||
function renderRow(props) {
|
|
||||||
const { data, index, style } = props;
|
|
||||||
const dataSet = data[index];
|
|
||||||
const inlineStyle = {
|
|
||||||
...style,
|
|
||||||
top: style.top + LISTBOX_PADDING,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={inlineStyle}>
|
|
||||||
{dataSet}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const OuterElementContext = React.createContext({});
|
|
||||||
|
|
||||||
const OuterElementType = React.forwardRef((props, ref) => {
|
|
||||||
const outerProps = React.useContext(OuterElementContext);
|
|
||||||
return <div ref={ref} {...props} {...outerProps} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
const ListboxComponent = React.forwardRef(function ListboxComponent(props, ref) {
|
|
||||||
const { children, ...other } = props;
|
|
||||||
|
|
||||||
if (!FixedSizeList) {
|
|
||||||
return <ul ref={ref} {...other}>{children}</ul>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemData = [];
|
|
||||||
|
|
||||||
React.Children.forEach(children, (item) => {
|
|
||||||
itemData.push(item);
|
|
||||||
itemData.push(...(item.children || []));
|
|
||||||
});
|
|
||||||
|
|
||||||
const itemCount = itemData.length;
|
|
||||||
const itemSize = 48;
|
|
||||||
|
|
||||||
const getHeight = () => {
|
|
||||||
if (itemCount > 8) {
|
|
||||||
return 8 * itemSize;
|
|
||||||
}
|
|
||||||
return itemCount * itemSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<OuterElementContext.Provider value={other}>
|
|
||||||
<FixedSizeList
|
|
||||||
itemData={itemData}
|
|
||||||
height={getHeight() + 2 * LISTBOX_PADDING}
|
|
||||||
width="100%"
|
|
||||||
outerElementType={OuterElementType}
|
|
||||||
innerElementType="ul"
|
|
||||||
itemSize={itemSize}
|
|
||||||
overscanCount={5}
|
|
||||||
itemCount={itemCount}
|
|
||||||
>
|
|
||||||
{renderRow}
|
|
||||||
</FixedSizeList>
|
|
||||||
</OuterElementContext.Provider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const VirtualizedSelect = ({ options, value, onChange, label, placeholder, isMulti, ...props }) => {
|
|
||||||
return (
|
|
||||||
<Autocomplete
|
|
||||||
multiple={isMulti}
|
|
||||||
options={options}
|
|
||||||
value={value}
|
|
||||||
onChange={(event, newValue) => {
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
disableListWrap
|
|
||||||
ListboxComponent={ListboxComponent}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label={label} placeholder={placeholder} variant="outlined" />
|
|
||||||
)}
|
|
||||||
isOptionEqualToValue={(option, value) => option.value === value.value}
|
|
||||||
getOptionLabel={(option) => option.label || ""}
|
|
||||||
{...props}>
|
|
||||||
</Autocomplete>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VirtualizedSelect
|
|
||||||
@ -1,15 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Crown, Bookmark, CurrencyIcon, Currency, Wallet, Receipt, Sparkles, MoonStar, IdCard, RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react";
|
import { useInView } from "react-intersection-observer";
|
||||||
import CakeIcon from "@mui/icons-material/Cake";
|
import { RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react";
|
||||||
import GroupsIcon from "@mui/icons-material/Groups";
|
|
||||||
import SchoolIcon from "@mui/icons-material/School";
|
|
||||||
import LocationOnIcon from "@mui/icons-material/LocationOn";
|
|
||||||
import AccessibilityNewIcon from "@mui/icons-material/AccessibilityNew";
|
|
||||||
import PersonIcon from "@mui/icons-material/Person";
|
import PersonIcon from "@mui/icons-material/Person";
|
||||||
import StarIcon from "@mui/icons-material/Star";
|
import StarIcon from "@mui/icons-material/Star";
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import PersonAddIcon from "@mui/icons-material/PersonAdd";
|
import PersonAddIcon from "@mui/icons-material/PersonAdd";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import FilterModal from "../../feature/FilterModal";
|
import FilterModal from "../../feature/FilterModal";
|
||||||
import bride1 from "../../assets/images/bride1.jpg";
|
import bride1 from "../../assets/images/bride1.jpg";
|
||||||
import bride2 from "../../assets/images/bride2.jpg";
|
import bride2 from "../../assets/images/bride2.jpg";
|
||||||
@ -23,207 +18,75 @@ import groom4 from "../../assets/images/groom4.jpg";
|
|||||||
|
|
||||||
import horoscope from "../../assets/images/horoscopeicon.png";
|
import horoscope from "../../assets/images/horoscopeicon.png";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Button, Fab } from "@mui/material";
|
|
||||||
import MessageIcon from "@mui/icons-material/Message";
|
|
||||||
import PhoneIcon from "@mui/icons-material/Phone";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { updateFilter } from "../../redux/filterSlice";
|
import { updateFilter } from "../../redux/filterSlice";
|
||||||
// Profile Card Component
|
import { useProfiles } from "../../hooks/useProfiles";
|
||||||
function ProfileCard({ profile }) {
|
import ProfileCardUI from "../common/ProfileCardUI";
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
import ProfileCardSkeleton from "../common/ProfileCardSkeleton";
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={() => navigate(`/profile-details/${profile.id}`)}
|
|
||||||
className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200"
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ delay: 0.2, type: "spring" }}
|
|
||||||
className="absolute top-4 left-4 z-10 bg-red-900 rounded-full p-2 shadow-lg"
|
|
||||||
>
|
|
||||||
<Crown className="w-5 h-5 text-white" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
className="absolute top-4 right-4 z-10 bg-white rounded-full px-4 py-2 shadow-lg flex items-center space-x-2 hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<Bookmark className="w-4 h-4" />
|
|
||||||
<span className="text-[12px] font-medium">Shortlist</span>
|
|
||||||
</motion.button>
|
|
||||||
<div
|
|
||||||
classname=" bg-gray-200 overflow-hidden w-full max-w-sm h-[300px]"
|
|
||||||
style={{ height: "300px" }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={profile.image}
|
|
||||||
alt="Profile"
|
|
||||||
className="w-full h-full object-cover bg-gray-200"
|
|
||||||
style={{
|
|
||||||
// objectFit:"inherit",
|
|
||||||
objectPosition: "top",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute bottom-0 left-0 right-0 h-25 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
"linear-gradient(rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 40%, rgb(255, 255, 255) 100%)",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-6 pb-1 text-gray-900">
|
|
||||||
<h1 className="text-[18px] text-green-900 font-bold mb-2">
|
|
||||||
{profile.name}
|
|
||||||
</h1>
|
|
||||||
<p className="text-[14px] text-gray-700 leading-relaxed">
|
|
||||||
Matrimony ID: {profile.id}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4 pt-2 pb-4 flex flex-col gap-2 bg-white">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<VisibilityIcon />
|
|
||||||
<span className="text-[14px] text-gray-900">{profile.lastseen}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CakeIcon className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
{profile.age} yr
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<AccessibilityNewIcon className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
{profile.height} cm
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Receipt className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
5 - 10 LPA
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MoonStar className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
Aries
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Sparkles className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
Scorpio
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<IdCard className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
Bramin
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<LocationOnIcon className="w-4 h-4 text-gray-700" />
|
|
||||||
<span className="text-[14px] font-semibold text-gray-900">
|
|
||||||
{profile.location}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 my-2 justify-between w-full px-[0px]">
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// your decline logic
|
|
||||||
}} className="gap-2 px-3 w-[fit-content] bg-red-50 border-1 border-red-200
|
|
||||||
font-400 text-base py-1.5 rounded-[20px] shadow-md text-[14px]
|
|
||||||
hover:shadow-lg transition-all duration-300 flex items-center justify-center transform hover:scale-95">
|
|
||||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
Decline
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="w-[fit-content] bg-green-50 border-1 border-green-200 font-400 text-base text-[14px]
|
|
||||||
rounded-[20px] px-3 gap-2 py-1 shadow-lg hover:shadow-xl transition-all duration-300
|
|
||||||
transform hover:scale-105 flex items-center justify-center"
|
|
||||||
onClick={(e) =>{
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsLiked(!isLiked);
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
{isLiked ? (
|
|
||||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#EF4444"/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
|
|
||||||
Interest
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className="flex gap-3 my-2 justify-between w-full px-[0px]">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Fab size="medium" color="primary" aria-label="add">
|
|
||||||
<MessageIcon />
|
|
||||||
</Fab>
|
|
||||||
|
|
||||||
<Fab size="medium" color="secondary" aria-label="add">
|
|
||||||
<PhoneIcon />
|
|
||||||
</Fab>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="#f5fbff"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
navigate(`/profile-details/${profile.id}`);
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
color: "#000000",
|
|
||||||
background: "#f5fbff",
|
|
||||||
fontWeight: "600",
|
|
||||||
borderRadius: "30px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View Details
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Component
|
// Main Component
|
||||||
export default function MatchesInterface() {
|
export default function MatchesInterface() {
|
||||||
|
const [showSkeleton, setShowSkeleton] = React.useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const filterType = useSelector((state) => state.filters.filter_type);
|
const filters = useSelector((state) => state.filters);
|
||||||
|
const filterType = filters.filter_type;
|
||||||
const selectedTab = filterType || "all_matches";
|
const selectedTab = filterType || "all_matches";
|
||||||
const isPaidMember = useSelector((state) => state.filters.isPaidMember);
|
const isPaidMember = filters.isPaidMember;
|
||||||
|
const { ref, inView } = useInView({
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: "300px"
|
||||||
|
});
|
||||||
|
// Fetch real profiles data
|
||||||
|
const {
|
||||||
|
data: profilesData,
|
||||||
|
isLoading,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useProfiles(filters);
|
||||||
|
const profiles =
|
||||||
|
profilesData?.pages.flatMap((page) => page?.data|| []) || [];
|
||||||
|
|
||||||
|
// const { ref, inView } = useInView();
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (inView && hasNextPage && !isFetchingNextPage) {
|
||||||
|
// fetchNextPage();
|
||||||
|
// }
|
||||||
|
// }, [inView, hasNextPage, isFetchingNextPage]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView && hasNextPage && !isFetchingNextPage) {
|
||||||
|
|
||||||
|
setShowSkeleton(true); // show skeleton
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
fetchNextPage();
|
||||||
|
setShowSkeleton(false); // hide skeleton after API call
|
||||||
|
}, 120); // 0.5 seconds
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Fetched profiles:", profiles);
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
inView,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
});
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
id: "all_matches",
|
id: "all_matches",
|
||||||
@ -290,89 +153,6 @@ export default function MatchesInterface() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const profiles = [
|
|
||||||
{
|
|
||||||
id: "JB2847593",
|
|
||||||
name: "Jerome Bell",
|
|
||||||
age: 22,
|
|
||||||
height: "5.2",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "BCA / Data analyst",
|
|
||||||
location: "Chennai",
|
|
||||||
image: bride1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "SA8392847",
|
|
||||||
name: "Sarah Anderson",
|
|
||||||
age: 24,
|
|
||||||
height: "5.4",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "MBA / Marketing Manager",
|
|
||||||
location: "Bangalore",
|
|
||||||
image: bride4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "PR9384756",
|
|
||||||
name: "Priya Reddy",
|
|
||||||
age: 23,
|
|
||||||
height: "5.3",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "B.Tech / Software Engineer",
|
|
||||||
location: "Hyderabad",
|
|
||||||
image: bride2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "AN4758392",
|
|
||||||
name: "Ananya Krishnan",
|
|
||||||
age: 25,
|
|
||||||
height: "5.5",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "MD / Doctor",
|
|
||||||
location: "Kochi",
|
|
||||||
image: bride3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "AN4758392",
|
|
||||||
name: "Ananya Krishnan",
|
|
||||||
age: 25,
|
|
||||||
height: "5.5",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "MD / Doctor",
|
|
||||||
location: "Kochi",
|
|
||||||
image: groom1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "AN4758392",
|
|
||||||
name: "Ananya Krishnan",
|
|
||||||
age: 25,
|
|
||||||
height: "5.5",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "MD / Doctor",
|
|
||||||
location: "Kochi",
|
|
||||||
image: groom2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "AN4758392",
|
|
||||||
name: "Ananya Krishnan",
|
|
||||||
age: 25,
|
|
||||||
height: "5.5",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "MD / Doctor",
|
|
||||||
location: "Kochi",
|
|
||||||
image: groom4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "AN4758392",
|
|
||||||
name: "Ananya Krishnan",
|
|
||||||
age: 25,
|
|
||||||
height: "5.5",
|
|
||||||
lastseen: "Last seen 14 Nov 2025",
|
|
||||||
education: "MD / Doctor",
|
|
||||||
location: "Kochi",
|
|
||||||
image: groom3,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let currentCategory = "";
|
let currentCategory = "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -501,9 +281,37 @@ export default function MatchesInterface() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
{profiles.map((profile) => (
|
{isLoading && !isFetchingNextPage ? (
|
||||||
<ProfileCard key={profile.id} profile={profile} />
|
[...Array(6)].map((_, i) => <ProfileCardSkeleton key={i} />)
|
||||||
))}
|
) : profiles.length > 0 ? (
|
||||||
|
profiles.map((profile) => (
|
||||||
|
<ProfileCardUI key={profile.id} profile={profile} />
|
||||||
|
))
|
||||||
|
) : !isLoading && !isFetchingNextPage ? (
|
||||||
|
<div className="col-span-full text-center py-10 text-gray-500">
|
||||||
|
No profiles found
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
{/* {isFetchingNextPage &&
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<ProfileCardSkeleton key={`skel-${i}`} />
|
||||||
|
))} */}
|
||||||
|
|
||||||
|
|
||||||
|
{(isFetchingNextPage || showSkeleton) &&
|
||||||
|
[...Array(6)].map((_, i) => (
|
||||||
|
<ProfileCardSkeleton key={`skel-${i}`} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div ref={ref} className="h-[20px]">
|
||||||
|
{!isLoading && !hasNextPage && profiles.length > 0 && (
|
||||||
|
<p className="text-center text-gray-500 py-8">
|
||||||
|
You've reached the end.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { updateFilter } from '../../redux/filterSlice';
|
||||||
|
import useDebounce from '../../hooks/useDebounce.jsx';
|
||||||
|
|
||||||
export default function SearchUI() {
|
export default function SearchUI() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const searchFromStore = useSelector((state) => state.filters.search);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
const debouncedSearchValue = useDebounce(searchValue, 500);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchValue(searchFromStore || '');
|
||||||
|
}, [searchFromStore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(updateFilter({ search: debouncedSearchValue }));
|
||||||
|
}, [debouncedSearchValue, dispatch]);
|
||||||
|
|
||||||
// Sample suggestions data - you can replace with dynamic data
|
// Sample suggestions data - you can replace with dynamic data
|
||||||
const allSuggestions = [
|
const allSuggestions = [
|
||||||
|
|||||||
14
src/hooks/useDebounce.js
Normal file
14
src/hooks/useDebounce.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export function useDebounce(value, delay=500) {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
useEffect(()=>{
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
},delay);
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
},[value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
};
|
||||||
19
src/hooks/useDebounce.jsx
Normal file
19
src/hooks/useDebounce.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const useDebounce = (value, delay) => {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDebounce;
|
||||||
@ -1,10 +1,62 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||||
import { getProfilesFilterList, getProfilesFilterMasters } from "../api/masters.api";
|
import {
|
||||||
|
getProfilesFilterList,
|
||||||
|
getProfilesFilterMasters,
|
||||||
|
} from "../api/masters.api";
|
||||||
|
|
||||||
|
export const useProfiles = (filters = {}) => {
|
||||||
|
|
||||||
|
// Remove empty filters
|
||||||
|
const cleanFilters = Object.entries(filters).reduce((acc, [key, value]) => {
|
||||||
|
if (
|
||||||
|
value !== "" &&
|
||||||
|
value !== null &&
|
||||||
|
value !== undefined &&
|
||||||
|
!(Array.isArray(value) && value.length === 0)
|
||||||
|
) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: ["profiles-filter-list", cleanFilters],
|
||||||
|
|
||||||
|
queryFn: ({ pageParam = 1 }) =>
|
||||||
|
getProfilesFilterList({
|
||||||
|
...cleanFilters,
|
||||||
|
page: pageParam,
|
||||||
|
}),
|
||||||
|
|
||||||
|
staleTime: 1000 * 60 * 2,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
|
||||||
|
|
||||||
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
|
|
||||||
|
const currentPageData =
|
||||||
|
lastPage?.data?.data || lastPage?.data || [];
|
||||||
|
|
||||||
|
const totalLoaded = allPages.reduce((acc, page) => {
|
||||||
|
const pageData = page?.data?.data || page?.data || [];
|
||||||
|
return acc + pageData.length;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const totalRecords =
|
||||||
|
lastPage?.data?.recordsFiltered ||
|
||||||
|
lastPage?.recordsFiltered ||
|
||||||
|
0;
|
||||||
|
|
||||||
|
console.log("totalLoaded:", totalLoaded);
|
||||||
|
console.log("totalRecords:", totalRecords);
|
||||||
|
|
||||||
|
if (totalLoaded < totalRecords) {
|
||||||
|
return allPages.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const useProfiles = (filters) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["profiles-filter-list", filters],
|
|
||||||
queryFn: () => getProfilesFilterList(filters),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const initialState = {
|
|||||||
filter_type: "all_matches",
|
filter_type: "all_matches",
|
||||||
page: 1,
|
page: 1,
|
||||||
isPaidMember: false,
|
isPaidMember: false,
|
||||||
|
search: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterSlice = createSlice({
|
const filterSlice = createSlice({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user