Browse Source

validation with libphonenumber added , serchbar added

main
sina_sajjadi 2 months ago
parent
commit
a81356e412
  1. 22
      package-lock.json
  2. 3
      package.json
  3. 2
      src/app/(account-pages)/my-trips/page.tsx
  4. 2
      src/app/(account-pages)/passengers-list/page.tsx
  5. 10
      src/app/(client-components)/(Header)/LangDropdown.tsx
  6. 39
      src/app/(client-components)/(Header)/MainNav1.tsx
  7. 64
      src/app/(client-components)/(Header)/SearchDropdown.tsx
  8. 4
      src/app/(client-components)/(HeroSearchForm2Mobile)/(stay-search-form)/StaySearchForm.tsx
  9. 21
      src/app/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx
  10. 47
      src/app/(client-components)/(HeroSearchForm2Mobile)/LocationInput.tsx
  11. 2
      src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx
  12. 2
      src/app/custom-history/page.tsx
  13. 7
      src/app/custom-trip/page.tsx
  14. 4
      src/app/faq/table.tsx
  15. 39
      src/app/forgot-password/page.tsx
  16. 58
      src/app/login/page.tsx
  17. 5
      src/app/signup/methodes/page.tsx
  18. 73
      src/app/signup/otp-code/page.tsx
  19. 20
      src/app/signup/page.tsx
  20. 138
      src/app/tours/Card.tsx
  21. 160
      src/app/tours/SectionGridFilterCard.tsx
  22. 6
      src/app/tours/TabFilters.tsx
  23. 37
      src/app/tours/[slug]/page.tsx
  24. 14
      src/components/FooterNav.tsx
  25. 75
      src/components/SearchCard.tsx
  26. 5
      src/components/StayCard2.tsx
  27. 5
      src/components/contexts/tourDetails.tsx
  28. 5
      src/components/contexts/userContext.tsx
  29. 1
      src/data/navigation.ts
  30. 23
      src/hooks/FormValidation.ts
  31. 7
      src/images/Group.svg
  32. 3
      src/images/Vector.svg
  33. 9
      src/shared/Navigation/NavMobile.tsx
  34. 15
      yarn.lock

22
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",

3
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"

2
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(() => {

2
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]);

10
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<LangDropdownProps> = ({
${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 `}
>
<GlobeAltIcon className="w-5 h-5 opacity-80" />
<span className="mx-1">/</span>
<BanknotesIcon className="w-5 h-5 opacity-80" />
<MdOutlineLanguage size={25}/>
{/* <span className="mx-1">/</span>
<BanknotesIcon className="w-5 h-5 opacity-80" /> */}
<ChevronDownIcon
className={`${open ? "-rotate-180" : "text-opacity-70"}
ml-1 h-4 w-4 group-hover:text-opacity-80 transition ease-in-out duration-150`}
@ -132,7 +134,7 @@ const LangDropdown: FC<LangDropdownProps> = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className={`absolute z-20 ${panelClassName}`}>
<Popover.Panel className={`absolute z-20 ${panelClassName}`}>
<div className="p-3 sm:p-6 rounded-2xl bg-white dark:bg-neutral-800 shadow-lg ring-1 ring-black ring-opacity-5">
<Tab.Group>
<Tab.List className="flex space-x-1 rounded-full bg-gray-100 dark:bg-slate-700 p-1">

39
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<MainNav1Props> = ({ className = "" }) => {
const {user} = useUserContext()
const { user } = useUserContext();
console.log(Object.keys(user).length);
console.log(Object.keys(user).length);
return (
<div className={`nc-MainNav1 relative z-10 ${className}`}>
@ -38,31 +39,37 @@ console.log(Object.keys(user).length);
<div className="hidden md:flex flex-shrink-0 justify-end flex-1 lg:flex-none text-neutral-700 dark:text-neutral-100">
<div className="hidden xl:flex space-x-0.5 items-center">
<div className="cursor-pointer self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center">
<MdOutlineLanguage size={25} />
</div>
<Link href={"/my-trips"} className="self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center">
<MdOutlineCardTravel size={25} />
</Link>
<LangDropdown />
<Link
href={`${Object.keys(user).length ? "/my-trips" : "signup"}`}
className="self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center"
>
<MdOutlineCardTravel size={25} />
</Link>
<SwitchDarkMode />
<SearchDropdown className="flex items-center" />
<div className="px-1" />
{Object.keys(user).length ? (<Link href={"/account"}>
<Avatar
imgUrl={user?.avatar}
sizeClass="w-10 h-10"
/>
{Object.keys(user).length ? (
<Link className="self-center" href={"/account"}>
<Avatar imgUrl={user?.avatar} sizeClass="w-10 h-10" />
</Link>
) : (
<div >
<Link className="mr-4 text-md" href={"/login"}>LogIn</Link>
<ButtonPrimary className="self-center" href="/signup">
Sign up
</ButtonPrimary>
</div>
)}
</div>
<div className="flex xl:hidden items-center">
<div className="cursor-pointer self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center">
<MdOutlineLanguage size={25} />
<LangDropdown
className="flex"
panelClassName="z-10 w-screen max-w-[280px] px-4 mb-3 right-3 bottom-full sm:px-0"
/>
</div>
<SwitchDarkMode />
<div className="px-0.5" />

64
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<Props> = ({ className = "" }) => {
const inputRef = React.createRef<HTMLInputElement>();
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 (
<React.Fragment>
<Popover className={`relative ${className}`}>
@ -29,7 +56,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
<Transition
show={open}
as={Fragment}
as="div"
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
@ -41,15 +68,26 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
static
className="absolute right-0 z-10 top-full w-screen max-w-sm"
>
<form action="" method="POST">
<Input
ref={inputRef}
rounded="rounded-full"
type="search"
placeholder="Type and press enter"
/>
<input type="submit" hidden value="" />
</form>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
ref={inputRef}
rounded="rounded-full"
type="text"
placeholder="Type and search"
/>{value.replaceAll(" " , "") &&(
<Popover.Panel className="absolute right-0 z-10 top-full w-screen max-w-sm bg-white shadow-md rounded-3xl mt-1 ">
{filterdTours?.length ? (
filterdTours?.map((item) => (
<SearchCard key={item.id} data={item} />
))
) : (
<h3 className="p-4 text-center">No Matches Found</h3>
)}
</Popover.Panel>
)}
</Popover.Panel>
</Transition>
</>

4
src/app/(client-components)/(HeroSearchForm2Mobile)/(stay-search-form)/StaySearchForm.tsx

@ -134,9 +134,9 @@ const StaySearchForm = () => {
{/* */}
{renderInputLocation()}
{/* */}
{renderInputDates()}
{/* {renderInputDates()} */}
{/* */}
{renderInputGuests()}
{/* {renderInputGuests()} */}
</div>
</div>
);

21
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 (
<button
@ -88,7 +93,7 @@ const HeroSearchForm2Mobile = () => {
</div>
<Tab.List className="pt-12 flex w-full justify-center font-semibold text-sm sm:text-base text-neutral-500 dark:text-neutral-400 space-x-6 sm:space-x-8">
{["Stay", "Experiences", "Cars", "Flights"].map(
{/* {["Stay", "Experiences", "Cars", "Flights"].map(
(item, index) => (
<Tab key={index} as={Fragment}>
{({ selected }) => (
@ -109,7 +114,7 @@ const HeroSearchForm2Mobile = () => {
)}
</Tab>
)
)}
)} */}
</Tab.List>
<div className="flex-1 pt-3 px-1.5 sm:px-4 flex overflow-hidden">
<Tab.Panels className="flex-1 overflow-y-auto hiddenScrollbar py-4">
@ -118,7 +123,7 @@ const HeroSearchForm2Mobile = () => {
<StaySearchForm />
</div>
</Tab.Panel>
<Tab.Panel>
{/* <Tab.Panel>
<div className="transition-opacity animate-[myblur_0.4s_ease-in-out]">
<StaySearchForm />
</div>
@ -132,10 +137,10 @@ const HeroSearchForm2Mobile = () => {
<div className="transition-opacity animate-[myblur_0.4s_ease-in-out]">
<FlightSearchForm />
</div>
</Tab.Panel>
</Tab.Panel> */}
</Tab.Panels>
</div>
<div className="px-4 py-3 bg-white dark:bg-neutral-900 border-t border-neutral-200 dark:border-neutral-700 flex justify-between">
{/* <div className="px-4 py-3 bg-white dark:bg-neutral-900 border-t border-neutral-200 dark:border-neutral-700 flex justify-between">
<button
type="button"
className="underline font-semibold flex-shrink-0"
@ -151,7 +156,7 @@ const HeroSearchForm2Mobile = () => {
closeModal();
}}
/>
</div>
</div> */}
</Tab.Group>
)}
</Dialog.Panel>

47
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<Props> = ({
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<Props> = ({
{items.map((item) => {
return (
<div
className="py-2 mb-1 flex items-center space-x-3 text-sm"
onClick={() => 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}
>
<MapPinIcon className="w-5 h-5 text-neutral-500 dark:text-neutral-400" />
<span className="">{item}</span>
<span className="">{item.title}</span>
</div>
);
})}
@ -72,7 +77,7 @@ const LocationInput: FC<Props> = ({
<div className="relative mt-5">
<input
className={`block w-full bg-transparent border px-4 py-3 pr-12 border-neutral-900 dark:border-neutral-200 rounded-xl focus:ring-0 focus:outline-none text-base leading-none placeholder-neutral-500 dark:placeholder-neutral-300 truncate font-bold placeholder:truncate`}
placeholder={"Search destinations"}
placeholder={"Search Tours"}
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
ref={inputRef}
@ -84,29 +89,17 @@ const LocationInput: FC<Props> = ({
<div className="mt-7">
{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,
})}
</div>
</div>
</div>
);
};
// `/tours/${details?.slug}-${details?.id}`
export default LocationInput;

2
src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx

@ -41,7 +41,7 @@ const PageAddListing1: FC<PageAddListing1Props> = ({
const [loading, setLoading] = useState(false);
if(!Object.keys(user).length){
router.replace("/")
router.replace("/signup")
}
useEffect(() => {

2
src/app/custom-history/page.tsx

@ -39,7 +39,7 @@ const PageAddListing10: FC<PageAddListing10Props> = () => {
console.log(error);
});
}else{
router.replace("/")
router.replace("/signup")
}
}, []);

7
src/app/custom-trip/page.tsx

@ -27,6 +27,13 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
const router = useRouter();
useEffect(() => {
if (!Object.keys(user).length) {
router.replace("/signup");
}
}, [user, router]);
const [countries, setCountries] = useState<Country[]>([]);
const [startCity, setStartCity] = useState<string>("");
const [startDate, setStartDate] = useState<string>("");

4
src/app/faq/table.tsx

@ -20,7 +20,7 @@ const Table: React.FC<TableProps> = ({ faq, isActive, onClick }) => {
onClick={onClick}
>
{faq.question}
<span>{isActive ? "-" : "+"}</span>
<span >{isActive ? "-" : "+"}</span>
</h3>
<motion.div
initial={false}
@ -28,7 +28,7 @@ const Table: React.FC<TableProps> = ({ faq, isActive, onClick }) => {
transition={{ duration: 0.4, ease: [0.6, 0.01, -0.05, 0.95] }}
style={{ overflow: "hidden" }}
>
<p className={`mt-2 text-gray-700 ${isActive ? "block" : "hidden"}`}>
<p className={`mt-2 text-gray-700 dark:text-white ${isActive ? "block" : "hidden"}`}>
{faq.answer}
</p>
</motion.div>

39
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<PageSignUpProps> = () => {
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<PageSignUpProps> = () => {
}
};
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<PageSignUpProps> = () => {
});
});
}, [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<PageSignUpProps> = () => {
progress: undefined,
theme: "light",
});
}finally{
setLoading(false)
}
};
return (
@ -100,16 +121,13 @@ const PageSignUp: FC<PageSignUpProps> = () => {
Change Password
</h2>
<div className="max-w-md mx-auto space-y-6">
{/* FORM */}
<form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}>
<label className="block">
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">
Phone Number
</span>
<div className={`flex items-center mt-1 rounded-2xl ${errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200" } bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">
+
</span>
<div className={`flex items-center mt-1 rounded-2xl ${errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"} bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">+</span>
<input
value={countryCode}
onChange={countryCodeHandler}
@ -145,11 +163,10 @@ const PageSignUp: FC<PageSignUpProps> = () => {
className={`mt-1 ${errors.confirmPassword ? "border-red-600" : ""}`}
/>
</label>
<ButtonPrimary onClick={submitHandler}>Continue</ButtonPrimary>
<ButtonPrimary loading={loading} onClick={submitHandler}>Continue</ButtonPrimary>
</form>
</div>
</div>
</div>
);
};

58
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<PageLoginProps> = () => {
const { user, setUser } = useUserContext();
const router = useRouter();
const phoneUtil = PhoneNumberUtil.getInstance();
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [countryCode, setCountryCode] = useState<string>("968");
const [loading, setLoading] = useState<boolean>(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<PageLoginProps> = () => {
}
}, [user, router]);
useEffect(()=>{
useEffect(() => {
Object.values(errors).forEach((error) => {
toast.error(error, {
position: "top-right",
@ -41,7 +44,7 @@ const PageLogin: FC<PageLoginProps> = () => {
theme: "light",
});
});
} , [errors])
}, [errors]);
const countryCodeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= 3) {
@ -49,12 +52,26 @@ const PageLogin: FC<PageLoginProps> = () => {
}
};
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<PageLoginProps> = () => {
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<PageLoginProps> = () => {
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<PageLoginProps> = () => {
theme: "light",
});
}
} finally {
setLoading(false);
}
};
@ -127,14 +148,14 @@ const PageLogin: FC<PageLoginProps> = () => {
</h2>
<div className="max-w-md mx-auto space-y-6">
<form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}>
<label className="block">
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">
Phone Number
</span>
<div className={`flex items-center mt-1 rounded-2xl ${errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200" } bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">
+
</span>
<div className={`flex items-center mt-1 rounded-2xl ${
errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"
} bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">+</span>
<input
value={countryCode}
onChange={countryCodeHandler}
@ -154,7 +175,7 @@ const PageLogin: FC<PageLoginProps> = () => {
</label>
<label className="block">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
Password
<Link href="/forgot-password" className="text-sm underline font-medium">
Forgot password?
@ -168,7 +189,9 @@ const PageLogin: FC<PageLoginProps> = () => {
/>
</label>
<ButtonPrimary onClick={submitHandler}>Continue</ButtonPrimary>
<ButtonPrimary onClick={submitHandler} loading={loading}>
Continue
</ButtonPrimary>
</form>
<span className="block text-center text-neutral-700 dark:text-neutral-300">
@ -178,7 +201,6 @@ const PageLogin: FC<PageLoginProps> = () => {
</span>
</div>
</div>
</div>
);
};

5
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");
}

73
src/app/signup/otp-code/page.tsx

@ -24,6 +24,8 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const [error, setError] = useState<string>("");
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<PageSignUpProps> = () => {
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<PageSignUpProps> = () => {
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<PageSignUpProps> = () => {
Verification Code
</h2>
<p className="text-center text-sm text-neutral-500 mb-4">
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
</p>
<div className="max-w-sm mx-auto space-y-6">
<div className="flex justify-center space-x-2 mb-4">
@ -105,13 +158,17 @@ const PageSignUp: FC<PageSignUpProps> = () => {
<p className="text-center text-sm text-neutral-500 mb-4">
Haven't got the confirmation code yet?{" "}
<button
className={`text-primary-600 hover:underline ${time > 0 ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}
className={`text-primary-600 hover:underline ${
time > 0 ? "cursor-not-allowed opacity-50" : "cursor-pointer"
}`}
onClick={handleResend}
disabled={time > 0}
>
Resend
</button>
{time > 0 && <span className="text-xs text-neutral-400">({time} Seconds)</span>}
{time > 0 && (
<span className="text-xs text-neutral-400">({time} Seconds)</span>
)}
</p>
{error && <p className="text-red-500 text-xs">{error}</p>}
<ButtonPrimary

20
src/app/signup/page.tsx

@ -38,7 +38,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
}
}, [user, router]);
useEffect(()=>{
useEffect(() => {
Object.values(errors).forEach((error) => {
toast.error(error, {
position: "top-right",
@ -51,7 +51,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
theme: "light",
});
});
} , [errors])
}, [errors]);
const submitHandler = async () => {
const form = {
@ -102,16 +102,13 @@ const PageSignUp: FC<PageSignUpProps> = () => {
placeholder="Full Name"
className={`mt-1 ${errors.name ? "border-red-600" : "border-neutral-300"}`}
/>
</label>
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">
Phone Number
</span>
<div className={`flex items-center mt-1 rounded-2xl ${errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200" } bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">
+
</span>
<span className="text-neutral-800 dark:text-neutral-200">Phone Number</span>
<div className={`flex items-center mt-1 rounded-2xl ${
errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"
} bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">+</span>
<input
value={countryCode}
onChange={countryCodeHandler}
@ -153,7 +150,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
onClick={submitHandler}
disabled={loading}
>
{loading ? "Loading..." : "Continue"}
Continue
</ButtonPrimary>
</form>
<span className="not-italic block text-center text-neutral-700 dark:text-neutral-300">
@ -164,7 +161,6 @@ const PageSignUp: FC<PageSignUpProps> = () => {
</span>
</div>
</div>
</div>
);
};

138
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<StayCard2Props> = ({
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 (
<>
<Link
href={`/tours/${data?.slug}-${data?.id}`}
className="relative w-full"
>
<div className="h-40 lg:h-56 sm:h-40 overflow-hidden rounded-xl">
<Image
className="w-full h-full object-cover"
src={data?.image?.image_url?.md || "/default-image.jpg"}
alt={title || "Stay Image"}
width={500}
height={300}
/>
</div>
</Link>
{
<Badge
className="opacity-70 absolute left-3 top-3"
name={data.status}
color={data.status}
/>
}
</>
);
};
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 (
<div className={size === "default" ? "mt-3 space-y-3" : "mt-2 space-y-2"}>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<h2
className={`font-semibold capitalize text-neutral-900 dark:text-white ${
size === "default" ? "text-base" : "text-base"
}`}
>
<span className="line-clamp-1">{title}</span>
</h2>
</div>
<div className="flex items-center text-neutral-500 dark:text-neutral-400 text-sm space-x-1.5">
<Image alt="calendar" src={calender} />
<span className="">
{formattedStartDate} - {formattedEndDate}
</span>
</div>
<p className="flex items-center text-neutral-500 dark:text-neutral-400 text-sm space-x-1.5">
({tripDuration} Days)
</p>
</div>
<div className="w-14 border-b border-neutral-100 dark:border-neutral-800"></div>
<div className="flex justify-between items-center">
<span className="text-base font-semibold">
{price}
{` `}
</span>
{!!reviewStart && (
<StartRating reviewCount={reviewCount} point={reviewStart} />
)}
</div>
</div>
);
};
return (
<div className={`nc-StayCard2 group relative ${className}`}>
{renderSliderGallery()}
<Link href={`/tours/${data?.slug}-${data?.id}`}>{renderContent()}</Link>
</div>
);
};
export default StayCard2;

160
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<SectionGridFilterCardProps> = ({
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<SectionGridFilterCardProps> = ({
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 (
<div
className={`nc-SectionGridFilterCard container ${className}`}
data-nc-id="SectionGridFilterCard"
>
<Heading2 />
<div className={`nc-SectionGridFilterCard container ${className}`} data-nc-id="SectionGridFilterCard">
<div className={`mb-12 lg:mb-16 ${className}`}>
<h2 className="text-4xl font-semibold">{"All Tours"}</h2>
{/* <span className="block text-neutral-500 dark:text-neutral-400 mt-3">
233 stays
<span className="mx-2">·</span>
Aug 12 - 18
<span className="mx-2">·</span>2 Guests
</span> */}
</div>
<div className="mb-8 lg:mb-11">
<TabFilters onChangeCountry={setChecked} data={countries} />
</div>
<MotionConfig
transition={{
x: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
}}
>
<div className="relative flow-root" {...handlers}>
<div className="flow-root overflow-hidden rounded-xl">
<motion.ul
initial={false}
className="relative whitespace-nowrap -mx-2 xl:-mx-4"
>
<AnimatePresence initial={false} custom={direction}>
{countryTours.map((stay, indx) => (
<motion.li
className={`relative inline-block px-2 xl:px-4`}
custom={direction}
initial={{ x: `${(currentIndex - 1) * -100}%` }}
animate={{ x: `${currentIndex * -100}%` }}
variants={variants(200, 1)}
key={indx}
style={{ width: `calc(1/${numberOfItems} * 100%)` }}
>
<StayCard2 key={stay.id} data={stay} />
</motion.li>
))}
</AnimatePresence>
</motion.ul>
</div>
{currentIndex > 0 && (
<PrevBtn
style={{ transform: "translate3d(0, 0, 0)" }}
onClick={() => 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 && (
<NextBtn
style={{ transform: "translate3d(0, 0, 0)" }}
onClick={() => 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]"
/>
)}
</div>
</MotionConfig>
{countryTours.length === 0 && <h2>No tours Available</h2>}
<div className="grid grid-cols-1 gap-6 md:gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{countryTours.length > 0 ? (
countryTours.map((stay) => <StayCard2 key={stay.id} data={stay} />)
) : (
<h2>No tours Available</h2>
)}
</div>
</div>
);
};
export default SectionGridFilterCard;
export default SectionGridFilterCard;

6
src/app/tours/TabFilters.tsx

@ -651,13 +651,13 @@ const renderMoreFilterItem = (
</div>
</div>
<div className="p-4 sm:p-6 flex-shrink-0 bg-neutral-50 dark:bg-neutral-900 dark:border-t dark:border-neutral-800 flex items-center justify-between">
<ButtonThird
<div className=" p-4 sm:p-6 flex-shrink-0 bg-neutral-50 dark:bg-neutral-900 dark:border-t dark:border-neutral-800 flex items-center justify-between">
{/* <ButtonThird
onClick={closeModalMoreFilterMobile}
sizeClass="px-4 py-2 sm:px-5"
>
Clear
</ButtonThird>
</ButtonThird> */}
<ButtonPrimary
onClick={closeModalMoreFilterMobile}
sizeClass="px-4 py-2 sm:px-5"

37
src/app/tours/[slug]/page.tsx

@ -489,12 +489,15 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
<div className="listingSectionSidebar__wrap shadow-xl">
{/* PRICE */}
<div className="flex justify-between">
<span className="text-3xl font-semibold">
{details?.price}
{/* <span className="ml-1 text-base font-normal text-neutral-500 dark:text-neutral-400">
/night
</span> */}
</span>
{+details?.price === +details?.final_price ? (
<span className="text-3xl font-semibold">{details?.price}</span>
) : (
<div >
<span className="mr-2 text-3xl font-semibold">{details?.final_price}</span>
<span className="line-through">{details?.price}</span>{" "}
</div>
)}
<StartRating />
</div>
@ -509,13 +512,13 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
<div className="flex flex-col space-y-4">
<div className="flex justify-between text-neutral-6000 dark:text-neutral-300">
<span>
{details?.price} x {passengers} passengers
{details?.final_price} x {passengers} passengers
</span>
<span>
{" "}
{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()}
</span>
</div>
<div className="flex justify-between text-neutral-6000 dark:text-neutral-300">
@ -554,13 +557,15 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
<header className="rounded-md sm:rounded-xl">
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2">
<div className="col-span-2 row-span-3 sm:row-span-2 relative rounded-md sm:rounded-xl overflow-hidden cursor-pointer">
{details && <Image
fill
className="object-cover rounded-md sm:rounded-xl"
src={details?.images[0]?.image_url.lg}
alt=""
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 50vw"
/>}
{details && (
<Image
fill
className="object-cover rounded-md sm:rounded-xl"
src={details?.images[0]?.image_url.lg}
alt=""
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 50vw"
/>
)}
{/* <div className="aspect-w-4 aspect-h-3 sm:aspect-w-6 sm:aspect-h-5">
<Image
fill

14
src/components/FooterNav.tsx

@ -4,6 +4,8 @@ import {
HeartIcon,
MagnifyingGlassIcon,
UserCircleIcon,
HomeIcon ,
BriefcaseIcon
} from "@heroicons/react/24/outline";
import React, { useEffect, useRef } from "react";
import { PathName } from "@/routers/types";
@ -25,17 +27,17 @@ interface NavItem {
const NAV: NavItem[] = [
{
name: "Explore",
name: "Home",
link: "/",
icon: MagnifyingGlassIcon,
icon: HomeIcon,
},
{
name: "Wishlists",
link: "/account-savelists",
icon: HeartIcon,
name: "My Trips",
link: "/my-trips",
icon: BriefcaseIcon,
},
{
name: "Log in",
name: "Account",
link: "/account",
icon: UserCircleIcon,
},

75
src/components/SearchCard.tsx

@ -0,0 +1,75 @@
import { useEffect, useState } from "react";
import { useToursContext } from "./contexts/tourDetails";
import Image from "next/image";
import axiosInstance from "./api/axios";
import calender from "./../images/Group.svg";
function SearchCard({ data }) {
const { details } = useToursContext();
const [tourDetail, setTourDetail] = useState();
console.log(data.image.image_url.sm);
useEffect(() => {
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 (
<div className="flex hover:bg-neutral-100 p-4 cursor-pointer rounded-3xl">
<div className="h-30 inset-0 w-30 h-30 overflow-hidden rounded-xl relative">
<Image
className="max-h-16 object-cover"
width={100}
height={100}
alt={data.title}
src={data.image.image_url.sm}
/>
</div>
<div className="ml-4">
<h3 className="text-amber-800">{data?.title}</h3>
<div className="flex items-center text-neutral-500 dark:text-neutral-400 text-sm space-x-1.5">
<Image alt="calendar" src={calender} />
<span className="">
{formattedStartDate} - {formattedEndDate}
</span>
</div>
<p className="flex items-center text-neutral-500 dark:text-neutral-400 text-sm space-x-1.5">
({tripDuration} Days)
</p>
</div>
</div>
);
}
}
export default SearchCard;

5
src/components/StayCard2.tsx

@ -38,9 +38,11 @@ const StayCard2: FC<StayCard2Props> = ({
reviewCount,
id,
} = data;
const renderSliderGallery = () => {
return (
<>
<Link
@ -65,6 +67,7 @@ const StayCard2: FC<StayCard2Props> = ({
};
const renderContent = () => {
return (
<div className={size === "default" ? "mt-3 space-y-3" : "mt-2 space-y-2"}>
<div className="space-y-2">

5
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<number>(0);
const [tours, setTours] = useState<Tour[]>([]);
const [countries, setCountries] = useState<Country[]>([]);
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}/`);

5
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<UserProviderProps> = ({ children }) => {
const [form, setForm] = useState<Record<string, any>>({});
const [user, setUser] = useState<Record<string, any>>({});
const [method, setMethod] = useState<any[]>([]);
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<UserProviderProps> = ({ children }) => {
const clerUser = () => {
setUser({});
localStorage.removeItem("user");
router.replace("/")
};
return (

1
src/data/navigation.ts

@ -175,7 +175,6 @@ export const NAVIGATION_DEMO: NavItemType[] = [
href: "/tours",
name: "All Tours",
type: "dropdown",
children: demoChildMenus,
},
{
id: ncNanoId(),

23
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<Record<string, string>>({});
const phoneUtil = PhoneNumberUtil.getInstance();
const validateForm = (form: SignUpForm) => {
let newErrors: Record<string, string> = {};
@ -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;

7
src/images/Group.svg

@ -0,0 +1,7 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0489 4.11768V10.571C12.0489 11.0599 11.6578 11.4999 11.12 11.4999H1.92889C1.44 11.4999 1 11.1088 1 10.571V4.11768H12.0489Z" stroke="#D09460" stroke-width="0.75" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.0489 2.40667V4.11779H1.04889V2.40667C1.04889 1.91778 1.44 1.47778 1.97778 1.47778H11.1689C11.6089 1.52667 12.0489 1.91778 12.0489 2.40667Z" stroke="#D09460" stroke-width="0.75" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.42223 7.78448H3.34668C3.15112 7.78448 3.00446 7.63781 3.00446 7.44225V6.31781C3.00446 6.12225 3.15112 5.97559 3.34668 5.97559H4.47112C4.66668 5.97559 4.81335 6.12225 4.81335 6.31781V7.44225C4.81335 7.63781 4.61779 7.78448 4.42223 7.78448Z" stroke="#D09460" stroke-width="0.75" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.88446 2.26V0.5" stroke="#D09460" stroke-width="0.75" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.16443 0.5V2.26" stroke="#D09460" stroke-width="0.75" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

3
src/images/Vector.svg

@ -0,0 +1,3 @@
<svg width="13" height="9" viewBox="0 0 13 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0489 1.11768V7.57102C12.0489 8.05991 11.6578 8.49991 11.12 8.49991H1.92889C1.44 8.49991 1 8.1088 1 7.57102V1.11768H12.0489Z" stroke="#D09460" stroke-width="0.75" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

9
src/shared/Navigation/NavMobile.tsx

@ -128,14 +128,7 @@ const NavMobile: React.FC<NavMobileProps> = ({
{data.map(_renderItem)}
</ul>
<div className="flex items-center justify-between py-6 px-5">
<a
className="inline-block"
href="https://themeforest.net/item/chisfis-online-booking-nextjs-template/43399526"
target="_blank"
rel="noopener noreferrer"
>
<ButtonPrimary>Get Template</ButtonPrimary>
</a>
<ButtonPrimary href="/custom-trip">Custom Tour</ButtonPrimary>
<LangDropdown
className="flex"

15
yarn.lock

@ -227,6 +227,11 @@
lodash.merge "^4.6.2"
postcss-selector-parser "6.0.10"
"@types/google-libphonenumber@^7.4.30":
version "7.4.30"
resolved "https://registry.npmjs.org/@types/google-libphonenumber/-/google-libphonenumber-7.4.30.tgz"
integrity sha512-Td1X1ayRxePEm6/jPHUBs2tT6TzW1lrVB6ZX7ViPGellyzO/0xMNi+wx5nH6jEitjznq276VGIqjK5qAju0XVw==
"@types/google-map-react@^2.1.7":
version "2.1.7"
resolved "https://registry.npmjs.org/@types/google-map-react/-/google-map-react-2.1.7.tgz"
@ -1439,6 +1444,11 @@ globby@^13.1.3:
merge2 "^1.4.1"
slash "^4.0.0"
google-libphonenumber@^3.2.38:
version "3.2.38"
resolved "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.38.tgz"
integrity sha512-t/K0dsVmA0gMMVLJgcMeB9g1Ar4ANVWfkY+AJGSdfyJ2Ay7Bu8ceLYpUlC6FZSilZgaF1qbkM9tZydGBEBHqAg==
google-map-react@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/google-map-react/-/google-map-react-2.2.1.tgz"
@ -1874,6 +1884,11 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
libphonenumber-js@^1.11.9:
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==
lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"

Loading…
Cancel
Save