profiles list using virtualization react window

This commit is contained in:
Meenadeveloper 2026-03-11 17:27:17 +05:30
parent 9427677a72
commit ccf638f7f3
9 changed files with 362 additions and 389 deletions

View 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>
);
}

View 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>
);
}

View File

@ -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

View File

@ -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>

View File

@ -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
View 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
View 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;

View File

@ -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),
}); });
}; };

View File

@ -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({