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 { Crown, Bookmark, CurrencyIcon, Currency, Wallet, Receipt, Sparkles, MoonStar, IdCard, RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react";
|
||||
import CakeIcon from "@mui/icons-material/Cake";
|
||||
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 React, { useEffect } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { RockingChair, LocateFixed, School, WorkflowIcon, Lock } from "lucide-react";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import PersonAddIcon from "@mui/icons-material/PersonAdd";
|
||||
import { motion } from "framer-motion";
|
||||
import FilterModal from "../../feature/FilterModal";
|
||||
import bride1 from "../../assets/images/bride1.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 { 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 { useSelector, useDispatch } from "react-redux";
|
||||
import { updateFilter } from "../../redux/filterSlice";
|
||||
// Profile Card Component
|
||||
function ProfileCard({ profile }) {
|
||||
const [isLiked, setIsLiked] = useState(false);
|
||||
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>
|
||||
);
|
||||
}
|
||||
import { useProfiles } from "../../hooks/useProfiles";
|
||||
import ProfileCardUI from "../common/ProfileCardUI";
|
||||
import ProfileCardSkeleton from "../common/ProfileCardSkeleton";
|
||||
|
||||
// Main Component
|
||||
export default function MatchesInterface() {
|
||||
const [showSkeleton, setShowSkeleton] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
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 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 = [
|
||||
{
|
||||
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 = "";
|
||||
|
||||
return (
|
||||
@ -501,9 +281,37 @@ export default function MatchesInterface() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||
{profiles.map((profile) => (
|
||||
<ProfileCard key={profile.id} profile={profile} />
|
||||
))}
|
||||
{isLoading && !isFetchingNextPage ? (
|
||||
[...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>
|
||||
|
||||
@ -1,9 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from '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() {
|
||||
const dispatch = useDispatch();
|
||||
const searchFromStore = useSelector((state) => state.filters.search);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
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
|
||||
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 { getProfilesFilterList, getProfilesFilterMasters } from "../api/masters.api";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
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",
|
||||
page: 1,
|
||||
isPaidMember: false,
|
||||
search: "",
|
||||
};
|
||||
|
||||
const filterSlice = createSlice({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user