Browse Source

added : My Trips , custom tour (not complete), tour suggestion

main
sina_sajjadi 2 months ago
parent
commit
b0ecca0b62
  1. 2
      src/app/(account-pages)/(components)/Nav.tsx
  2. 88
      src/app/(account-pages)/account-savelists/page.tsx
  3. 34
      src/app/(account-pages)/account/page.tsx
  4. 69
      src/app/(account-pages)/my-trips/page.tsx
  5. 111
      src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx
  6. 128
      src/app/add-listing/[[...stepIndex]]/page.tsx
  7. 175
      src/app/custom-trip/page.tsx
  8. 3
      src/app/page.tsx
  9. 4
      src/app/signup/methodes/page.tsx
  10. 10
      src/app/signup/page.tsx
  11. 331
      src/app/tours/[slug]/page.tsx
  12. 51
      src/components/CardCategory1.tsx
  13. 2
      src/components/StayCard2.tsx
  14. 252
      src/components/TourSuggestion.tsx
  15. 11
      src/hooks/FormValidation.ts
  16. 26
      src/hooks/passengerValidation.ts
  17. 2
      src/shared/Navigation/Navigation.tsx
  18. 74
      src/shared/popUp.tsx

2
src/app/(account-pages)/(components)/Nav.tsx

@ -10,7 +10,7 @@ export const Nav = () => {
const listNav: Route[] = [ const listNav: Route[] = [
"/account", "/account",
"/account-savelists",
"/my-trips",
"/account-password", "/account-password",
"/passengers-list", "/passengers-list",
]; ];

88
src/app/(account-pages)/account-savelists/page.tsx

@ -1,88 +0,0 @@
"use client";
import { Tab } from "@headlessui/react";
import CarCard from "@/components/CarCard";
import ExperiencesCard from "@/components/ExperiencesCard";
import StayCard from "@/components/StayCard";
import {
DEMO_CAR_LISTINGS,
DEMO_EXPERIENCES_LISTINGS,
DEMO_STAY_LISTINGS,
} from "@/data/listings";
import React, { Fragment, useState } from "react";
import ButtonSecondary from "@/shared/ButtonSecondary";
const AccountSavelists = () => {
let [categories] = useState(["Stays", "Experiences", "Cars"]);
const renderSection1 = () => {
return (
<div className="space-y-6 sm:space-y-8">
<div>
<h2 className="text-3xl font-semibold">Save lists</h2>
</div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div>
<Tab.Group>
<Tab.List className="flex space-x-1 overflow-x-auto">
{categories.map((item) => (
<Tab key={item} as={Fragment}>
{({ selected }) => (
<button
className={`flex-shrink-0 block !leading-none font-medium px-5 py-2.5 text-sm sm:text-base sm:px-6 sm:py-3 capitalize rounded-full focus:outline-none ${
selected
? "bg-secondary-900 text-secondary-50 "
: "text-neutral-500 dark:text-neutral-400 dark:hover:text-neutral-100 hover:text-neutral-900 hover:bg-neutral-100 dark:hover:bg-neutral-800"
} `}
>
{item}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel className="mt-8">
<div className="grid grid-cols-1 gap-6 md:gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{DEMO_STAY_LISTINGS.filter((_, i) => i < 8).map((stay) => (
<StayCard key={stay.id} data={stay} />
))}
</div>
<div className="flex mt-11 justify-center items-center">
<ButtonSecondary>Show me more</ButtonSecondary>
</div>
</Tab.Panel>
<Tab.Panel className="mt-8">
<div className="grid grid-cols-1 gap-6 md:gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{DEMO_EXPERIENCES_LISTINGS.filter((_, i) => i < 8).map(
(stay) => (
<ExperiencesCard key={stay.id} data={stay} />
)
)}
</div>
<div className="flex mt-11 justify-center items-center">
<ButtonSecondary>Show me more</ButtonSecondary>
</div>
</Tab.Panel>
<Tab.Panel className="mt-8">
<div className="grid grid-cols-1 gap-6 md:gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{DEMO_CAR_LISTINGS.filter((_, i) => i < 8).map((stay) => (
<CarCard key={stay.id} data={stay} />
))}
</div>
<div className="flex mt-11 justify-center items-center">
<ButtonSecondary>Show me more</ButtonSecondary>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
);
};
return renderSection1();
};
export default AccountSavelists;

34
src/app/(account-pages)/account/page.tsx

@ -15,7 +15,7 @@ export interface AccountPageProps {}
const AccountPage = () => { const AccountPage = () => {
const router = useRouter(); const router = useRouter();
const User = JSON.parse(localStorage.getItem("user"))
const User = JSON.parse(localStorage.getItem("user"));
let user = JSON.parse(localStorage.getItem("user")); let user = JSON.parse(localStorage.getItem("user"));
if (!user) { if (!user) {
return router.replace("/"); return router.replace("/");
@ -29,12 +29,11 @@ const AccountPage = () => {
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
const [imageURL, setImageURL] = useState<string | null>(null); const [imageURL, setImageURL] = useState<string | null>(null);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState({});
const deleteHandler = async () => { const deleteHandler = async () => {
setStatus(false);
setError(""); setError("");
setLoading({delete : true})
try { try {
const response = await axiosInstance.delete( const response = await axiosInstance.delete(
`/api/account/profile/delete/`, `/api/account/profile/delete/`,
@ -46,12 +45,19 @@ const AccountPage = () => {
); );
if (response.status === 204) { if (response.status === 204) {
localStorage.removeItem("user"); localStorage.removeItem("user");
setStatus(false);
router.replace("/"); router.replace("/");
} else { } else {
setError("Something went wrong"); setError("Something went wrong");
} }
} catch (error) { } catch (error) {
setError(error.message); setError(error.message);
} finally{
setLoading({delete : false})
} }
}; };
const signOutHandler = () => { const signOutHandler = () => {
@ -63,6 +69,8 @@ const AccountPage = () => {
const changeHandler = async () => { const changeHandler = async () => {
setError(""); setError("");
setLoading({change : true})
const formData = new FormData(); const formData = new FormData();
formData.append("fullname", name); formData.append("fullname", name);
formData.append("email", email); formData.append("email", email);
@ -83,17 +91,25 @@ const AccountPage = () => {
} }
); );
if (response.status === 200) { if (response.status === 200) {
console.log(response);
user.avatar = response.data.avatar; user.avatar = response.data.avatar;
user.email = response.data.email; user.email = response.data.email;
user.fullname = response.data.fullname; user.fullname = response.data.fullname;
user.phone_number = response.data.phone_number; user.phone_number = response.data.phone_number;
localStorage.setItem("user", JSON.stringify(user)); localStorage.setItem("user", JSON.stringify(user));
} else { } else {
setError("Something went wrong"); setError("Something went wrong");
} }
} catch (error) { } catch (error) {
setError(error.message); setError(error.message);
} finally{
setLoading({change : false})
} }
}; };
@ -103,13 +119,10 @@ const AccountPage = () => {
const uploadedFile = await getImageURL(file); const uploadedFile = await getImageURL(file);
setImageURL(uploadedFile.url); setImageURL(uploadedFile.url);
};
}
setImage(file); setImage(file);
}
};
return ( return (
<div className="space-y-6 sm:space-y-8"> <div className="space-y-6 sm:space-y-8">
@ -188,7 +201,7 @@ const AccountPage = () => {
</div> </div>
{error && <p className=" text-red-500 text-xs">{error}</p>} {error && <p className=" text-red-500 text-xs">{error}</p>}
<div className=" flex flex-col pt-2 sm:flex-row "> <div className=" flex flex-col pt-2 sm:flex-row ">
<ButtonPrimary className="mt-8" onClick={changeHandler}>
<ButtonPrimary className="mt-8" loading={loading.change} onClick={changeHandler}>
Update info Update info
</ButtonPrimary> </ButtonPrimary>
<ButtonPrimary className="mt-8 sm:ml-8 " onClick={signOutHandler}> <ButtonPrimary className="mt-8 sm:ml-8 " onClick={signOutHandler}>
@ -196,6 +209,7 @@ const AccountPage = () => {
Sign out Sign out
</ButtonPrimary> </ButtonPrimary>
<ButtonSecondary <ButtonSecondary
loading={loading.delete}
onClick={deleteHandler} onClick={deleteHandler}
className="opacity-60 mt-8 hover:bg-red-500 hover:text-white hover:opacity-100 sm:ml-8" className="opacity-60 mt-8 hover:bg-red-500 hover:text-white hover:opacity-100 sm:ml-8"
> >

69
src/app/(account-pages)/my-trips/page.tsx

@ -0,0 +1,69 @@
"use client";
import { Tab } from "@headlessui/react";
import CarCard from "@/components/CarCard";
import ExperiencesCard from "@/components/ExperiencesCard";
import StayCard from "@/components/StayCard";
import {
DEMO_CAR_LISTINGS,
DEMO_EXPERIENCES_LISTINGS,
DEMO_STAY_LISTINGS,
} from "@/data/listings";
import React, { Fragment, useEffect, useState } from "react";
import ButtonSecondary from "@/shared/ButtonSecondary";
import axiosInstance from "@/components/api/axios";
import StayCard2 from "@/components/StayCard2";
const MyTrips = () => {
const user = JSON.parse(localStorage.getItem("user"));
let [categories] = useState(["Stays", "Experiences", "Cars"]);
let [tours , setTours] = useState([]);
useEffect(() => {
axiosInstance
.get("/api/tours/orders/", {
headers: {
Authorization: `token ${user.token}`,
},
})
.then((response) => {
setTours(response.data.results);
console.log(response);
})
.catch((error) => {
console.error(error);
});
}, []);
const renderSection1 = () => {
return (
<div className="space-y-6 sm:space-y-8">
<div>
<h2 className="text-3xl font-semibold">Save lists</h2>
</div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div>
<Tab.Group>
<Tab.Panels>
<Tab.Panel className="mt-8">
<div className="grid grid-cols-1 gap-6 md:gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{tours.length ? (tours?.filter((_, i) => i < 8).map((stay) => (
<StayCard2 key={stay.id} data={stay} />
))) : (<h1 className="text-2xl"> You have no trips </h1>)}
</div>
<div className="flex mt-11 justify-center items-center">
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
);
};
return renderSection1();
};
export default MyTrips;

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

@ -1,71 +1,147 @@
import React, { FC } from "react";
import React, { FC, useEffect, useState } from "react";
import Input from "@/shared/Input"; import Input from "@/shared/Input";
import Select from "@/shared/Select"; import Select from "@/shared/Select";
import FormItem from "../FormItem"; import FormItem from "../FormItem";
import { IoPersonAddOutline } from "react-icons/io5"; import { IoPersonAddOutline } from "react-icons/io5";
import getImageURL from "@/components/api/getImageURL"; import getImageURL from "@/components/api/getImageURL";
import ConfirmModal from "../../../shared/popUp";
import axiosInstance from "@/components/api/axios";
import PassengerTable from "@/app/(account-pages)/passengers-list/PassengerTable";
export interface PageAddListing1Props {}
export interface PageAddListing1Props {
Passenger: any;
setNewPassenger: (passenger: any) => void;
errors: any;
passengerID: any[];
setPassengerID: (ids: any[]) => void;
selectedPassenger: any; // Add prop for selectedPassenger
setSelectedPassenger: (passenger: any) => void; // Add prop for setting selectedPassenger
}
const PageAddListing1: FC<PageAddListing1Props> = ({ const PageAddListing1: FC<PageAddListing1Props> = ({
Passenger, Passenger,
setNewPassenger, setNewPassenger,
errors,
passengerID,
setPassengerID,
selectedPassenger,
setSelectedPassenger, // Receive the function to reset selectedPassenger
}) => { }) => {
const { fullName, date, number, passport } = Passenger; const { fullName, date, number, passport } = Passenger;
const user = JSON.parse(localStorage.getItem("user"));
const [savedPassengers, setSavedPassengers] = useState([]);
useEffect(() => {
axiosInstance
.get("/api/account/passengers/", {
headers: {
Authorization: `token ${user.token}`,
},
})
.then((response) => {
setSavedPassengers(response.data.results);
console.log(response);
})
.catch((error) => {
console.error(error);
});
}, []);
console.log(savedPassengers);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
const uploadedFile = await getImageURL(file); const uploadedFile = await getImageURL(file);
setNewPassenger((prev) => ({ ...prev, image: uploadedFile.url }));
setNewPassenger((prev) => ({ ...prev, passport_image: uploadedFile.url }));
} }
};
const AddPassengerId = (passenger) => {
setSelectedPassenger(passenger);
console.log(passenger.id);
setPassengerID((prev) => {
if (!Array.isArray(prev)) {
prev = [];
}
const exists = prev.some((p) => p.passenger_id === passenger.id);
if (exists) {
return prev;
}
return [...prev, { passenger_id: passenger.id }];
});
}; };
return ( return (
!selectedPassenger ? (
<> <>
<h2 className="text-2xl font-semibold">Choosing listing categories</h2> <h2 className="text-2xl font-semibold">Choosing listing categories</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> <div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
{/* FORM */} {/* FORM */}
<div className="space-y-8">
{/* ITEM */}
<ConfirmModal
lable={
<div className="flex items-center space-x-2 text-orange-500 cursor-pointer hover:text-orange-600"> <div className="flex items-center space-x-2 text-orange-500 cursor-pointer hover:text-orange-600">
<IoPersonAddOutline className="text-xl" /> {/* Adjust icon size */} <IoPersonAddOutline className="text-xl" /> {/* Adjust icon size */}
<p className="text-sm font-medium">Add From passengers List</p>
<p className="text-sm font-medium">Add from passenger list</p>
</div> </div>
}
>
{(closeModal) => (
<ul className="space-y-2 p-3">
{savedPassengers?.map((item, index) => (
<li onClick={() => { AddPassengerId(item); closeModal(); }}
key={index}
className={`cursor-pointer rounded-md p-3 hover:bg-bronze bg-white border-2 dark:bg-gray-700 text-neutral-900 dark:text-white`}
>
<h3 className="text-lg font-semibold">{item.fullname}</h3>
</li>
))}
</ul>
)}
</ConfirmModal>
<div className="space-y-8">
{/* ITEM */}
<FormItem label="Full Name" desc=""> <FormItem label="Full Name" desc="">
<Input <Input
onChange={(e) => onChange={(e) =>
setNewPassenger((prev) => ({ ...prev, fullName: e.target.value }))
setNewPassenger((prev) => ({ ...prev, fullname: e.target.value }))
} }
value={fullName} value={fullName}
placeholder="Full Name" placeholder="Full Name"
/> />
{errors.fullName && (
<p className="text-xs text-red-500"> {errors.fullName} </p>
)}
</FormItem> </FormItem>
<FormItem label="Passport Number" desc=""> <FormItem label="Passport Number" desc="">
<Input <Input
type="number" type="number"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
onChange={(e) => onChange={(e) =>
setNewPassenger((prev) => ({ ...prev, passport: e.target.value }))
setNewPassenger((prev) => ({ ...prev, passport_number: e.target.value }))
} }
value={passport} value={passport}
placeholder="Passport Number" placeholder="Passport Number"
/> />
{errors.passport && (
<p className="text-xs text-red-500"> {errors.passport} </p>
)}
</FormItem> </FormItem>
<FormItem label="Date of Birth" desc=""> <FormItem label="Date of Birth" desc="">
<Input <Input
type="date" type="date"
value={date} value={date}
onChange={(e) => onChange={(e) =>
setNewPassenger((prev) => ({ ...prev, date: e.target.value }))
setNewPassenger((prev) => ({ ...prev, birthdate: e.target.value }))
} }
placeholder="Date of Birth" placeholder="Date of Birth"
/> />
{errors.date && (
<p className="text-xs text-red-500"> {errors.date} </p>
)}
</FormItem> </FormItem>
<FormItem label="Phone Number" desc=""> <FormItem label="Phone Number" desc="">
<Input <Input
@ -73,10 +149,13 @@ const PageAddListing1: FC<PageAddListing1Props> = ({
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
value={number} value={number}
onChange={(e) => onChange={(e) =>
setNewPassenger((prev) => ({ ...prev, number: e.target.value }))
setNewPassenger((prev) => ({ ...prev, phone_number: e.target.value }))
} }
placeholder="Phone Number" placeholder="Phone Number"
/> />
{errors.number && (
<p className="text-xs text-red-500"> {errors.number} </p>
)}
</FormItem> </FormItem>
<FormItem label="Upload Passport image Here" desc=""> <FormItem label="Upload Passport image Here" desc="">
<Input <Input
@ -84,9 +163,15 @@ const PageAddListing1: FC<PageAddListing1Props> = ({
onChange={handleFileChange} onChange={handleFileChange}
placeholder="Passport" placeholder="Passport"
/> />
{errors.image && (
<p className="text-xs text-red-500"> {errors.image} </p>
)}
</FormItem> </FormItem>
</div> </div>
</> </>
) : (
<p className="text-xs text-red-500"> {selectedPassenger.fullname}</p>
)
); );
}; };

128
src/app/add-listing/[[...stepIndex]]/page.tsx

@ -1,12 +1,14 @@
"use client"
"use client";
import React, { useState } from "react";
import React, { useContext, useState, useEffect } from "react";
import { FC } from "react"; import { FC } from "react";
import ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonPrimary from "@/shared/ButtonPrimary";
import ButtonSecondary from "@/shared/ButtonSecondary"; import ButtonSecondary from "@/shared/ButtonSecondary";
import { Route } from "@/routers/types";
import PageAddListing1 from "./PageAddListing1"; import PageAddListing1 from "./PageAddListing1";
import { setHttpClientAndAgentOptions } from "next/dist/server/config";
import validatePassenger from "@/hooks/passengerValidation";
import { Context } from "@/components/contexts/tourDetails";
import { useRouter } from "next/navigation";
import axiosInstance from "@/components/api/axios";
export interface CommonLayoutProps { export interface CommonLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -16,48 +18,114 @@ export interface CommonLayoutProps {
} }
const CommonLayout: FC<CommonLayoutProps> = ({ params }) => { const CommonLayout: FC<CommonLayoutProps> = ({ params }) => {
const router = useRouter();
const user = JSON.parse(localStorage.getItem("user"));
const [passengers , setPassengers] =useState([])
const [index, setIndex] = useState(1);
const [passengers, setPassengers] = useState([]);
const [errors, setErrors] = useState({});
const [newPassenger, setNewPassenger] = useState({ const [newPassenger, setNewPassenger] = useState({
fullName :"",
date : "" ,
number : "",
passport : "",
image : ""
})
fullname: "",
passport_number: "",
birthdate: "",
phone_number: "",
passport_image: "",
});
const [passengerID, setPassengerID] = useState([]);
const [selectedPassenger, setSelectedPassenger] = useState(null);
const tourID = params.stepIndex[0];
const totalPassengers = useContext(Context).passengers;
const nextHref = () => setIndex((prev) => prev + 1);
const backtHref = () => (index > 1 ? setIndex((prev) => prev - 1) : index);
const nextBtnText = index > totalPassengers ? "Save Passengers" : "Continue";
const sendPassengers = async () => {
try {
const response = await axiosInstance.post(
"/api/tours/orders/purchase/",
{
tour_id: tourID,
passengers: {
passenger_ids: passengerID,
new_passengers: passengers,
},
},
{
headers: {
Authorization: `token ${user.token}`,
'Content-Type': 'application/json',
},
}
);
console.log(response);
router.replace("/my-trips");
} catch (error) {
console.error("Error submitting passengers:", error);
}
};
useEffect(() => {
if (index > totalPassengers && passengers.length === totalPassengers) {
sendPassengers();
}
}, [passengers, index, totalPassengers]);
const nextHandler = () => { const nextHandler = () => {
setPassengers((prev) => [...prev, newPassenger]);
const validationErrors = validatePassenger(newPassenger);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
console.log("Validation errors:", validationErrors);
return;
} }
const index = Number(params.stepIndex) || 1;
const nextHref = (
index < 10 ? `/add-listing/${index + 1}` : `/add-listing/${1}`
) as Route;
const backtHref = (
index > 1 ? `/add-listing/${index - 1}` : `/add-listing/${1}`
) as Route;
const nextBtnText = index > 9 ? "Publish listing" : "Continue";
setSelectedPassenger(null);
setPassengers((prevPassengers) => [...prevPassengers, newPassenger]);
setNewPassenger({
fullname: "",
passport_number: "",
birthdate: "",
phone_number: "",
passport_image: "",
});
setErrors({});
nextHref();
};
return ( return (
<div
className={`nc-PageAddListing1 px-4 max-w-3xl mx-auto pb-24 pt-14 sm:py-24 lg:pb-32`}
>
<div className={`nc-PageAddListing1 px-4 max-w-3xl mx-auto pb-24 pt-14 sm:py-24 lg:pb-32`}>
<div className="space-y-11"> <div className="space-y-11">
<div> <div>
<span className="text-4xl font-semibold">{index}</span>{" "} <span className="text-4xl font-semibold">{index}</span>{" "}
<span className="text-lg text-neutral-500 dark:text-neutral-400"> <span className="text-lg text-neutral-500 dark:text-neutral-400">
/ 10
/ {totalPassengers}
</span> </span>
</div> </div>
{/* --------------------- */}
<div className="listingSection__wrap "><PageAddListing1 Passenger={newPassenger} setNewPassenger ={setNewPassenger} /></div>
{/* Passenger Form */}
<div className="listingSection__wrap">
<PageAddListing1
Passenger={newPassenger}
setNewPassenger={setNewPassenger}
errors={errors}
setPassengerID={setPassengerID}
passengerID={passengerID}
selectedPassenger={selectedPassenger}
setSelectedPassenger={setSelectedPassenger}
/>
</div>
{/* --------------------- */}
<div className="flex justify-end space-x-5"> <div className="flex justify-end space-x-5">
{index > 1 && <ButtonSecondary href={backtHref}>Go back</ButtonSecondary>}
<ButtonPrimary onClick={nextHandler} href={nextHref}>
{nextBtnText || "Continue"}
{/* {index > 1 && (
<ButtonSecondary onClick={backtHref}>Go back</ButtonSecondary>
)} */}
<ButtonPrimary onClick={nextHandler}>
{nextBtnText}
</ButtonPrimary> </ButtonPrimary>
</div> </div>
</div> </div>

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

@ -0,0 +1,175 @@
"use client";
import React, { useEffect, useState } from "react";
import { FC } from "react";
import ButtonPrimary from "@/shared/ButtonPrimary";
import FormItem from "../add-listing/FormItem";
import axiosInstance from "@/components/api/axios";
import Select from "@/shared/Select";
import Input from "@/shared/Input";
interface City {
name: string;
slug: string;
}
interface Country {
name: string;
city: City[];
}
interface CommonLayoutProps {}
const CommonLayout: FC<CommonLayoutProps> = () => {
const [countries, setCountries] = useState<Country[]>([]);
const [startCity, setStartCity] = useState<string>("");
const [endCity, setEndCity] = useState<string>("");
const [transport, setTransport] = useState<any[]>([]);
const [hotel, setHotel] = useState<any[]>([]);
const [selectedHotel, setSelectedHotel] = useState<string>("");
const [selectedTransport, setSelectedTransport] = useState<string>("");
const [destinations, setDestinations] = useState<{ endCity: string, transport: string, hotel: string }[]>([]);
useEffect(() => {
axiosInstance.get("/api/cityguide/countries/?service=custom_trip")
.then((response) => setCountries(response.data.results))
.catch((error) => console.error(error));
}, []);
useEffect(() => {
console.log(destinations[destinations.length-1]?.endCity);
if (destinations[destinations.length-1]?.endCity) {
axiosInstance.get(`/api/trip/custom/transport/?from_city=${startCity}&to_city=${destinations[destinations.length-1]?.endCity}`)
.then((response) => { console.log(response)
setTransport(response.data)})
.catch((error) => console.error(error));
axiosInstance.get(`/api/trip/hotels/${destinations[destinations.length-1]?.endCity}/`)
.then((response) =>{ console.log(response)
setHotel(response.data.results)})
.catch((error) => console.error(error));
}
}, [destinations]);
const addDestination = () => {
setDestinations([...destinations, { endCity: "", transport: "", hotel: "" }]);
};
const handleDestinationChange = (index: number, field: string, value: string) => {
const updatedDestinations = [...destinations];
updatedDestinations[index] = { ...updatedDestinations[index], [field]: value };
setDestinations(updatedDestinations);
};
console.log(destinations);
return (
<div className="nc-PageAddListing1 px-4 max-w-3xl mx-auto pb-24 pt-14 sm:py-24 lg:pb-32">
<div className="space-y-11">
<form>
<div className="listingSection__wrap">
<h2 className="text-2xl font-semibold">Custom Trip</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<h3 className="text-[#BD3F3F] font-medium">Guide</h3>
<p>
First, write the origin of your departure, then choose the first
destination of your trip, the number of nights of stay and the
means of travel, then choose your travel destinations if you
wish.
</p>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="space-y-8">
<p className="text-bronze text-xs">Begin your trip</p>
<FormItem label="" desc="">
<Select value={startCity} onChange={(e) => setStartCity(e.target.value)}>
<option value="">Select City</option>
{countries.flatMap((country) =>
country.city.map((city) => (
<option key={city.name} value={city.slug}>
{`${country.name} - ${city.name}`}
</option>
))
)}
</Select>
</FormItem>
<div className="flex w-[100%]">
<FormItem className="w-[50%]" label="Start Date">
<Input type="date" />
</FormItem>
<FormItem className="w-[50%] ml-4" label="Number Of Passengers">
<Input type="number" />
</FormItem>
</div>
</div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
{destinations.map((destination, index) => (
<div key={index} className="space-y-8">
<p className="text-bronze text-xs">Destination {index + 1}</p>
<FormItem label="" desc="">
<Select
value={destination.endCity}
onChange={(e) => handleDestinationChange(index, 'endCity', e.target.value)}
>
<option value="">Select City</option>
{countries.flatMap((country) =>
country.city.map((city) => (
<option key={city.name} value={city.slug}>
{`${country.name} - ${city.name}`}
</option>
))
)}
</Select>
</FormItem>
<FormItem label="Transportation" desc="">
<Select
value={destination.transport}
onChange={(e) => handleDestinationChange(index, 'transport', e.target.value)}
>
<option value="">Select Transport</option>
{transport?.map((item) =>
<option key={item.transportaion.id} value={item.transportaion.name}>
{item.transportaion.name}
</option>
)}
</Select>
</FormItem>
<FormItem label="Hotel" desc="">
<Select
value={destination.hotel}
onChange={(e) => handleDestinationChange(index, 'hotel', e.target.value)}
>
<option value="">Select Hotel</option>
{hotel?.map((hotelItem) =>
<option key={hotelItem.name} value={hotelItem.name}>
{hotelItem.name}
</option>
)}
</Select>
</FormItem>
<div className="flex w-[100%]">
<FormItem className="w-[50%]" label="Duration">
<Input type="number" />
</FormItem>
<FormItem className="w-[50%] ml-4" label="Finish date">
<Input type="number" />
</FormItem>
</div>
</div>
))}
</div>
<div className="flex mt-4 justify-end space-x-5">
<ButtonPrimary onClick={addDestination} type="button">Add Destination</ButtonPrimary>
<ButtonPrimary type="button">Continue</ButtonPrimary>
</div>
</form>
</div>
</div>
);
};
export default CommonLayout;

3
src/app/page.tsx

@ -15,6 +15,7 @@ import SectionClientSay from "@/components/SectionClientSay";
import SectionCustomTour from "@/components/SectionCustomTour"; import SectionCustomTour from "@/components/SectionCustomTour";
import axios from "axios"; import axios from "axios";
import axiosInstance from "@/components/api/axios"; import axiosInstance from "@/components/api/axios";
import TourSuggestion from "@/components/TourSuggestion";
const DEMO_CATS: TaxonomyType[] = [ const DEMO_CATS: TaxonomyType[] = [
{ {
@ -175,7 +176,7 @@ function PageHome() {
<div className="relative py-16"> <div className="relative py-16">
<BackgroundSection className="bg-orange-50 dark:bg-black/20" /> <BackgroundSection className="bg-orange-50 dark:bg-black/20" />
<SectionSliderNewCategories
<TourSuggestion
categories={DEMO_CATS_2} categories={DEMO_CATS_2}
categoryCardType="card4" categoryCardType="card4"
itemPerRow={4} itemPerRow={4}

4
src/app/signup/methodes/page.tsx

@ -10,8 +10,8 @@ import { MdOutlineTextsms } from "react-icons/md";
function SelectMethods() { function SelectMethods() {
const router = useRouter(); const router = useRouter();
let user = JSON.parse(localStorage.getItem("user"));
if (user) {
let User = JSON.parse(localStorage.getItem("user"));
if (User) {
return router.replace("/"); return router.replace("/");
} }
const { method, form } = useContext(user); const { method, form } = useContext(user);

10
src/app/signup/page.tsx

@ -29,6 +29,8 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [failed , setFailed ] =useState("")
const { errors, validateForm } = useFormValidation(); const { errors, validateForm } = useFormValidation();
const countryCodeHandler = (e) => { const countryCodeHandler = (e) => {
@ -51,17 +53,20 @@ const PageSignUp: FC<PageSignUpProps> = () => {
setForm(form); setForm(form);
try { try {
const response = await axiosInstance.get( const response = await axiosInstance.get(
`/api/account/verification/?range_phone=${countryCode}&phone_number=${phoneNumber}`
`/api/account/verfication/?range_phone=${countryCode}&phone_number=${phoneNumber}`
); );
setMethod(response.data.verification_method); setMethod(response.data.verification_method);
router.push("/signup/methods");
router.push("/signup/methodes");
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
setFailed(error.message)
} finally { } finally {
setLoading(false); setLoading(false);
} }
} else { } else {
console.log("Form has errors:", errors); console.log("Form has errors:", errors);
} }
}; };
@ -137,6 +142,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
<p className="text-xs text-red-600">{errors.confirmPassword}</p> <p className="text-xs text-red-600">{errors.confirmPassword}</p>
)} )}
</label> </label>
{failed && (<p><p className="text-xs text-red-600">{failed}</p></p>)}
<ButtonPrimary <ButtonPrimary
loading={loading} loading={loading}
onClick={submitHandler} onClick={submitHandler}

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

@ -21,6 +21,10 @@ import StayDatesRangeInput from "./StayDatesRangeInput";
import GuestsInput from "./GuestsInput"; import GuestsInput from "./GuestsInput";
import { Route } from "next"; import { Route } from "next";
import { Context } from "@/components/contexts/tourDetails"; import { Context } from "@/components/contexts/tourDetails";
import axiosInstance from "@/components/api/axios";
import { FaCheckCircle } from "react-icons/fa";
import ConfirmModal from "@/shared/popUp";
import { IoPersonAddOutline } from "react-icons/io5";
export interface ListingStayDetailPageProps {} export interface ListingStayDetailPageProps {}
@ -28,14 +32,26 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
// //
const { getTourData, details, passengers } = useContext(Context); const { getTourData, details, passengers } = useContext(Context);
const [iteneries, setIteneries] = useState();
const r = /-?(\d+)$/; const r = /-?(\d+)$/;
const id: number = useParams().slug.match(r)[1]; const id: number = useParams().slug.match(r)[1];
const totalIteneries = iteneries?.length;
console.log(details);
useEffect(() => { useEffect(() => {
getTourData(id); getTourData(id);
axiosInstance
.get(`https://aqila.nwhco.ir/api/tours/${id}/itinerary/`)
.then((response) => {
console.log(response.data.results);
setIteneries(response.data.results);
})
.catch((error) => {
console.error("Error fetching data:", error);
});
}, []); }, []);
let [isOpenModalAmenities, setIsOpenModalAmenities] = useState(false); let [isOpenModalAmenities, setIsOpenModalAmenities] = useState(false);
const thisPathname = usePathname(); const thisPathname = usePathname();
@ -125,22 +141,10 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
const renderSection2 = () => { const renderSection2 = () => {
return ( return (
<div className="listingSection__wrap"> <div className="listingSection__wrap">
<h2 className="text-2xl font-semibold">Stay information</h2>
<h2 className="text-3xl font-semibold">{details?.title}</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> <div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="text-neutral-6000 dark:text-neutral-300"> <div className="text-neutral-6000 dark:text-neutral-300">
<span>{details?.description}</span> <span>{details?.description}</span>
<br />
<br />
<span>
There is a private bathroom with bidet in all units, along with a
hairdryer and free toiletries.
</span>
<br /> <br />
<span>
The Symphony 9 Tam Coc offers a terrace. Both a bicycle rental
service and a car rental service are available at the accommodation,
while cycling can be enjoyed nearby.
</span>
</div> </div>
</div> </div>
); );
@ -150,142 +154,133 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
return ( return (
<div className="listingSection__wrap"> <div className="listingSection__wrap">
<div> <div>
<h2 className="text-2xl font-semibold">Amenities </h2>
<span className="block mt-2 text-neutral-500 dark:text-neutral-400">
{` About the property's amenities and services`}
</span>
<h2 className="text-2xl font-semibold">Tour Features </h2>
</div> </div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> <div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
{/* 6 */} {/* 6 */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6 text-sm text-neutral-700 dark:text-neutral-300 ">
{details && details.tour_features?.map((item) => (
<div className="grid gap-6 text-sm text-neutral-700 dark:text-neutral-300 ">
{details &&
details.tour_features?.map((item) => (
<div key={item.id} className="flex items-center space-x-3"> <div key={item.id} className="flex items-center space-x-3">
<span className=" ">{item.title}</span>
<FaCheckCircle className="w-5 text-green-500" />
<span>{item.title}</span>
</div> </div>
))} ))}
</div> </div>
<div className="w-14 border-b border-neutral-200"></div>
{/* <div className="w-14 border-b border-neutral-200"></div>
<div> <div>
<ButtonSecondary onClick={openModalAmenities}> <ButtonSecondary onClick={openModalAmenities}>
View more 20 amenities View more 20 amenities
</ButtonSecondary> </ButtonSecondary>
</div> </div>
{renderMotalAmenities()}
{renderMotalAmenities()} */}
</div> </div>
); );
}; };
const renderMotalAmenities = () => {
return (
<Transition appear show={isOpenModalAmenities} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-50 overflow-y-auto"
onClose={closeModalAmenities}
>
<div className="min-h-screen px-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="inline-block py-8 h-screen w-full max-w-4xl">
<div className="inline-flex pb-2 flex-col w-full text-left align-middle transition-all transform overflow-hidden rounded-2xl bg-white dark:bg-neutral-900 dark:border dark:border-neutral-700 dark:text-neutral-100 shadow-xl h-full">
<div className="relative flex-shrink-0 px-6 py-4 border-b border-neutral-200 dark:border-neutral-800 text-center">
<h3
className="text-lg font-medium leading-6 text-gray-900"
id="headlessui-dialog-title-70"
>
Amenities
</h3>
<span className="absolute left-3 top-3">
<ButtonClose onClick={closeModalAmenities} />
</span>
</div>
<div className="px-8 overflow-auto text-neutral-700 dark:text-neutral-300 divide-y divide-neutral-200">
{Amenities_demos.filter((_, i) => i < 1212).map((item) => (
<div
key={item.name}
className="flex items-center py-2.5 sm:py-4 lg:py-5 space-x-5 lg:space-x-8"
>
<i
className={`text-4xl text-neutral-6000 las ${item.icon}`}
></i>
<span>{item.name}</span>
</div>
))}
</div>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
};
// const renderMotalAmenities = () => {
// return (
// <Transition appear show={isOpenModalAmenities} as={Fragment}>
// <Dialog
// as="div"
// className="fixed inset-0 z-50 overflow-y-auto"
// onClose={closeModalAmenities}
// >
// <div className="min-h-screen px-4 text-center">
// <Transition.Child
// as={Fragment}
// enter="ease-out duration-300"
// enterFrom="opacity-0"
// enterTo="opacity-100"
// leave="ease-in duration-200"
// leaveFrom="opacity-100"
// leaveTo="opacity-0"
// >
// <Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-40" />
// </Transition.Child>
// {/* This element is to trick the browser into centering the modal contents. */}
// <span
// className="inline-block h-screen align-middle"
// aria-hidden="true"
// >
// &#8203;
// </span>
// <Transition.Child
// as={Fragment}
// enter="ease-out duration-300"
// enterFrom="opacity-0 scale-95"
// enterTo="opacity-100 scale-100"
// leave="ease-in duration-200"
// leaveFrom="opacity-100 scale-100"
// leaveTo="opacity-0 scale-95"
// >
// <div className="inline-block py-8 h-screen w-full max-w-4xl">
// <div className="inline-flex pb-2 flex-col w-full text-left align-middle transition-all transform overflow-hidden rounded-2xl bg-white dark:bg-neutral-900 dark:border dark:border-neutral-700 dark:text-neutral-100 shadow-xl h-full">
// <div className="relative flex-shrink-0 px-6 py-4 border-b border-neutral-200 dark:border-neutral-800 text-center">
// <h3
// className="text-lg font-medium leading-6 text-gray-900"
// id="headlessui-dialog-title-70"
// >
// Amenities
// </h3>
// <span className="absolute left-3 top-3">
// <ButtonClose onClick={closeModalAmenities} />
// </span>
// </div>
// <div className="px-8 overflow-auto text-neutral-700 dark:text-neutral-300 divide-y divide-neutral-200">
// {Amenities_demos.filter((_, i) => i < 1212).map((item) => (
// <div
// key={item.name}
// className="flex items-center py-2.5 sm:py-4 lg:py-5 space-x-5 lg:space-x-8"
// >
// <i
// className={`text-4xl text-neutral-6000 las ${item.icon}`}
// ></i>
// <span>{item.name}</span>
// </div>
// ))}
// </div>
// </div>
// </div>
// </Transition.Child>
// </div>
// </Dialog>
// </Transition>
// );
// };
const renderSection4 = () => { const renderSection4 = () => {
return ( return (
<div className="listingSection__wrap"> <div className="listingSection__wrap">
{/* HEADING */} {/* HEADING */}
<div> <div>
<h2 className="text-2xl font-semibold">Room Rates </h2>
<span className="block mt-2 text-neutral-500 dark:text-neutral-400">
Prices may increase on weekends or holidays
</span>
<h2 className="text-2xl font-semibold">Itinerary </h2>
</div> </div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> <div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
{/* CONTENT */} {/* CONTENT */}
<div className="flow-root"> <div className="flow-root">
<div className="text-sm sm:text-base text-neutral-6000 dark:text-neutral-300 -mb-4">
<div className="p-4 bg-neutral-100 dark:bg-neutral-800 flex justify-between items-center space-x-4 rounded-lg">
<span>Monday - Thursday</span>
<span>$199</span>
</div>
<div className="p-4 flex justify-between items-center space-x-4 rounded-lg">
<span>Monday - Thursday</span>
<span>$199</span>
</div>
<div className="p-4 bg-neutral-100 dark:bg-neutral-800 flex justify-between items-center space-x-4 rounded-lg">
<span>Friday - Sunday</span>
<span>$219</span>
</div>
<div className="p-4 flex justify-between items-center space-x-4 rounded-lg">
<span>Rent by month</span>
<span>-8.34 %</span>
</div>
<div className="p-4 bg-neutral-100 dark:bg-neutral-800 flex justify-between items-center space-x-4 rounded-lg">
<span>Minimum number of nights</span>
<span>1 night</span>
<div className="text-sm sm:text-base text-neutral-6000 dark:text-neutral-300 mb-4">
{iteneries?.map((item, index) => (
<div className="flex">
<div className="mt-2">
<div className=" mx-4 transform -translate-x-1/2 w-8 h-8 bg-white border-secondery-bronze border-[3px] rounded-full"></div>
{totalIteneries !== index + 1 && (
<div className=" mx-4 mt-[-2px] transform -translate-x-1/2 w-[1px] h-full border-secondery-bronze border-[2px]"></div>
)}
</div>
<div className="p-5 rounded-3xl mb-4 border-2">
<h1 className="text-black font-bold mb-1">{item?.title}</h1>
<span>
{item?.started_at
.replaceAll("-", " ")
.replaceAll("T", " | ")}
</span>
<div className="mt-3">{item.summary}</div>
</div> </div>
<div className="p-4 flex justify-between items-center space-x-4 rounded-lg">
<span>Max number of nights</span>
<span>90 nights</span>
</div> </div>
))}
</div> </div>
</div> </div>
</div> </div>
@ -462,36 +457,7 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
return ( return (
<div className="listingSection__wrap"> <div className="listingSection__wrap">
{/* HEADING */} {/* HEADING */}
<h2 className="text-2xl font-semibold">Things to know</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700" />
{/* CONTENT */}
<div>
<h4 className="text-lg font-semibold">Cancellation policy</h4>
<span className="block mt-3 text-neutral-500 dark:text-neutral-400">
Refund 50% of the booking value when customers cancel the room
within 48 hours after successful booking and 14 days before the
check-in time. <br />
Then, cancel the room 14 days before the check-in time, get a 50%
refund of the total amount paid (minus the service fee).
</span>
</div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700" />
{/* CONTENT */}
<div>
<h4 className="text-lg font-semibold">Check-in time</h4>
<div className="mt-3 text-neutral-500 dark:text-neutral-400 max-w-md text-sm sm:text-base">
<div className="flex space-x-10 justify-between p-3 bg-neutral-100 dark:bg-neutral-800 rounded-lg">
<span>Check-in</span>
<span>08:00 am - 12:00 am</span>
</div>
<div className="flex space-x-10 justify-between p-3">
<span>Check-out</span>
<span>02:00 pm - 04:00 pm</span>
</div>
</div>
</div>
<h2 className="text-2xl font-semibold">Travel guide and tips</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700" /> <div className="w-14 border-b border-neutral-200 dark:border-neutral-700" />
{/* CONTENT */} {/* CONTENT */}
@ -499,17 +465,13 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
<h4 className="text-lg font-semibold">Special Note</h4> <h4 className="text-lg font-semibold">Special Note</h4>
<div className="prose sm:prose"> <div className="prose sm:prose">
<ul className="mt-3 text-neutral-500 dark:text-neutral-400 space-y-2"> <ul className="mt-3 text-neutral-500 dark:text-neutral-400 space-y-2">
{details && details.travel_tips.map((item)=>(
<>
<h4>
{item.title}
</h4>
<p>
{item.description}
</p>
</>
))
}
{details &&
details.travel_tips.map((item) => (
<li>
<h4>{item.title}</h4>
<p>{item.description}</p>
</li>
))}
</ul> </ul>
</div> </div>
</div> </div>
@ -558,7 +520,7 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
</div> </div>
{/* SUBMIT */} {/* SUBMIT */}
<ButtonPrimary href={"/checkout"}>Reserve</ButtonPrimary>
<ButtonPrimary href={`/add-listing/${id}`}>Reserve</ButtonPrimary>
</div> </div>
); );
}; };
@ -570,8 +532,8 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2"> <div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2">
<div <div
className="col-span-2 row-span-3 sm:row-span-2 relative rounded-md sm:rounded-xl overflow-hidden cursor-pointer" className="col-span-2 row-span-3 sm:row-span-2 relative rounded-md sm:rounded-xl overflow-hidden cursor-pointer"
onClick={handleOpenModalImageGallery}
> >
<Image <Image
fill fill
className="object-cover rounded-md sm:rounded-xl" className="object-cover rounded-md sm:rounded-xl"
@ -579,16 +541,28 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
alt="" alt=""
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 50vw" 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
className="object-cover rounded-md sm:rounded-xl "
src={item.image_url.lg}
alt=""
sizes="400px"
/>
</div> */}
<div className="absolute inset-0 bg-neutral-900 bg-opacity-20 opacity-0 hover:opacity-100 transition-opacity"></div> <div className="absolute inset-0 bg-neutral-900 bg-opacity-20 opacity-0 hover:opacity-100 transition-opacity"></div>
</div> </div>
{details?.images.filter((_, i) => i >= 1 && i < 5).map((item, index) => (
{details?.images
.filter((_, i) => i >= 1 && i < 5)
.map((item, index) => (
<div <div
key={index} key={index}
className={`relative rounded-md sm:rounded-xl overflow-hidden ${ className={`relative rounded-md sm:rounded-xl overflow-hidden ${
index >= 3 ? "hidden sm:block" : "" index >= 3 ? "hidden sm:block" : ""
}`} }`}
> >
<ConfirmModal
lable={
<div className="aspect-w-4 aspect-h-3 sm:aspect-w-6 sm:aspect-h-5"> <div className="aspect-w-4 aspect-h-3 sm:aspect-w-6 sm:aspect-h-5">
<Image <Image
fill fill
@ -598,12 +572,21 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
sizes="400px" sizes="400px"
/> />
</div> </div>
}
>
{(closeModal) => (
<div className="h-[600px] w-[600px]">
<Image
fill
className="object-cover rounded-md sm:rounded-xl "
src={item.image_url.lg}
alt=""
/>
</div>
)}
</ConfirmModal>
{/* OVERLAY */} {/* OVERLAY */}
<div
className="absolute inset-0 bg-neutral-900 bg-opacity-20 opacity-0 hover:opacity-100 transition-opacity cursor-pointer"
onClick={handleOpenModalImageGallery}
/>
</div> </div>
))} ))}
@ -623,14 +606,14 @@ const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => {
<main className=" relative z-10 mt-11 flex flex-col lg:flex-row "> <main className=" relative z-10 mt-11 flex flex-col lg:flex-row ">
{/* CONTENT */} {/* CONTENT */}
<div className="w-full lg:w-3/5 xl:w-2/3 space-y-8 lg:space-y-10 lg:pr-10"> <div className="w-full lg:w-3/5 xl:w-2/3 space-y-8 lg:space-y-10 lg:pr-10">
{renderSection1()}
{/* {renderSection1()} */}
{renderSection2()} {renderSection2()}
{renderSection3()} {renderSection3()}
{renderSection8()}
{renderSection4()} {renderSection4()}
{renderSection5()}
{/* {renderSection5()} */}
{/* {renderSection6()} */} {/* {renderSection6()} */}
{renderSection7()}
{renderSection8()}
{/* {renderSection7()} */}
</div> </div>
{/* SIDEBAR */} {/* SIDEBAR */}

51
src/components/CardCategory1.tsx
File diff suppressed because it is too large
View File

2
src/components/StayCard2.tsx

@ -57,7 +57,7 @@ const StayCard2: FC<StayCard2Props> = ({
/> />
</div> </div>
</Link> </Link>
<BtnLikeIcon isLiked={like} className="absolute right-3 top-3 z-[1]" />
{/* <BtnLikeIcon isLiked={like} className="absolute right-3 top-3 z-[1]" /> */}
{<Badge className=" opacity-70 absolute left-3 top-3" name={data.status} color={data.status} />} {<Badge className=" opacity-70 absolute left-3 top-3" name={data.status} color={data.status} />}
</> </>

252
src/components/TourSuggestion.tsx

@ -0,0 +1,252 @@
"use client";
import React, { FC, useContext, useEffect, useState } from "react";
import { TaxonomyType } from "@/data/types";
import CardCategory3 from "@/components/CardCategory3";
import CardCategory4 from "@/components/CardCategory4";
import CardCategory5 from "@/components/CardCategory5";
import Heading from "@/shared/Heading";
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { useSwipeable } from "react-swipeable";
import PrevBtn from "./PrevBtn";
import NextBtn from "./NextBtn";
import { variants } from "@/utils/animationVariants";
import { useWindowSize } from "react-use";
import axiosInstance from "./api/axios";
import { Context } from "./contexts/tourDetails";
import CardCategory1 from "./CardCategory1";
export interface TourSuggestionProps {
className?: string;
itemClassName?: string;
heading?: string;
subHeading?: string;
categories?: TaxonomyType[];
categoryCardType?: "card3" | "card4" | "card5";
itemPerRow?: 4 | 5;
sliderStyle?: "style1" | "style2";
}
const DEMO_CATS: TaxonomyType[] = [
{
id: "1",
href: "/listing-stay-map",
name: "Nature House",
taxonomy: "category",
count: 17288,
thumbnail:
"https://images.pexels.com/photos/2581922/pexels-photo-2581922.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260",
},
{
id: "2",
href: "/listing-stay-map",
name: "Wooden house",
taxonomy: "category",
count: 2118,
thumbnail:
"https://images.pexels.com/photos/2351649/pexels-photo-2351649.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "3",
href: "/listing-stay-map",
name: "Houseboat",
taxonomy: "category",
count: 36612,
thumbnail:
"https://images.pexels.com/photos/962464/pexels-photo-962464.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "4",
href: "/listing-stay-map",
name: "Farm House",
taxonomy: "category",
count: 18188,
thumbnail:
"https://images.pexels.com/photos/248837/pexels-photo-248837.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "5",
href: "/listing-stay-map",
name: "Dome House",
taxonomy: "category",
count: 22288,
thumbnail:
"https://images.pexels.com/photos/3613236/pexels-photo-3613236.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
},
{
id: "6",
href: "/listing-stay-map",
name: "Dome House",
taxonomy: "category",
count: 188288,
thumbnail:
"https://images.pexels.com/photos/14534337/pexels-photo-14534337.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load",
},
{
id: "7",
href: "/listing-stay-map",
name: "Wooden house",
taxonomy: "category",
count: 2118,
thumbnail:
"https://images.pexels.com/photos/2351649/pexels-photo-2351649.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "8",
href: "/listing-stay-map",
name: "Wooden Dome",
taxonomy: "category",
count: 515,
thumbnail:
"https://images.pexels.com/photos/9039238/pexels-photo-9039238.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load",
},
];
const TourSuggestion: FC<TourSuggestionProps> = ({
heading = "Countries ",
subHeading = "Popular places to recommends for you",
className = "",
itemClassName = "",
categories = DEMO_CATS,
itemPerRow = 5,
categoryCardType = "card3",
sliderStyle = "style1",
}) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [numberOfItems, setNumberOfitem] = useState(0);
const { tours } = useContext(Context);
const windowWidth = useWindowSize().width;
useEffect(() => {
if (windowWidth < 320) {
return setNumberOfitem(1);
}
if (windowWidth < 500) {
return setNumberOfitem(itemPerRow - 3);
}
if (windowWidth < 1024) {
return setNumberOfitem(itemPerRow - 2);
}
if (windowWidth < 1280) {
return setNumberOfitem(itemPerRow - 1);
}
setNumberOfitem(itemPerRow);
}, [itemPerRow, windowWidth]);
// useEffect(() => {
// axiosInstance
// .get("/api/tours/")
// .then((response) => {
// setTours(response.data);
// })
// .catch((error) => {
// console.error("Error fetching data:", error);
// });
// }, []);
function changeItemId(newVal: number) {
if (newVal > currentIndex) {
setDirection(1);
} else {
setDirection(-1);
}
setCurrentIndex(newVal);
}
const handlers = useSwipeable({
onSwipedLeft: () => {
if (currentIndex < tours.results?.length - 1) {
changeItemId(currentIndex + 1);
}
},
onSwipedRight: () => {
if (currentIndex > 0) {
changeItemId(currentIndex - 1);
}
},
trackMouse: true,
});
// const renderCard = (item: TaxonomyType) => {
// switch (categoryCardType) {
// case "card3":
// return <CardCategory3 taxonomy={item} />;
// case "card4":
// return <CardCategory4 taxonomy={item} />;
// case "card5":
// return <CardCategory5 taxonomy={item} />;
// default:
// return <CardCategory3 taxonomy={item} />;
// }
// };
if (!numberOfItems) return null;
return (
<div className={`nc-SectionSliderNewCategories ${className}`}>
<Heading desc={subHeading} isCenter={sliderStyle === "style2"}>
{heading}
</Heading>
<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}>
{tours.results?.map((item, indx) => (
<motion.li
className={`relative inline-block px-2 xl:px-4 ${itemClassName}`}
custom={direction}
initial={{
x: `${(currentIndex - 1) * -100}%`,
}}
animate={{
x: `${currentIndex * -100}%`,
}}
variants={variants(200, 1)}
key={indx}
style={{
width: `calc(1/${numberOfItems} * 100%)`,
}}
>
<CardCategory1 taxonomy={item} />
</motion.li>
))}
</AnimatePresence>
</motion.ul>
</div>
{currentIndex ? (
<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]"
/>
) : null}
{tours.results?.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]"
/>
) : null}
</div>
</MotionConfig>
</div>
);
};
export default TourSuggestion;

11
src/hooks/FormValidation.ts

@ -5,33 +5,27 @@ import { useState } from 'react';
const useFormValidation = () => { const useFormValidation = () => {
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
// Validate form fields
const validateForm = (form) => { const validateForm = (form) => {
let newErrors = {}; let newErrors = {};
// Full Name validation
if (!form.name) { if (!form.name) {
newErrors.name = 'Full Name is required'; newErrors.name = 'Full Name is required';
} }
// Country Code validation: must be a number and up to 3 digits
if (!form.countryCode || !/^\d{1,3}$/.test(form.countryCode)) { if (!form.countryCode || !/^\d{1,3}$/.test(form.countryCode)) {
newErrors.countryCode = 'Country Code must be a number with up to 3 digits'; newErrors.countryCode = 'Country Code must be a number with up to 3 digits';
} }
// Phone Number validation: must be a number and not empty
if (!form.phoneNumber || !/^\d+$/.test(form.phoneNumber)) { if (!form.phoneNumber || !/^\d+$/.test(form.phoneNumber)) {
newErrors.phoneNumber = 'Phone Number is required and must be a number'; newErrors.phoneNumber = 'Phone Number is required and must be a number';
} }
// Password validation
if (!form.password) { if (!form.password) {
newErrors.password = 'Password is required'; newErrors.password = 'Password is required';
} else if (form.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
} else if (form.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
} }
// Confirm Password validation
if (!form.confirmPassword) { if (!form.confirmPassword) {
newErrors.confirmPassword = 'Confirm Password is required'; newErrors.confirmPassword = 'Confirm Password is required';
} else if (form.password !== form.confirmPassword) { } else if (form.password !== form.confirmPassword) {
@ -39,7 +33,6 @@ const useFormValidation = () => {
} }
setErrors(newErrors); setErrors(newErrors);
// Return true if no errors
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
}; };

26
src/hooks/passengerValidation.ts

@ -0,0 +1,26 @@
// Validation function
const validatePassenger = (passenger) => {
console.log(passenger);
const errors = {};
if (!passenger.fullname.trim()) {
errors.fullName = "Full Name is required";
}
if (!passenger.birthdate) {
errors.date = "Date of Birth is required";
}
if (!passenger.phone_number.trim()) {
errors.number = "Phone Number is required";
}
if (!passenger.passport_number.trim()) {
errors.passport = "Passport Number is required";
}
if (!passenger.passport_image) {
errors.image = "Passport image is required";
}
return errors;
};
export default validatePassenger

2
src/shared/Navigation/Navigation.tsx

@ -10,7 +10,7 @@ function Navigation() {
{NAVIGATION_DEMO.map((item) => ( {NAVIGATION_DEMO.map((item) => (
<NavigationItem key={item.id} menuItem={item} /> <NavigationItem key={item.id} menuItem={item} />
))} ))}
<ButtonSecondary className="m-5">Custoum Tour</ButtonSecondary>
<ButtonSecondary href="/custom-trip" className="m-5">Custoum Tour</ButtonSecondary>
</ul> </ul>
); );
} }

74
src/shared/popUp.tsx

@ -0,0 +1,74 @@
"use client";
import { useEffect, useState } from "react";
const ConfirmModal = ({ children, lable }) => {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
document.body.classList.add("overflow-y-hidden");
};
const closeModal = () => {
setIsOpen(false);
document.body.classList.remove("overflow-y-hidden");
};
// Handle Esc key to close modal
const handleKeyDown = (event) => {
if (event.key === "Escape") {
closeModal();
}
};
// Add event listener for keydown on mount and cleanup on unmount
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div >
<div onClick={openModal}>
{lable}
</div>
{isOpen && (
<div
id="modelConfirm"
className="fixed z-50 inset-0 bg-gray-900 bg-opacity-60 overflow-y-auto h-full w-full flex items-center justify-center"
>
<div className="relative mx-auto shadow-xl rounded-md bg-white max-w-4xl w-full p-4">
<div className="flex justify-end p-2">
<button
onClick={closeModal}
type="button"
className="text-gray-400 z-40 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
{children(closeModal)}
</div>
</div>
)}
</div>
);
};
export default ConfirmModal;
Loading…
Cancel
Save