diff --git a/package-lock.json b/package-lock.json index baef904..b16a66d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,9 @@ "react-lazy-load-image-component": "^1.6.3", "react-redux": "^9.2.0", "react-router-dom": "^7.9.6", + "react-select": "^5.10.2", + "react-window": "^2.2.7", + "redux-persist": "^6.0.0", "swiper": "^12.0.3", "tailwindcss": "^4.1.17" }, @@ -1719,6 +1722,31 @@ "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", "license": "Apache-2.0" }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@grpc/grpc-js": { "version": "1.9.15", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", @@ -7139,6 +7167,12 @@ "node": ">= 0.4" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8053,6 +8087,27 @@ "react-dom": ">=18" } }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-smooth": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", @@ -8116,6 +8171,16 @@ } } }, + "node_modules/react-window": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz", + "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8205,6 +8270,15 @@ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", @@ -9069,6 +9143,20 @@ "punycode": "^2.1.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/package.json b/package.json index 1345bf1..ad96a48 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,9 @@ "react-lazy-load-image-component": "^1.6.3", "react-redux": "^9.2.0", "react-router-dom": "^7.9.6", + "react-select": "^5.10.2", + "react-window": "^2.2.7", + "redux-persist": "^6.0.0", "swiper": "^12.0.3", "tailwindcss": "^4.1.17" }, diff --git a/src/api/apiEndpoints.js b/src/api/apiEndpoints.js index d872116..4ded3e8 100644 --- a/src/api/apiEndpoints.js +++ b/src/api/apiEndpoints.js @@ -53,5 +53,9 @@ BE_SAFE_ONLINE:"get_be_safe_online", NOTIFICATION_LIST:"notification/lists", NOTIFICATION_COUNT:"notification/un_read_count", +// filter with profiles list api's +PROFILES_FILTER_LIST:"profiles/lists", +PROFILES_FILTER_MASTER:"profiles/filter/masters", + }; diff --git a/src/api/masters.api.js b/src/api/masters.api.js index c7ea4a6..1e9296c 100644 --- a/src/api/masters.api.js +++ b/src/api/masters.api.js @@ -63,3 +63,17 @@ export const getPartnerPreferenceMasters = async () => { ); return res.data; }; + + +// profile filter masters +export const getProfilesFilterList = async (filters) => { + const res = await axiosInstance.get(API_ENDPOINTS.PROFILES_FILTER_LIST, { + params: filters, + }); + return res.data; +}; + +export const getProfilesFilterMasters = async () => { + const res = await axiosInstance.get(API_ENDPOINTS.PROFILES_FILTER_MASTER); + return res.data; +}; \ No newline at end of file diff --git a/src/components/common/VirtualizedSelect.jsx b/src/components/common/VirtualizedSelect.jsx new file mode 100644 index 0000000..b1e9230 --- /dev/null +++ b/src/components/common/VirtualizedSelect.jsx @@ -0,0 +1,97 @@ +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 023a036..897da7a 100644 --- a/src/components/matches/MatchesProfilesTab.jsx +++ b/src/components/matches/MatchesProfilesTab.jsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Crown, Bookmark, CurrencyIcon, Currency, Wallet, Receipt, Sparkles, MoonStar, IdCard, RockingChair, LocateFixed, School, WorkflowIcon } from "lucide-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"; @@ -26,6 +26,9 @@ 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); @@ -216,67 +219,70 @@ function ProfileCard({ profile }) { // Main Component export default function MatchesInterface() { const navigate = useNavigate(); - const [selectedTab, setSelectedTab] = useState("your-matches"); + const dispatch = useDispatch(); + const filterType = useSelector((state) => state.filters.filter_type); + const selectedTab = filterType || "all_matches"; + const isPaidMember = useSelector((state) => state.filters.isPaidMember); const tabs = [ { - id: "your-matches", + id: "all_matches", icon: , title: "Your Matches", description: "View all the profiles that match your preferences", category: "All Matches", }, { - id: "shortlisted-by-you", + id: "shorlisted_by_you", icon: , title: "Shortlisted by you", description: "Matches you have shortlisted", category: "Based on activity", }, { - id: "viewed-you", + id: "viewed_you", icon: , title: "Viewed you", description: "Matches who have viewed your profile", category: "Based on activity", }, { - id: "shortlisted-you", + id: "shorlisted_you", icon: , title: "Shortlisted you", description: "Matches who have shortlisted your profile", category: "Based on activity", }, { - id: "viewed-by-you", + id: "viewed_by_you", icon: , title: "Viewed by you", description: "Matches you have viewed", category: "Based on activity", }, { - id: "newly-joined", + id: "newly_joined", icon: , title: "Newly Joined", description: "Matches who Joined within the last 30 days", category: "Based on activity", }, { - id: "location", + id: "location_matches", icon: , title: "Location matches", description: "Matches near your location", category: "Based on activity", }, { - id: "education", + id: "education_matches", icon: , title: "Education matches", description: "Matches near your education match", category: "Based on activity", }, { - id: "job", + id: "job_matches", icon: , title: "Job matches", description: "Matches near your job", @@ -406,7 +412,9 @@ export default function MatchesInterface() { )}
setSelectedTab(tab.id)} + onClick={() => { + dispatch(updateFilter({ filter_type: tab.id })); + }} className={`p-4 rounded-lg mb-3 cursor-pointer transition-all ${ selectedTab === tab.id ? "bg-green-50 border-l-4 border-green-600" @@ -472,12 +480,23 @@ export default function MatchesInterface() { {tabs.find((t) => t.id === selectedTab)?.title}
- { +
{ + if (isPaidMember) { navigate("/horoscoper-generate"); - }} - /> + } else { + toast.error("Star Matching is locked for free members"); + } + }}> + + {!isPaidMember && ( +
+ +
+ )} +
diff --git a/src/feature/AdvancedDropzone.jsx b/src/feature/AdvancedDropzone.jsx index 02f1e35..6c87e3b 100644 --- a/src/feature/AdvancedDropzone.jsx +++ b/src/feature/AdvancedDropzone.jsx @@ -107,14 +107,14 @@ const AdvancedDropzone = ({ value, onChange }) => { {hoveredId === file.id && (
{ const dispatch = useDispatch(); @@ -40,34 +41,61 @@ const FilterForm = () => { location: false, lifestyle: false, family: false, + paidBenefit: false, }); + const { data: filterMasters, isLoading, isError } = useProfilesFilterMasters(); + + const { data: cityData } = useCityMasters( + filters.state && filters.state.length > 0 ? filters.state : null + ); + + const cityOptions = useMemo(() => { + const raw = cityData; + if (!raw) return []; + if (Array.isArray(raw)) return raw; + return raw.subCaste || raw.district || raw.data || []; + }, [cityData]); + const handleAccordionChange = (section) => (event, isExpanded) => { setExpandedSections((prev) => ({ ...prev, [section]: isExpanded })); }; - const casteOptions = ["Agamudayar", "Pillai", "Vellalar"]; - const motherTongueOptions = [ - "Tamil", - "Telugu", - "Malayalam", - "Kannada", - "Hindi", - ]; const handleSubmit = () => { console.log("Filter Values:", filters); }; + const handleClear = () => { + dispatch(resetFilters()); + toast.success("Filters cleared"); + }; + const handleSelectionChange = (field, value) => { console.log(`${field} selected:`, value); }; + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ Failed to load filter options +
+ ); + } + return (
{/* Header */} -
+
Partner Filter preference @@ -98,7 +126,7 @@ const FilterForm = () => { Age
{ dispatch(setAge(newValue)); handleSelectionChange("Age", newValue); @@ -119,7 +147,7 @@ const FilterForm = () => { Height
{ dispatch(setHeight(newValue)); handleSelectionChange("Height", newValue); @@ -141,46 +169,40 @@ const FilterForm = () => { Marital Status - - - {/* Mother Tongue */} - - Mother Tongue - + + +
@@ -207,61 +229,69 @@ const FilterForm = () => { Religion - {/* Matches with Horoscope */} - { - dispatch(setMatchesWithHoroscope(e.target.checked)); - handleSelectionChange( - "Matches with Horoscope", - e.target.checked - ); - }} - /> - } - label="Matches with horoscope" - /> - {/* Caste */} - Caste (Multi Select) + Caste @@ -269,21 +299,19 @@ const FilterForm = () => { {/* Sub-Caste */} - Sub-Caste (Multi Select) + Sub-Caste - {/* Star */} - - Star - - - {/* Dasham */} - - Dasham - -
@@ -356,66 +352,108 @@ const FilterForm = () => { Occupation Annual Income Employee Type Education
@@ -443,50 +481,59 @@ const FilterForm = () => { State - Country + City - - Citizenship - - +
@@ -512,54 +559,22 @@ const FilterForm = () => { Eating Habits - {/* - Smoking Habits - - - - - Drinking Habits - - */} +
@@ -585,64 +600,116 @@ const FilterForm = () => { Family Type - - Family Status - - +
+ + + {/* Paid Membership Benefit Section */} + { + if (!filters.isPaidMember) { + toast.error("This feature is locked for paid members only", { + icon: "🔒", + }); + return; + } + handleAccordionChange("paidBenefit")(e, isExpanded); + }} + className="mb-4" + > + : } + sx={{ + backgroundColor: "#f5fbff", + "&:hover": { + backgroundColor: "#fff6f0", + }, + }} + > +
+ + + Paid Membership Benefit + +
+
+ +
+ {/* Star Filter */} - Family Value + Star
- {/* Submit Button */} -
+ {/* Sticky Footer Actions */} +
+ diff --git a/src/feature/FilterModal.jsx b/src/feature/FilterModal.jsx index a4716a6..84bb9b9 100644 --- a/src/feature/FilterModal.jsx +++ b/src/feature/FilterModal.jsx @@ -27,7 +27,7 @@ const FilterModal = () => { open={open} onClose={handleClose} fullWidth - maxWidth="lg" // adjust: "sm" | "md" | "lg" + maxWidth="md" // adjust: "sm" | "md" | "lg" > diff --git a/src/feature/LifestyleDetailsForm.jsx b/src/feature/LifestyleDetailsForm.jsx index 15915c8..020493d 100644 --- a/src/feature/LifestyleDetailsForm.jsx +++ b/src/feature/LifestyleDetailsForm.jsx @@ -16,6 +16,7 @@ import { DialogContent, DialogContentText, DialogActions, + Tooltip, } from "@mui/material"; import { LocalizationProvider } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; @@ -184,6 +185,7 @@ const LifestyleDetailsForm = ({ }; const renderChartCell = (label, value, onChange) => ( + 0 ? value.join(", ") : ""} arrow placement="top">
{label} @@ -196,7 +198,17 @@ const LifestyleDetailsForm = ({ onChange={(e) => onChange(e.target.value)} renderValue={(selected) => { if (!selected || selected.length === 0) return `+ Add ${label}`; - return selected.join(", "); + return ( +
+ {selected.join(", ")} +
+ ); }} MenuProps={{ PaperProps: { style: { maxHeight: 280 } }, @@ -211,6 +223,7 @@ const LifestyleDetailsForm = ({
+
); const renderChartGrid = (type) => { diff --git a/src/feature/PersonalDetailsForm.jsx b/src/feature/PersonalDetailsForm.jsx index 50e77f8..89a67ea 100644 --- a/src/feature/PersonalDetailsForm.jsx +++ b/src/feature/PersonalDetailsForm.jsx @@ -39,6 +39,7 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat const dispatch = useDispatch(); const data = useSelector((state) => state.registerform.personalDetails); const nameInputRef = useRef(null); + const mobileInputRef = useRef(null); const requiredMark = *; const [showOtp, setShowOtp] = useState(false); @@ -56,15 +57,23 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat if (isStep1Update && data.mobileNumber && !data.verifiedMobileNumber) { dispatch(updatePersonalDetails({ verifiedMobileNumber: data.mobileNumber })); setMobileOtpVerified(true); + setMobileNumberError(""); } }, [isStep1Update, data.mobileNumber, data.verifiedMobileNumber, dispatch]); useEffect(() => { if (data.verifiedMobileNumber && data.mobileNumber === data.verifiedMobileNumber) { setMobileOtpVerified(true); + setMobileNumberError(""); } }, [data.verifiedMobileNumber, data.mobileNumber]); + useEffect(() => { + if (!isStep1Update && !data.religion) { + dispatch(updatePersonalDetails({ religion: 1 })); + } + }, [isStep1Update, data.religion, dispatch]); + const { data: personalMasters, isLoading: isPersonalMastersLoading } = usePersonalDetailsMasters(); @@ -120,7 +129,7 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat const raw = cityQuery.data; if (!raw) return []; if (Array.isArray(raw)) return raw; - return raw.subCaste || raw.district || raw.data || []; + return raw.subCaste || raw.sub_caste || raw.district || raw.data || []; }, [cityQuery.data]); const starOptions = useMemo(() => { @@ -384,12 +393,6 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat } }; - useEffect(() => { - if (showOtp && !mobileOtpVerified && isOtpComplete) { - handleOtpSubmit(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [otp]); // file upload @@ -426,32 +429,31 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat const handleSubmit = async () => { - if (showOtp && !isOtpComplete) { - setOtpError("OTP is required and must be complete"); - return; - } - - if (showOtp && !mobileOtpVerified) { - try { - await verifyOtp.mutateAsync({ - mobile: data.mobileNumber, - otp: otp.join(""), - }); - setMobileOtpVerified(true); - setOtpError(""); - console.log("Submitting personal details:", data); // log here - onSubmitStep(); - console.log("OTP verified on submit"); - } catch (err) { - setOtpError(err || "OTP verification failed"); + // For new registrations, mobile verification is mandatory. + // For updates, it's also mandatory if the number was changed. + // The `mobileOtpVerified` state covers both scenarios. + if (!mobileOtpVerified) { + // Provide a more specific error message. + if (showOtp) { + if (!isOtpComplete) { + setOtpError("OTP is required and must be complete"); + } else { + setOtpError("Please submit the OTP to verify your number."); + } + const firstOtpInput = document.getElementById("otp-0"); + if (firstOtpInput) { + firstOtpInput.focus(); + } + } else { + setMobileNumberError("Please verify your mobile number to proceed."); + if (mobileInputRef.current) { + mobileInputRef.current.focus(); + } + } + return; } - return; - } - - // no OTP or already verified - console.log("Submitting personal details:", data); // log here - onSubmitStep(); -}; + onSubmitStep(); + }; const formatTimer = (sec) => { @@ -532,6 +534,7 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat - + {otpTimer > 0 ? ( ` ${formatTimer(otpTimer) } Seconds` ) : ( @@ -712,6 +715,8 @@ const PersonalDetailsForm = ({ onSubmitStep, errors, onFieldChange, isStep1Updat fullWidth name="height" label="Enter Height" + + max={3} type="number" value={data.height} onChange={(e) => handleChange("height", e.target.value)} diff --git a/src/feature/StepperForm.jsx b/src/feature/StepperForm.jsx index 3f4b727..8d3ac05 100644 --- a/src/feature/StepperForm.jsx +++ b/src/feature/StepperForm.jsx @@ -233,8 +233,7 @@ const StepperForm = () => { const location = useLocation(); const navigate = useNavigate(); const hideStepperRoutes = ["/profile-edit"]; - -const shouldHideStepper = hideStepperRoutes.includes(location.pathname); +const shouldHideStepper = hideStepperRoutes.some((route) => location.pathname.startsWith(route)); const personalDetails = useSelector( (state) => state.registerform.personalDetails ); @@ -421,78 +420,84 @@ const [completedSteps, setCompletedSteps] = useState([]); } }, [location.state, navigate, location.pathname]); + const { data: personalDetailsData } = useQuery({ + queryKey: ["personalDetails"], + queryFn: async () => { + const response = await axiosInstance.get(API_ENDPOINTS.EDIT_PERSONAL_DETAILS); + return response.data; + }, + enabled: isAuth || shouldHideStepper, + retry: false, + refetchOnWindowFocus: false, + }); + useEffect(() => { - if (!isAuth) return; - const fetchPersonalDetails = async () => { - try { - const response = await axiosInstance.get(API_ENDPOINTS.EDIT_PERSONAL_DETAILS); - const data = response.data; - if (data.status === "success" && data.personal_details) { - const pd = data.personal_details; - setIsStep1Update(true); + const processData = async () => { + if (personalDetailsData?.status === "success" && personalDetailsData?.personal_details) { + const pd = personalDetailsData.personal_details; + setIsStep1Update(true); - const rawImages = pd.profile_images || pd.images || []; - const mappedImages = await Promise.all( - rawImages.map(async (url, index) => { - const imageUrl = typeof url === "string" ? url : url?.url; - let file = null; - let mimeType = "image/jpeg"; - let fileName = `image-${index}.jpg`; + const rawImages = pd.profile_images || pd.images || []; + const mappedImages = await Promise.all( + rawImages.map(async (url, index) => { + const imageUrl = typeof url === "string" ? url : url?.url; + let file = null; + let mimeType = "image/jpeg"; + let fileName = `image-${index}.jpg`; - try { + try { + if (imageUrl) { const response = await fetch(imageUrl); const blob = await response.blob(); if (blob.type) mimeType = blob.type; const ext = mimeType.split("/")[1] || "jpg"; fileName = `image-${index}.${ext}`; file = new File([blob], fileName, { type: mimeType }); - } catch (error) { - console.error("Error converting image URL to File:", error); } - return { - id: `server-${index}`, - preview: imageUrl, - imageUrl: imageUrl, - file: file, - name: fileName, - type: mimeType, - valid: true, - }; - }) - ); + } catch (error) { + console.error("Error converting image URL to File:", error); + } + return { + id: `server-${index}`, + preview: imageUrl, + imageUrl: imageUrl, + file: file, + name: fileName, + type: mimeType, + valid: true, + }; + }) + ); - const formattedDob = pd.dob ? pd.dob.split("T")[0] : ""; + const formattedDob = pd.dob ? pd.dob.split("T")[0] : ""; - dispatch( - updatePersonalDetails({ - name: pd.name || "", - mobileNumber: pd.mobile || "", - email: pd.email || "", - gender: pd.gender || "", - dob: formattedDob, - height: pd.height || "", - weight: pd.weight || "", - maritalStatus: pd.marital_status_id || "", - religion: pd.religion_id || "", - profileFor: pd.profile_for_id || "", - caste: pd.caste_id || "", - subCaste: pd.sub_caste_id || "", - gothram: pd.gothram_id || "", - raasi: pd.raasi_id || "", - star: pd.star_id || "", - state: pd.state_id || "", - city: pd.district_id || "", - pincode: pd.pincode || "", - profiles: mappedImages, - }) - ); - } - } catch (error) { - console.error("Error fetching personal details:", error); + dispatch( + updatePersonalDetails({ + name: pd.name || "", + mobileNumber: pd.mobile || "", + email: pd.email || "", + gender: pd.gender || "", + dob: formattedDob, + height: pd.height || "", + weight: pd.weight || "", + maritalStatus: pd.marital_status_id || "", + religion: pd.religion_id || "", + profileFor: pd.profile_for_id || "", + caste: pd.caste_id || "", + subCaste: pd.sub_caste_id || "", + gothram: pd.gothram_id || "", + raasi: pd.raasi_id || "", + star: pd.star_id || "", + state: pd.state_id || "", + city: pd.district_id || "", + pincode: pd.pincode || "", + profiles: mappedImages, + }) + ); } }; - fetchPersonalDetails(); - }, [dispatch, isAuth]); + processData(); + }, [personalDetailsData, dispatch]); // Fetch Educational Details @@ -502,7 +507,7 @@ const [completedSteps, setCompletedSteps] = useState([]); const response = await axiosInstance.get(API_ENDPOINTS.EDIT_EDUCATION_DETAILS); return response.data; }, - enabled: isAuth, + enabled: isAuth || shouldHideStepper, retry: false, refetchOnWindowFocus: false, }); @@ -535,7 +540,7 @@ const {data:familyData} = useQuery({ const response = await axiosInstance.get(API_ENDPOINTS.EDIT_FAMILY_DETAILS); return response.data; }, - enabled: isAuth, + enabled: isAuth || shouldHideStepper, retry:false, refetchOnWindowFocus:false, }); @@ -584,9 +589,10 @@ useEffect(()=>{ const response = await axiosInstance.get(API_ENDPOINTS.EDIT_LIFESTYLE_DETAILS); return response.data; }, - enabled: isAuth, + enabled: isAuth || shouldHideStepper, retry: false, refetchOnWindowFocus: false, + refetchOnMount: true, }); useEffect(() => { @@ -608,8 +614,8 @@ useEffect(()=>{ updateLifestyleDetails({ diets: ld.diet_id || "", hobbies: ld.hobbies_ids || [], - dob: ld.time_of_birth || "", - tob: ld.date_of_birth ? ld.date_of_birth.substring(0, 5) : "", + dob: ld.time_of_birth ? ld.time_of_birth.split("T")[0] : "", + tob: ld.time_of_birth_formated ? ld.time_of_birth_formated.substring(0, 5) : "", placeOfBirth: ld.place_of_birth || "", graha: mapChart("graha"), amsam: mapChart("amsam"), @@ -626,7 +632,7 @@ useEffect(()=>{ const response = await axiosInstance.get(API_ENDPOINTS.EDIT_PREFERED_PARTNER_DETAILS); return response.data; }, - enabled: isAuth, + enabled: isAuth || shouldHideStepper, retry: false, refetchOnWindowFocus: false, }); @@ -878,6 +884,8 @@ useEffect(()=>{ if (fileToAppend) { formData.append(`profile_images[${index}]`, fileToAppend); + } else if (item.preview && typeof item.preview === "string") { + formData.append(`profile_images[${index}]`, item.preview); } }) ); @@ -932,7 +940,7 @@ useEffect(()=>{ formData.append("tob", lifestyleDetails.tob || ""); formData.append("place_of_birth", lifestyleDetails.placeOfBirth || ""); - formData.append("diet", lifestyleDetails.diets || ""); + formData.append("diets", lifestyleDetails.diets || ""); (lifestyleDetails.hobbies || []).forEach((id, index) => { formData.append(`hobbies[${index}]`, id); @@ -1022,6 +1030,9 @@ useEffect(()=>{ try { const previewData = await getPreviewDetails(); + queryClient.invalidateQueries({ + queryKey: ["preview-details"] + }); if (previewData?.personal_details) { const pd = previewData.personal_details; const images = pd.images || pd.profile_images; @@ -1032,7 +1043,7 @@ useEffect(()=>{ dispatch(updatePersonalDetails({ profiles: formattedImages })); } } - queryClient.invalidateQueries({ queryKey: ["previewDetails"] }); + queryClient.invalidateQueries({ queryKey: ["personalDetails"] }); } catch (error) { console.error("Error refreshing preview details:", error); } @@ -1047,21 +1058,29 @@ useEffect(()=>{ case 2: payload = buildRegisterStep2Payload(); await registerStep2.mutateAsync(payload); + queryClient.invalidateQueries({ queryKey: ["educationalDetails"] }); + queryClient.invalidateQueries({ queryKey: ["preview-details"] }); break; case 3: payload = buildRegisterStep3Payload(); await registerStep3.mutateAsync(payload); + queryClient.invalidateQueries({ queryKey: ["familyDetails"] }) + queryClient.invalidateQueries({ queryKey: ["preview-details"] }); break; case 4: payload = buildRegisterStep4Payload(); await registerStep4.mutateAsync(payload); + queryClient.invalidateQueries({ queryKey: ["lifestyleDetails"] }); + queryClient.invalidateQueries({ queryKey: ["preview-details"] }); break; case 5: payload = buildRegisterStep5Payload(); await registerStep5.mutateAsync(payload); + queryClient.invalidateQueries({ queryKey: ["partnerPreferences"] }); + queryClient.invalidateQueries({ queryKey: ["preview-details"] }); break; default: diff --git a/src/hooks/useProfiles.js b/src/hooks/useProfiles.js new file mode 100644 index 0000000..9b9b6bf --- /dev/null +++ b/src/hooks/useProfiles.js @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { getProfilesFilterList, getProfilesFilterMasters } from "../api/masters.api"; + +export const useProfiles = (filters) => { + return useQuery({ + queryKey: ["profiles-filter-list", filters], + queryFn: () => getProfilesFilterList(filters), + }); +}; + +export const useProfilesFilterMasters = () => + useQuery({ + queryKey: ["profiles-filter-masters"], + queryFn: getProfilesFilterMasters, + }); \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index d647b16..456cf95 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -7,8 +7,9 @@ import { ThemeProvider } from "@mui/material/styles"; import theme from "./theme"; import { Provider } from "react-redux"; -import { store } from "./redux/store.js"; +import { persistor, store } from "./redux/store.js"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { PersistGate } from "redux-persist/integration/react"; // Disable noisy logs in production while keeping warnings/errors. if (import.meta.env.PROD) { console.log = () => {}; @@ -29,12 +30,14 @@ const queryClient = new QueryClient({ ReactDOM.createRoot(document.getElementById("root")).render( - - - - - + + + + + + + - + ); diff --git a/src/redux/filterSlice.jsx b/src/redux/filterSlice.jsx index 8f38444..c5a3eba 100644 --- a/src/redux/filterSlice.jsx +++ b/src/redux/filterSlice.jsx @@ -1,29 +1,26 @@ import { createSlice } from "@reduxjs/toolkit"; const initialState = { - age: [18, 70], - height: [4.0, 7.11], - maritalStatus: "All Profile", - motherTongue: [], - religion: "Hindu", - matchesWithHoroscope: false, + from_age: 18, + to_age: 70, + from_height: 4.0, + to_height: 7.11, + marital_status: [], + religion: [], caste: [], - subCaste: [], - star: "Any", - dasham: "Doesn't matter", - occupation: "Any", - annualIncome: "Any", - employeeType: "Any", - education: "Any", - state: "Any", - country: "Any", - citizenship: "Any", - eatingHabits: "Any", - smokingHabits: "Doesn't matter", - drinkingHabits: "Doesn't matter", - familyType: "Any", - familyStatus: "Any", - familyValue: "Any", + sub_caste: [], + star: [], + occupation: [], + annual_income: [], + employee_type: [], + education: [], + state: [], + district: [], + diet: "", + family_type: [], + filter_type: "all_matches", + page: 1, + isPaidMember: false, }; const filterSlice = createSlice({ @@ -31,73 +28,24 @@ const filterSlice = createSlice({ initialState, reducers: { setAge: (state, action) => { - state.age = action.payload; + state.from_age = action.payload[0]; + state.to_age = action.payload[1]; }, setHeight: (state, action) => { - state.height = action.payload; + state.from_height = action.payload[0]; + state.to_height = action.payload[1]; }, setMaritalStatus: (state, action) => { - state.maritalStatus = action.payload; - }, - setMotherTongue: (state, action) => { - state.motherTongue = action.payload; + state.marital_status = Array.isArray(action.payload) ? action.payload : [action.payload]; }, setReligion: (state, action) => { - state.religion = action.payload; - }, - setMatchesWithHoroscope: (state, action) => { - state.matchesWithHoroscope = action.payload; + state.religion = Array.isArray(action.payload) ? action.payload : [action.payload]; }, setCaste: (state, action) => { state.caste = action.payload; }, setSubCaste: (state, action) => { - state.subCaste = action.payload; - }, - setStar: (state, action) => { - state.star = action.payload; - }, - setDasham: (state, action) => { - state.dasham = action.payload; - }, - setOccupation: (state, action) => { - state.occupation = action.payload; - }, - setAnnualIncome: (state, action) => { - state.annualIncome = action.payload; - }, - setEmployeeType: (state, action) => { - state.employeeType = action.payload; - }, - setEducation: (state, action) => { - state.education = action.payload; - }, - setState: (state, action) => { - state.state = action.payload; - }, - setCountry: (state, action) => { - state.country = action.payload; - }, - setCitizenship: (state, action) => { - state.citizenship = action.payload; - }, - setEatingHabits: (state, action) => { - state.eatingHabits = action.payload; - }, - setSmokingHabits: (state, action) => { - state.smokingHabits = action.payload; - }, - setDrinkingHabits: (state, action) => { - state.drinkingHabits = action.payload; - }, - setFamilyType: (state, action) => { - state.familyType = action.payload; - }, - setFamilyStatus: (state, action) => { - state.familyStatus = action.payload; - }, - setFamilyValue: (state, action) => { - state.familyValue = action.payload; + state.sub_caste = action.payload; }, // universal update @@ -105,7 +53,16 @@ const filterSlice = createSlice({ return { ...state, ...action.payload }; }, - resetFilters: () => initialState, + resetFilters: (state) => { + // Reset all filters but preserve the paid member status + return { ...initialState, isPaidMember: state.isPaidMember }; + }, + setPage: (state, action) => { + state.page = action.payload; + }, + setPaidMemberStatus: (state, action) => { + state.isPaidMember = action.payload; + }, }, }); @@ -113,28 +70,13 @@ export const { setAge, setHeight, setMaritalStatus, - setMotherTongue, setReligion, - setMatchesWithHoroscope, setCaste, setSubCaste, - setStar, - setDasham, - setOccupation, - setAnnualIncome, - setEmployeeType, - setEducation, - setState, - setCountry, - setCitizenship, - setEatingHabits, - setSmokingHabits, - setDrinkingHabits, - setFamilyType, - setFamilyStatus, - setFamilyValue, updateFilter, resetFilters, + setPage, + setPaidMemberStatus, } = filterSlice.actions; export default filterSlice.reducer; diff --git a/src/redux/store.js b/src/redux/store.js index 92e486c..6161766 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,9 +1,36 @@ import { configureStore } from "@reduxjs/toolkit"; import registerformReducer from "./registrationFormSlice"; import filtersReducer from "./filterSlice"; +import { + persistStore, + persistReducer, + FLUSH, + REHYDRATE, + PAUSE, + PERSIST, + PURGE, + REGISTER, +} from "redux-persist"; +import storage from "redux-persist/lib/storage"; // defaults to localStorage for web + +const persistConfig = { + key: "filters", + storage, +}; + +const persistedFiltersReducer = persistReducer(persistConfig, filtersReducer); + export const store = configureStore({ reducer: { - registerform:registerformReducer, - filters:filtersReducer, + registerform: registerformReducer, + filters: persistedFiltersReducer, }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), }); + +export const persistor = persistStore(store);