From a81356e412166f7fb9f81e315d06058a11b6730e Mon Sep 17 00:00:00 2001 From: sina_sajjadi Date: Tue, 24 Sep 2024 12:57:52 +0330 Subject: [PATCH] validation with libphonenumber added , serchbar added --- package-lock.json | 22 +++ package.json | 3 + src/app/(account-pages)/my-trips/page.tsx | 2 +- .../(account-pages)/passengers-list/page.tsx | 2 +- .../(Header)/LangDropdown.tsx | 10 +- .../(client-components)/(Header)/MainNav1.tsx | 39 +++-- .../(Header)/SearchDropdown.tsx | 64 +++++-- .../(stay-search-form)/StaySearchForm.tsx | 4 +- .../HeroSearchForm2Mobile.tsx | 21 ++- .../(HeroSearchForm2Mobile)/LocationInput.tsx | 47 +++-- .../[[...stepIndex]]/PageAddListing1.tsx | 2 +- src/app/custom-history/page.tsx | 2 +- src/app/custom-trip/page.tsx | 7 + src/app/faq/table.tsx | 4 +- src/app/forgot-password/page.tsx | 39 +++-- src/app/login/page.tsx | 58 +++++-- src/app/signup/methodes/page.tsx | 5 +- src/app/signup/otp-code/page.tsx | 73 +++++++- src/app/signup/page.tsx | 20 +-- src/app/tours/Card.tsx | 138 +++++++++++++++ src/app/tours/SectionGridFilterCard.tsx | 160 +++++------------- src/app/tours/TabFilters.tsx | 6 +- src/app/tours/[slug]/page.tsx | 37 ++-- src/components/FooterNav.tsx | 14 +- src/components/SearchCard.tsx | 75 ++++++++ src/components/StayCard2.tsx | 5 +- src/components/contexts/tourDetails.tsx | 5 + src/components/contexts/userContext.tsx | 5 +- src/data/navigation.ts | 1 - src/hooks/FormValidation.ts | 23 ++- src/images/Group.svg | 7 + src/images/Vector.svg | 3 + src/shared/Navigation/NavMobile.tsx | 9 +- yarn.lock | 15 ++ 34 files changed, 632 insertions(+), 295 deletions(-) create mode 100644 src/app/tours/Card.tsx create mode 100644 src/components/SearchCard.tsx create mode 100644 src/images/Group.svg create mode 100644 src/images/Vector.svg diff --git a/package-lock.json b/package-lock.json index 91b5516..3cdfe06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,9 @@ "eslint": "8.41.0", "eslint-config-next": "^13.4.3", "framer-motion": "^10.12.16", + "google-libphonenumber": "^3.2.38", "google-map-react": "^2.2.1", + "libphonenumber-js": "^1.11.9", "lodash": "^4.17.21", "next": "^13.4.3", "next-i18next": "^15.3.1", @@ -42,6 +44,7 @@ "typescript": "5.0.4" }, "devDependencies": { + "@types/google-libphonenumber": "^7.4.30", "autoprefixer": "^10.4.14", "postcss": "^8.4.23", "tailwindcss": "^3.3.2" @@ -570,6 +573,12 @@ "node": ">=4" } }, + "node_modules/@types/google-libphonenumber": { + "version": "7.4.30", + "resolved": "https://registry.npmjs.org/@types/google-libphonenumber/-/google-libphonenumber-7.4.30.tgz", + "integrity": "sha512-Td1X1ayRxePEm6/jPHUBs2tT6TzW1lrVB6ZX7ViPGellyzO/0xMNi+wx5nH6jEitjznq276VGIqjK5qAju0XVw==", + "dev": true + }, "node_modules/@types/google-map-react": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@types/google-map-react/-/google-map-react-2.1.7.tgz", @@ -2574,6 +2583,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-libphonenumber": { + "version": "3.2.38", + "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.38.tgz", + "integrity": "sha512-t/K0dsVmA0gMMVLJgcMeB9g1Ar4ANVWfkY+AJGSdfyJ2Ay7Bu8ceLYpUlC6FZSilZgaF1qbkM9tZydGBEBHqAg==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/google-map-react": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/google-map-react/-/google-map-react-2.2.1.tgz", @@ -3324,6 +3341,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.9.tgz", + "integrity": "sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A==" + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", diff --git a/package.json b/package.json index eae0e20..a4439fb 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "eslint": "8.41.0", "eslint-config-next": "^13.4.3", "framer-motion": "^10.12.16", + "google-libphonenumber": "^3.2.38", "google-map-react": "^2.2.1", + "libphonenumber-js": "^1.11.9", "lodash": "^4.17.21", "next": "^13.4.3", "next-i18next": "^15.3.1", @@ -49,6 +51,7 @@ "typescript": "5.0.4" }, "devDependencies": { + "@types/google-libphonenumber": "^7.4.30", "autoprefixer": "^10.4.14", "postcss": "^8.4.23", "tailwindcss": "^3.3.2" diff --git a/src/app/(account-pages)/my-trips/page.tsx b/src/app/(account-pages)/my-trips/page.tsx index f087405..e0abd3b 100644 --- a/src/app/(account-pages)/my-trips/page.tsx +++ b/src/app/(account-pages)/my-trips/page.tsx @@ -23,7 +23,7 @@ const MyTrips = () => { useEffect(() => { if (!Object.keys(user).length) { - router.replace("/"); + router.replace("/signup"); } }, [user, router]); useEffect(() => { diff --git a/src/app/(account-pages)/passengers-list/page.tsx b/src/app/(account-pages)/passengers-list/page.tsx index 04e54e3..b93b0b3 100644 --- a/src/app/(account-pages)/passengers-list/page.tsx +++ b/src/app/(account-pages)/passengers-list/page.tsx @@ -15,7 +15,7 @@ const router = useRouter() useEffect(() => { if (!Object.keys(user).length) { - router.replace("/"); + router.replace("/signup"); } }, [user, router]); diff --git a/src/app/(client-components)/(Header)/LangDropdown.tsx b/src/app/(client-components)/(Header)/LangDropdown.tsx index 895ec8c..63c71ce 100644 --- a/src/app/(client-components)/(Header)/LangDropdown.tsx +++ b/src/app/(client-components)/(Header)/LangDropdown.tsx @@ -6,6 +6,8 @@ import { } from "@heroicons/react/24/outline"; import { FC, Fragment } from "react"; import { headerCurrency } from "./CurrencyDropdown"; +import { MdOutlineLanguage } from "react-icons/md"; + export const headerLanguage = [ { @@ -114,9 +116,9 @@ const LangDropdown: FC = ({ ${open ? "" : "text-opacity-80"} group self-center h-10 sm:h-12 px-3 py-1.5 inline-flex items-center text-sm text-gray-800 dark:text-neutral-200 font-medium hover:text-opacity-100 focus:outline-none `} > - - / - + + {/* / + */} = ({ leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - +
diff --git a/src/app/(client-components)/(Header)/MainNav1.tsx b/src/app/(client-components)/(Header)/MainNav1.tsx index cee1d5c..5d92155 100644 --- a/src/app/(client-components)/(Header)/MainNav1.tsx +++ b/src/app/(client-components)/(Header)/MainNav1.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import React, { FC, use, useContext, useEffect, useState } from "react"; import Logo from "@/shared/Logo"; @@ -8,19 +8,20 @@ import ButtonPrimary from "@/shared/ButtonPrimary"; import MenuBar from "@/shared/MenuBar"; import SwitchDarkMode from "@/shared/SwitchDarkMode"; import HeroSearchForm2MobileFactory from "../(HeroSearchForm2Mobile)/HeroSearchForm2MobileFactory"; -import { MdOutlineLanguage , MdOutlineCardTravel } from "react-icons/md"; +import { MdOutlineCardTravel } from "react-icons/md"; import Avatar from "@/shared/Avatar"; import Link from "next/link"; import { useUserContext } from "@/components/contexts/userContext"; +import LangDropdown from "./LangDropdown"; export interface MainNav1Props { className?: string; } const MainNav1: FC = ({ className = "" }) => { - const {user} = useUserContext() + const { user } = useUserContext(); -console.log(Object.keys(user).length); + console.log(Object.keys(user).length); return (
@@ -38,31 +39,37 @@ console.log(Object.keys(user).length);
-
- -
- - - + + + + +
- {Object.keys(user).length ? ( - + {Object.keys(user).length ? ( + + ) : ( +
+ LogIn Sign up +
)}
- +
diff --git a/src/app/(client-components)/(Header)/SearchDropdown.tsx b/src/app/(client-components)/(Header)/SearchDropdown.tsx index 76467f0..7d50866 100644 --- a/src/app/(client-components)/(Header)/SearchDropdown.tsx +++ b/src/app/(client-components)/(Header)/SearchDropdown.tsx @@ -1,8 +1,9 @@ -"use client"; - import { Popover, Transition } from "@headlessui/react"; import Input from "@/shared/Input"; -import React, { FC, Fragment } from "react"; +import React, { FC, Fragment, useEffect, useState } from "react"; +import { useToursContext } from "@/components/contexts/tourDetails"; +import SearchCard from "@/components/SearchCard"; +import axiosInstance from "@/components/api/axios"; interface Props { className?: string; @@ -11,6 +12,32 @@ interface Props { const SearchDropdown: FC = ({ className = "" }) => { const inputRef = React.createRef(); + const [value, setValue] = useState(""); + const [toursDetails, setToursDetail] = useState([]); + const { getTourData, tours } = useToursContext(); + + useEffect(() => { + // Fetch detailed tour data (including description) for all tours + tours?.results?.forEach((item) => { + axiosInstance + .get(`/api/tours/${item.id}/`) + .then((response) => { + setToursDetail((prev) => [...prev, { ...item, ...response.data }]); // Combine tour data with fetched detail + }) + .catch((error) => { + console.log(error); + }); + }); + }, [tours]); + + // Filter tours based on title or description using the fetched tour details + const filterdTours = toursDetails?.filter((item) => { + return ( + item.title.toLowerCase().includes(value.toLowerCase()) || + item.description?.toLowerCase().includes(value.toLowerCase()) + ); + }); + return ( @@ -29,7 +56,7 @@ const SearchDropdown: FC = ({ className = "" }) => { = ({ className = "" }) => { static className="absolute right-0 z-10 top-full w-screen max-w-sm" > -
- - -
+ { + setValue(e.target.value); + }} + ref={inputRef} + rounded="rounded-full" + type="text" + placeholder="Type and search" + />{value.replaceAll(" " , "") &&( + + {filterdTours?.length ? ( + filterdTours?.map((item) => ( + + )) + ) : ( +

No Matches Found

+ )} +
+ )}
diff --git a/src/app/(client-components)/(HeroSearchForm2Mobile)/(stay-search-form)/StaySearchForm.tsx b/src/app/(client-components)/(HeroSearchForm2Mobile)/(stay-search-form)/StaySearchForm.tsx index 47ff686..dc5797a 100644 --- a/src/app/(client-components)/(HeroSearchForm2Mobile)/(stay-search-form)/StaySearchForm.tsx +++ b/src/app/(client-components)/(HeroSearchForm2Mobile)/(stay-search-form)/StaySearchForm.tsx @@ -134,9 +134,9 @@ const StaySearchForm = () => { {/* */} {renderInputLocation()} {/* */} - {renderInputDates()} + {/* {renderInputDates()} */} {/* */} - {renderInputGuests()} + {/* {renderInputGuests()} */}
); diff --git a/src/app/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx b/src/app/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx index 828e8fc..e90d7db 100644 --- a/src/app/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx +++ b/src/app/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { Fragment, useState } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { Dialog, Tab, Transition } from "@headlessui/react"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "@heroicons/react/24/solid"; @@ -9,12 +9,15 @@ import { useTimeoutFn } from "react-use"; import StaySearchForm from "./(stay-search-form)/StaySearchForm"; import CarsSearchForm from "./(car-search-form)/CarsSearchForm"; import FlightSearchForm from "./(flight-search-form)/FlightSearchForm"; +import { usePathname, useRouter } from "next/navigation"; const HeroSearchForm2Mobile = () => { const [showModal, setShowModal] = useState(false); // FOR RESET ALL DATA WHEN CLICK CLEAR BUTTON const [showDialog, setShowDialog] = useState(false); + const path = usePathname() + let [, , resetIsShowingDialog] = useTimeoutFn(() => setShowDialog(true), 1); // function closeModal() { @@ -24,7 +27,9 @@ const HeroSearchForm2Mobile = () => { function openModal() { setShowModal(true); } - +useEffect(()=>{ + closeModal() +} , [path]) const renderButtonOpenModal = () => { return (
- {["Stay", "Experiences", "Cars", "Flights"].map( + {/* {["Stay", "Experiences", "Cars", "Flights"].map( (item, index) => ( {({ selected }) => ( @@ -109,7 +114,7 @@ const HeroSearchForm2Mobile = () => { )} ) - )} + )} */}
@@ -118,7 +123,7 @@ const HeroSearchForm2Mobile = () => {
- + {/*
@@ -132,10 +137,10 @@ const HeroSearchForm2Mobile = () => {
-
+
*/}
-
+ {/*
+
*/} )} diff --git a/src/app/(client-components)/(HeroSearchForm2Mobile)/LocationInput.tsx b/src/app/(client-components)/(HeroSearchForm2Mobile)/LocationInput.tsx index 008f340..278f63d 100644 --- a/src/app/(client-components)/(HeroSearchForm2Mobile)/LocationInput.tsx +++ b/src/app/(client-components)/(HeroSearchForm2Mobile)/LocationInput.tsx @@ -1,6 +1,8 @@ "use client"; +import { useToursContext } from "@/components/contexts/tourDetails"; import { MapPinIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/navigation"; import React, { useState, useEffect, useRef, FC } from "react"; interface Props { @@ -20,18 +22,21 @@ const LocationInput: FC = ({ const [value, setValue] = useState(""); const containerRef = useRef(null); const inputRef = useRef(null); + const { tours } = useToursContext() + const router = useRouter() useEffect(() => { setValue(defaultValue); }, [defaultValue]); const handleSelectLocation = (item: string) => { - // DO NOT REMOVE SETTIMEOUT FUNC - setTimeout(() => { - setValue(item); - onChange && onChange(item); - }, 0); + console.log(item); + + }; + console.log(tours.results); + + const filterdTours = tours?.results?.filter((item)=>{return item.title.toLowerCase().includes(value.toLowerCase())}) const renderSearchValues = ({ heading, @@ -49,12 +54,12 @@ const LocationInput: FC = ({ {items.map((item) => { return (
handleSelectLocation(item)} - key={item} + className="cursor-pointer py-2 mb-1 flex items-center space-x-3 text-sm" + onClick={() => router.push(`/tours/${item?.slug}-${item?.id}`)} + key={item.id} > - {item} + {item.title}
); })} @@ -72,7 +77,7 @@ const LocationInput: FC = ({
setValue(e.currentTarget.value)} ref={inputRef} @@ -84,29 +89,17 @@ const LocationInput: FC = ({
{value ? renderSearchValues({ - heading: "Locations", - items: [ - "Afghanistan", - "Albania", - "Algeria", - "American Samao", - "Andorra", - ], + heading: "Tours", + items: filterdTours, }) : renderSearchValues({ - heading: "Popular destinations", - items: [ - "Australia", - "Canada", - "Germany", - "United Kingdom", - "United Arab Emirates", - ], + heading: "All Tours", + items: tours?.results, })}
); }; - +// `/tours/${details?.slug}-${details?.id}` export default LocationInput; diff --git a/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx b/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx index 169603e..5b275a8 100644 --- a/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx +++ b/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx @@ -41,7 +41,7 @@ const PageAddListing1: FC = ({ const [loading, setLoading] = useState(false); if(!Object.keys(user).length){ - router.replace("/") + router.replace("/signup") } useEffect(() => { diff --git a/src/app/custom-history/page.tsx b/src/app/custom-history/page.tsx index 1320cba..44d0595 100644 --- a/src/app/custom-history/page.tsx +++ b/src/app/custom-history/page.tsx @@ -39,7 +39,7 @@ const PageAddListing10: FC = () => { console.log(error); }); }else{ - router.replace("/") + router.replace("/signup") } }, []); diff --git a/src/app/custom-trip/page.tsx b/src/app/custom-trip/page.tsx index c8ad67d..7ccb809 100644 --- a/src/app/custom-trip/page.tsx +++ b/src/app/custom-trip/page.tsx @@ -27,6 +27,13 @@ const CommonLayout: FC = () => { const router = useRouter(); + + useEffect(() => { + if (!Object.keys(user).length) { + router.replace("/signup"); + } + }, [user, router]); + const [countries, setCountries] = useState([]); const [startCity, setStartCity] = useState(""); const [startDate, setStartDate] = useState(""); diff --git a/src/app/faq/table.tsx b/src/app/faq/table.tsx index c52548f..9e3d033 100644 --- a/src/app/faq/table.tsx +++ b/src/app/faq/table.tsx @@ -20,7 +20,7 @@ const Table: React.FC = ({ faq, isActive, onClick }) => { onClick={onClick} > {faq.question} - {isActive ? "-" : "+"} + {isActive ? "-" : "+"} = ({ faq, isActive, onClick }) => { transition={{ duration: 0.4, ease: [0.6, 0.01, -0.05, 0.95] }} style={{ overflow: "hidden" }} > -

+

{faq.answer}

diff --git a/src/app/forgot-password/page.tsx b/src/app/forgot-password/page.tsx index 0bc6cad..c789bb3 100644 --- a/src/app/forgot-password/page.tsx +++ b/src/app/forgot-password/page.tsx @@ -9,18 +9,21 @@ import { useRouter } from "next/navigation"; import { useUserContext } from "@/components/contexts/userContext"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import { PhoneNumberUtil, PhoneNumberFormat } from "google-libphonenumber"; // Import libphonenumber export interface PageSignUpProps {} const PageSignUp: FC = () => { const router = useRouter(); const { setForm, setMethod } = useUserContext(); + const phoneUtil = PhoneNumberUtil.getInstance(); const [name, setName] = useState(''); const [countryCode, setCountryCode] = useState('968'); const [phoneNumber, setPhoneNumber] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [loading , setLoading] = useState(false) const [errors, setErrors] = useState<{ phoneNumber?: string; password?: string; confirmPassword?: string }>({}); @@ -30,16 +33,30 @@ const PageSignUp: FC = () => { } }; + const validatePhoneNumber = () => { + try { + const number = phoneUtil.parseAndKeepRawInput("+" + countryCode + phoneNumber, countryCode); + if (!phoneUtil.isValidNumber(number)) { + return "Invalid phone number."; + } + return null; + } catch (error) { + return "Invalid phone number."; + } + }; + const validateForm = () => { const newErrors: { phoneNumber?: string; password?: string; confirmPassword?: string } = {}; - - if (!phoneNumber) newErrors.phoneNumber = "Phone number is required."; + + const phoneError = validatePhoneNumber(); + if (phoneError) newErrors.phoneNumber = phoneError; if (!password) newErrors.password = "Password is required."; if (password !== confirmPassword) newErrors.confirmPassword = "Passwords do not match."; setErrors(newErrors); return Object.keys(newErrors).length === 0; // Return true if no errors }; + useEffect(() => { Object.values(errors).forEach((error) => { toast.error(error, { @@ -54,10 +71,11 @@ const PageSignUp: FC = () => { }); }); }, [errors]); + const submitHandler = async () => { + setLoading(true) setErrors({}); // Clear previous errors - if (!validateForm()) { return; // Prevent submission if there are validation errors } @@ -90,7 +108,10 @@ const PageSignUp: FC = () => { progress: undefined, theme: "light", }); + }finally{ + setLoading(false) } + }; return ( @@ -100,16 +121,13 @@ const PageSignUp: FC = () => { Change Password
- {/* FORM */}
e.preventDefault()}> -
); }; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index ce678f4..9588c88 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -9,16 +9,19 @@ import { useRouter } from "next/navigation"; import { useUserContext } from "@/components/contexts/userContext"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import { PhoneNumberUtil } from 'google-libphonenumber'; export interface PageLoginProps {} const PageLogin: FC = () => { const { user, setUser } = useUserContext(); const router = useRouter(); + const phoneUtil = PhoneNumberUtil.getInstance(); const [phoneNumber, setPhoneNumber] = useState(""); const [password, setPassword] = useState(""); const [countryCode, setCountryCode] = useState("968"); + const [loading, setLoading] = useState(false); const [errors, setErrors] = useState<{ phoneNumber?: string; password?: string }>({}); // Redirect to home if the user is already logged in @@ -28,7 +31,7 @@ const PageLogin: FC = () => { } }, [user, router]); - useEffect(()=>{ + useEffect(() => { Object.values(errors).forEach((error) => { toast.error(error, { position: "top-right", @@ -41,7 +44,7 @@ const PageLogin: FC = () => { theme: "light", }); }); - } , [errors]) + }, [errors]); const countryCodeHandler = (e: React.ChangeEvent) => { if (e.target.value.length <= 3) { @@ -49,12 +52,26 @@ const PageLogin: FC = () => { } }; + const validatePhoneNumber = () => { + try { + const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + countryCode + phoneNumber, countryCode); + if (!phoneUtil.isValidNumber(parsedNumber)) { + return "Invalid phone number."; + } + return null; + } catch (error) { + return "Invalid phone number format."; + } + }; + const validateForm = () => { const newErrors: { phoneNumber?: string; password?: string } = {}; - - if (!phoneNumber) { - newErrors.phoneNumber = "Phone number is required."; + + const phoneError = validatePhoneNumber(); + if (phoneError) { + newErrors.phoneNumber = phoneError; } + if (!password) { newErrors.password = "Password is required."; } @@ -67,21 +84,23 @@ const PageLogin: FC = () => { setErrors({}); // Clear previous errors if (!validateForm()) { - return; // Prevent submission if there are validation errors } + setLoading(true); + try { const response = await axiosInstance.post(`/api/account/login/`, { - phone_number: phoneNumber, + phone_number: `${countryCode}${phoneNumber}`, password, }); if (response.status === 201) { - toast.success("Your login was successful") + toast.success("Login successful!"); setUser(response.data); + router.push("/"); // Redirect to home or any other page after successful login } else { - toast.error("Something went wrong", { + toast.error("Login failed, please check your credentials.", { position: "top-right", autoClose: 5000, hideProgressBar: false, @@ -105,7 +124,7 @@ const PageLogin: FC = () => { theme: "light", }); } else { - toast.error("An unknown error occurred", { + toast.error("An unknown error occurred.", { position: "top-right", autoClose: 5000, hideProgressBar: false, @@ -116,6 +135,8 @@ const PageLogin: FC = () => { theme: "light", }); } + } finally { + setLoading(false); } }; @@ -127,14 +148,14 @@ const PageLogin: FC = () => {
e.preventDefault()}> -
); }; diff --git a/src/app/signup/methodes/page.tsx b/src/app/signup/methodes/page.tsx index 964426c..ef4c080 100644 --- a/src/app/signup/methodes/page.tsx +++ b/src/app/signup/methodes/page.tsx @@ -10,7 +10,7 @@ import { MdOutlineTextsms } from "react-icons/md"; function SelectMethods() { const router = useRouter(); - const { user, form } = useUserContext(); + const { user, form ,setForm } = useUserContext(); const [selectedMethod, setSelectedMethod] = useState(""); const [error, setError] = useState(""); @@ -56,6 +56,8 @@ function SelectMethods() { }); if (response.status === 202) { + setForm((prev) => ({ ...prev, verification_method: selectedMethod })); + setLoading(false); router.replace("signup/otp-code"); } @@ -68,6 +70,7 @@ function SelectMethods() { }); if (response.status === 202) { + setForm((prev) => ({ ...prev, verification_method: selectedMethod })); setLoading(false); router.replace("signup/otp-code"); } diff --git a/src/app/signup/otp-code/page.tsx b/src/app/signup/otp-code/page.tsx index df987ba..6e2dc3b 100644 --- a/src/app/signup/otp-code/page.tsx +++ b/src/app/signup/otp-code/page.tsx @@ -24,6 +24,8 @@ const PageSignUp: FC = () => { const [error, setError] = useState(""); const otpRefs = useRef<(HTMLInputElement | null)[]>([]); + const [loading, setLoading] = useState(false); + const handleOtpChange = (value: string, index: number) => { if (/^[0-9]?$/.test(value)) { const newOtp = [...otp]; @@ -46,17 +48,66 @@ const PageSignUp: FC = () => { useEffect(() => { if (time > 0) { - const timer = setInterval(() => setTime((prevTime) => prevTime - 1), 1000); + const timer = setInterval( + () => setTime((prevTime) => prevTime - 1), + 1000 + ); return () => clearInterval(timer); } }, [time]); - const handleResend = () => { + const handleResend = async () => { if (time === 0) { setTime(30); - // Logic to resend OTP can be added here + setLoading(true); + try { + const payload = { + phone_number: form.phoneNumber, + verification_method: form.verification_method, + range_phone: form.countryCode, + }; + + if (form.method === "register") { + payload.fullname = form.name; + payload.password = form.password; + payload.password_confirmation = form.confirmPassword; + + const response = await axiosInstance.post( + `/api/account/register/`, + payload, + { + headers: { Accept: "application/json" }, + } + ); + + if (response.status === 202) { + setLoading(false); + router.replace("signup/otp-code"); + } + } else if (form.method === "reset") { + payload.password = form.password; + payload.password_confirmation = form.confirmPassword; + + const response = await axiosInstance.post( + `/api/account/recover/`, + payload, + { + headers: { Accept: "application/json" }, + } + ); + + if (response.status === 202) { + setLoading(false); + } + } + } catch (error) { + setError(error); + console.log(error); + setLoading(false); + } } }; +console.log(form); const submitHandler = async () => { setError(""); @@ -69,11 +120,12 @@ const PageSignUp: FC = () => { if (response.status === 201) { setUser(response.data); - toast.success("Your Sign In was successful") + toast.success("Your Sign In was successful"); } else { toast.error("Something went wrong. Please try again."); } - } catch (error: any) { // Cast to 'any' or a specific error type + } catch (error: any) { + // Cast to 'any' or a specific error type toast.error(error.response?.data?.message || "An error occurred."); } }; @@ -85,7 +137,8 @@ const PageSignUp: FC = () => { Verification Code

- Enter the 5-digit code that we sent to complete your account registration + Enter the 5-digit code that we sent to complete your account + registration

@@ -105,13 +158,17 @@ const PageSignUp: FC = () => {

Haven't got the confirmation code yet?{" "} - {time > 0 && ({time} Seconds)} + {time > 0 && ( + ({time} Seconds) + )}

{error &&

{error}

} = () => { } }, [user, router]); - useEffect(()=>{ + useEffect(() => { Object.values(errors).forEach((error) => { toast.error(error, { position: "top-right", @@ -51,7 +51,7 @@ const PageSignUp: FC = () => { theme: "light", }); }); - } , [errors]) + }, [errors]); const submitHandler = async () => { const form = { @@ -102,16 +102,13 @@ const PageSignUp: FC = () => { placeholder="Full Name" className={`mt-1 ${errors.name ? "border-red-600" : "border-neutral-300"}`} /> -
); }; diff --git a/src/app/tours/Card.tsx b/src/app/tours/Card.tsx new file mode 100644 index 0000000..38f739a --- /dev/null +++ b/src/app/tours/Card.tsx @@ -0,0 +1,138 @@ +import React, { FC } from "react"; +import { DEMO_STAY_LISTINGS } from "@/data/listings"; +import { StayDataType } from "@/data/types"; +import StartRating from "@/components/StartRating"; +import BtnLikeIcon from "@/components/BtnLikeIcon"; +import SaleOffBadge from "@/components/SaleOffBadge"; +import Badge from "@/shared/Badge"; +import Link from "next/link"; +import Image from "next/image"; +import calender from "../../images/Group.svg"; + +export interface StayCard2Props { + className?: string; + data?: StayDataType; + size?: "default" | "small"; +} + +const DEMO_DATA = DEMO_STAY_LISTINGS[0]; + +const StayCard2: FC = ({ + size = "default", + className = "", + data = DEMO_DATA, +}) => { + const { + galleryImgs, + listingCategory, + address, + title, + bedrooms, + href, + like, + saleOff, + isAds, + price, + reviewStart, + reviewCount, + id, + } = data; + + // Function to format the dates + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + // Function to calculate the number of days between two dates + const calculateDuration = (start: string, end: string) => { + const startDate = new Date(start); + const endDate = new Date(end); + const timeDiff = endDate.getTime() - startDate.getTime(); + const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)); // Convert ms to days + return daysDiff; + }; + + const renderSliderGallery = () => { + return ( + <> + +
+ {title +
+ + { + + } + + ); + }; + console.log(data); + + const renderContent = () => { + const formattedStartDate = formatDate(data.started_at); + const formattedEndDate = formatDate(data.ended_at); + const tripDuration = calculateDuration(data.started_at, data.ended_at); + + return ( +
+
+
+

+ {title} +

+
+
+ calendar + + {formattedStartDate} - {formattedEndDate} + +
+

+ ({tripDuration} Days) +

+
+
+
+ + {price} + {` `} + + {!!reviewStart && ( + + )} +
+
+ ); + }; + + return ( +
+ {renderSliderGallery()} + {renderContent()} +
+ ); +}; + +export default StayCard2; diff --git a/src/app/tours/SectionGridFilterCard.tsx b/src/app/tours/SectionGridFilterCard.tsx index d3cfaa5..2ac85f9 100644 --- a/src/app/tours/SectionGridFilterCard.tsx +++ b/src/app/tours/SectionGridFilterCard.tsx @@ -5,16 +5,10 @@ import { DEMO_STAY_LISTINGS } from "@/data/listings"; import { StayDataType } from "@/data/types"; import TabFilters from "./TabFilters"; import Heading2 from "@/shared/Heading2"; -import StayCard2 from "@/components/StayCard2"; -import { Context, useToursContext } from "@/components/contexts/tourDetails"; -import { useSearchParams } from "next/navigation"; -import { motion, AnimatePresence, MotionConfig } from "framer-motion"; -import { useSwipeable } from "react-swipeable"; -import ButtonPrimary from "@/shared/ButtonPrimary"; -import NextBtn from "@/components/NextBtn"; -import PrevBtn from "@/components/PrevBtn"; -import { variants } from "@/utils/animationVariants"; -import { useWindowSize } from "react-use"; +import StayCard2 from "./Card"; +import { useToursContext } from "@/components/contexts/tourDetails"; +import { useParams, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; export interface SectionGridFilterCardProps { className?: string; @@ -25,26 +19,27 @@ const SectionGridFilterCard: FC = ({ className = "", data = DEMO_STAY_LISTINGS, }) => { - const { countries, tours } = useToursContext() + const { countries, tours } = useToursContext(); const [countryTours, setCountryTours] = useState(tours.results || []); const [checked, setChecked] = useState<{ [key: string]: boolean }>({}); - const searchParams = useSearchParams(); - const [currentIndex, setCurrentIndex] = useState(0); - const [numberOfItems, setNumberOfItems] = useState(4); // Adjust as needed - const [direction, setDirection] = useState(0); - // Get the list of selected countries - const filteredCountries = Object.keys(checked).filter( - (countryName) => checked[countryName] - ); - const windowWidth = useWindowSize().width; + const searchParams = useSearchParams() +console.log(searchParams); - useEffect(() => { - const country = searchParams.get("country"); - if (country) { - setChecked({ [country]: true }); + // Get the list of selected countries + const filteredCountries = Object.keys(checked).filter((countryName) => checked[countryName]); + + useEffect(()=>{ + const country = searchParams.get("country") + if (searchParams.has("country")){ + setChecked({ + [country] : true + }) } - }, []); + } , [searchParams]) +console.log(checked); + + useEffect(() => { if (!tours.results) return; @@ -65,110 +60,33 @@ const SectionGridFilterCard: FC = ({ setCountryTours(filteredTours); } - setCurrentIndex(0) }, [checked, countries, tours.results]); - useEffect(() => { - if (windowWidth < 320) { - return setNumberOfItems(1); - } - if (windowWidth < 500) { - return setNumberOfItems(2); - } - if (windowWidth < 1024) { - return setNumberOfItems(3); - } - if (windowWidth < 1280) { - return setNumberOfItems(4); - } - - setNumberOfItems(4); - }, [windowWidth]); - - function changeItemId(newVal: number) { - if (newVal > currentIndex) { - setDirection(1); - } else { - setDirection(-1); - } - setCurrentIndex(newVal); - } - - const handlers = useSwipeable({ - onSwipedLeft: () => { - if (currentIndex < countryTours.length - numberOfItems) { - changeItemId(currentIndex + 1); - } - }, - onSwipedRight: () => { - if (currentIndex > 0) { - changeItemId(currentIndex - 1); - } - }, - trackMouse: true, - }); - return ( -
- +
+
+

{"All Tours"}

+ {/* + 233 stays + · + Aug 12 - 18 + ·2 Guests + */} + +
- -
-
- - - {countryTours.map((stay, indx) => ( - - - - ))} - - -
- - {currentIndex > 0 && ( - changeItemId(currentIndex - 1)} - className="w-9 h-9 xl:w-12 xl:h-12 text-lg absolute -left-3 xl:-left-6 top-1/3 -translate-y-1/2 z-[1]" - /> - )} - - {countryTours.length > currentIndex + numberOfItems && ( - changeItemId(currentIndex + 1)} - className="w-9 h-9 xl:w-12 xl:h-12 text-lg absolute -right-3 xl:-right-6 top-1/3 -translate-y-1/2 z-[1]" - /> - )} -
-
- - {countryTours.length === 0 &&

No tours Available

} +
+ {countryTours.length > 0 ? ( + countryTours.map((stay) => ) + ) : ( +

No tours Available

+ )} +
); }; -export default SectionGridFilterCard; +export default SectionGridFilterCard; \ No newline at end of file diff --git a/src/app/tours/TabFilters.tsx b/src/app/tours/TabFilters.tsx index c5a69b2..998b50f 100644 --- a/src/app/tours/TabFilters.tsx +++ b/src/app/tours/TabFilters.tsx @@ -651,13 +651,13 @@ const renderMoreFilterItem = (
-
- + {/* Clear - + */} = ({}) => {
{/* PRICE */}
- - {details?.price} - {/* - /night - */} - + {+details?.price === +details?.final_price ? ( + {details?.price} + ) : ( +
+ {details?.final_price} + {details?.price}{" "} +
+ )} +
@@ -509,13 +512,13 @@ const ListingStayDetailPage: FC = ({}) => {
- {details?.price} x {passengers} passengers + {details?.final_price} x {passengers} passengers {" "} - {isNaN(details?.price * passengers) + {isNaN(details?.final_price * passengers) ? "N/A" // Or any fallback value, like "0" or a string message - : (details?.price * passengers).toString()} + : (details?.final_price * passengers).toString()}
@@ -554,13 +557,15 @@ const ListingStayDetailPage: FC = ({}) => {
- {details && } + {details && ( + + )} {/*
{ + axiosInstance + .get(`/api/tours/${data.id}/`) + .then((response) => { + setTourDetail(response.data); + }) + .catch((error) => { + console.log(error); + }); + }, [data]); + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + // Function to calculate the number of days between two dates + const calculateDuration = (start: string, end: string) => { + const startDate = new Date(start); + const endDate = new Date(end); + const timeDiff = endDate.getTime() - startDate.getTime(); + const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)); // Convert ms to days + return daysDiff; + }; + + const formattedStartDate = formatDate(data.started_at); + const formattedEndDate = formatDate(data.ended_at); + const tripDuration = calculateDuration(data.started_at, data.ended_at); + + if (tourDetail) { + return ( +
+
+ {data.title} +
+
+

{data?.title}

+
+ calendar + + {formattedStartDate} - {formattedEndDate} + +
+

+ ({tripDuration} Days) +

+
+
+ ); + } +} + +export default SearchCard; diff --git a/src/components/StayCard2.tsx b/src/components/StayCard2.tsx index 514374d..24d67bf 100644 --- a/src/components/StayCard2.tsx +++ b/src/components/StayCard2.tsx @@ -38,9 +38,11 @@ const StayCard2: FC = ({ reviewCount, id, } = data; - + const renderSliderGallery = () => { + + return ( <> = ({ }; const renderContent = () => { + return (
diff --git a/src/components/contexts/tourDetails.tsx b/src/components/contexts/tourDetails.tsx index cf3e25e..5f95ea5 100644 --- a/src/components/contexts/tourDetails.tsx +++ b/src/components/contexts/tourDetails.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePathname } from "next/navigation"; import axiosInstance from "../api/axios"; import React, { createContext, useContext, useEffect, useState, ReactNode } from "react"; @@ -45,6 +46,7 @@ export const ContextProvider = ({ children }: ContextProviderProps) => { const [passengers, setPassengers] = useState(0); const [tours, setTours] = useState([]); const [countries, setCountries] = useState([]); + const path = usePathname() useEffect(() => { axiosInstance @@ -68,6 +70,9 @@ export const ContextProvider = ({ children }: ContextProviderProps) => { }); }, []); + useEffect(()=>{ + setDetails(undefined) + } , [path]) const getTourData = async (item: number) => { try { const response = await axiosInstance.get(`/api/tours/${item}/`); diff --git a/src/components/contexts/userContext.tsx b/src/components/contexts/userContext.tsx index 39fba5f..c88ea4b 100644 --- a/src/components/contexts/userContext.tsx +++ b/src/components/contexts/userContext.tsx @@ -1,5 +1,6 @@ "use client"; +import { useRouter } from "next/navigation"; import React, { createContext, useContext, useState, ReactNode, useEffect } from "react"; type UserContextType = { @@ -22,7 +23,7 @@ export const UserProvider: React.FC = ({ children }) => { const [form, setForm] = useState>({}); const [user, setUser] = useState>({}); const [method, setMethod] = useState([]); - + const router = useRouter() // Load user from localStorage on initial render useEffect(() => { const storedUser = localStorage.getItem("user"); @@ -44,6 +45,8 @@ export const UserProvider: React.FC = ({ children }) => { const clerUser = () => { setUser({}); localStorage.removeItem("user"); + router.replace("/") + }; return ( diff --git a/src/data/navigation.ts b/src/data/navigation.ts index 446f0aa..1d8ed78 100644 --- a/src/data/navigation.ts +++ b/src/data/navigation.ts @@ -175,7 +175,6 @@ export const NAVIGATION_DEMO: NavItemType[] = [ href: "/tours", name: "All Tours", type: "dropdown", - children: demoChildMenus, }, { id: ncNanoId(), diff --git a/src/hooks/FormValidation.ts b/src/hooks/FormValidation.ts index 49e09b2..abe0d21 100644 --- a/src/hooks/FormValidation.ts +++ b/src/hooks/FormValidation.ts @@ -1,17 +1,10 @@ // hooks/FormValidation.ts import { useState } from 'react'; - -// Define a type for the form structure -interface SignUpForm { - name: string; - countryCode: string; - phoneNumber: string; - password: string; - confirmPassword: string; -} +import { PhoneNumberUtil } from 'google-libphonenumber'; const useFormValidation = () => { const [errors, setErrors] = useState>({}); + const phoneUtil = PhoneNumberUtil.getInstance(); const validateForm = (form: SignUpForm) => { let newErrors: Record = {}; @@ -24,8 +17,14 @@ const useFormValidation = () => { newErrors.countryCode = 'Country Code must be a number with up to 3 digits'; } - if (!form.phoneNumber || !/^\d+$/.test(form.phoneNumber)) { - newErrors.phoneNumber = 'Phone Number is required and must be a number'; + // Validate phone number using google-libphonenumber + try { + const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + form.countryCode + form.phoneNumber, form.countryCode); + if (!phoneUtil.isValidNumber(parsedNumber)) { + newErrors.phoneNumber = 'Invalid phone number for the selected country'; + } + } catch (error) { + newErrors.phoneNumber = 'Invalid phone number format'; } if (!form.password) { @@ -44,7 +43,7 @@ const useFormValidation = () => { return Object.keys(newErrors).length === 0; }; - return { errors, validateForm, setErrors }; // Optionally return setErrors + return { errors, validateForm, setErrors }; }; export default useFormValidation; diff --git a/src/images/Group.svg b/src/images/Group.svg new file mode 100644 index 0000000..13cee10 --- /dev/null +++ b/src/images/Group.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/images/Vector.svg b/src/images/Vector.svg new file mode 100644 index 0000000..8b7cc68 --- /dev/null +++ b/src/images/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/Navigation/NavMobile.tsx b/src/shared/Navigation/NavMobile.tsx index 2794194..db2eeb1 100644 --- a/src/shared/Navigation/NavMobile.tsx +++ b/src/shared/Navigation/NavMobile.tsx @@ -128,14 +128,7 @@ const NavMobile: React.FC = ({ {data.map(_renderItem)}
- - Get Template - + Custom Tour