diff --git a/src/components/common/ProfileCardSkeleton.jsx b/src/components/common/ProfileCardSkeleton.jsx new file mode 100644 index 0000000..eacea38 --- /dev/null +++ b/src/components/common/ProfileCardSkeleton.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +export default function ProfileCardSkeleton() { + return ( +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/common/ProfileCardUI.jsx b/src/components/common/ProfileCardUI.jsx new file mode 100644 index 0000000..09ab40c --- /dev/null +++ b/src/components/common/ProfileCardUI.jsx @@ -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 ( +
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" + > +
+ {/* Premium Badge */} + {profile.isPremium && ( + + + + )} + + { + 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" + > + + Shortlist + + +
+ {profile.name} { + e.target.src = "https://www.thirukalyanam.amrithaa.net/backend/app-assets/images/portrait/small/no-image.png"; + }} + /> +
+ + {/* Gradient Overlay */} +
+ +
+

{profile.name}

+

ID: {profile.member_id || profile.id}

+
+
+ +
+
+ + Last seen: {profile.last_seen_at && profile.last_seen_at !== "-" ? profile.last_seen_at : "Recently"} +
+ +
+
+ + {profile.age ? `${profile.age} yrs` : "-"} +
+
+ + {profile.height ? `${profile.height} cm` : "-"} +
+
+ + {profile.annual_income_name || "N/A"} +
+
+ +
+
+ + {profile.raasi_name || "-"} +
+
+ + {profile.star_name || "-"} +
+
+ + {profile.caste_name || "-"} +
+
+ +
+ + + {profile.district_name || profile.location || "-"} + {profile.state_name ? `, ${profile.state_name}` : ""} + +
+ +
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/common/VirtualizedSelect.jsx b/src/components/common/VirtualizedSelect.jsx deleted file mode 100644 index b1e9230..0000000 --- a/src/components/common/VirtualizedSelect.jsx +++ /dev/null @@ -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 ( -
- {dataSet} -
- ); -} - -const OuterElementContext = React.createContext({}); - -const OuterElementType = React.forwardRef((props, ref) => { - const outerProps = React.useContext(OuterElementContext); - return
; -}); - -const ListboxComponent = React.forwardRef(function ListboxComponent(props, ref) { - const { children, ...other } = props; - - if (!FixedSizeList) { - return ; - } - - 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 ( -
- - - {renderRow} - - -
- ); -}); - -const VirtualizedSelect = ({ options, value, onChange, label, placeholder, isMulti, ...props }) => { - return ( - { - onChange(newValue); - }} - disableListWrap - ListboxComponent={ListboxComponent} - renderInput={(params) => ( - - )} - isOptionEqualToValue={(option, value) => option.value === value.value} - getOptionLabel={(option) => option.label || ""} - {...props}> - - ) -} - -export default VirtualizedSelect \ No newline at end of file diff --git a/src/components/matches/MatchesProfilesTab.jsx b/src/components/matches/MatchesProfilesTab.jsx index 897da7a..7b42494 100644 --- a/src/components/matches/MatchesProfilesTab.jsx +++ b/src/components/matches/MatchesProfilesTab.jsx @@ -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 ( -
navigate(`/profile-details/${profile.id}`)} - className="w-full max-w-sm rounded-[10px] shadow-xl overflow-hidden border border-green-200" - > -
- - - - - - - Shortlist - -
- Profile -
-
- -
-

- {profile.name} -

-

- Matrimony ID: {profile.id} -

-
-
- -
-
- - {profile.lastseen} -
-
-
- - - {profile.age} yr - -
- -
- - - {profile.height} cm - -
- -
- - - 5 - 10 LPA - -
-
-
-
- - - Aries - -
- -
- - - Scorpio - -
- -
- - - Bramin - -
-
- -
- - - {profile.location} - -
- -
- - - -
- - {/*
-
- - - - - - - -
- - -
*/} -
-
- ); -} +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() {
- {profiles.map((profile) => ( - - ))} + {isLoading && !isFetchingNextPage ? ( + [...Array(6)].map((_, i) => ) + ) : profiles.length > 0 ? ( + profiles.map((profile) => ( + + )) + ) : !isLoading && !isFetchingNextPage ? ( +
+ No profiles found +
+ ) : null} + + + {/* {isFetchingNextPage && + [...Array(5)].map((_, i) => ( + + ))} */} + + + {(isFetchingNextPage || showSkeleton) && + [...Array(6)].map((_, i) => ( + + ))} + +
+
+ {!isLoading && !hasNextPage && profiles.length > 0 && ( +

+ You've reached the end. +

+ )}
diff --git a/src/components/matches/SearchUI.jsx b/src/components/matches/SearchUI.jsx index 230a34d..9620d8d 100644 --- a/src/components/matches/SearchUI.jsx +++ b/src/components/matches/SearchUI.jsx @@ -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 = [ diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.js new file mode 100644 index 0000000..599de9f --- /dev/null +++ b/src/hooks/useDebounce.js @@ -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; +}; \ No newline at end of file diff --git a/src/hooks/useDebounce.jsx b/src/hooks/useDebounce.jsx new file mode 100644 index 0000000..440ecc0 --- /dev/null +++ b/src/hooks/useDebounce.jsx @@ -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; \ No newline at end of file diff --git a/src/hooks/useProfiles.js b/src/hooks/useProfiles.js index 9b9b6bf..17ae0ed 100644 --- a/src/hooks/useProfiles.js +++ b/src/hooks/useProfiles.js @@ -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), }); }; diff --git a/src/redux/filterSlice.jsx b/src/redux/filterSlice.jsx index c5a3eba..01f8367 100644 --- a/src/redux/filterSlice.jsx +++ b/src/redux/filterSlice.jsx @@ -21,6 +21,7 @@ const initialState = { filter_type: "all_matches", page: 1, isPaidMember: false, + search: "", }; const filterSlice = createSlice({