sina_sajjadi
2 months ago
18 changed files with 1098 additions and 445 deletions
-
2src/app/(account-pages)/(components)/Nav.tsx
-
88src/app/(account-pages)/account-savelists/page.tsx
-
40src/app/(account-pages)/account/page.tsx
-
69src/app/(account-pages)/my-trips/page.tsx
-
217src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx
-
138src/app/add-listing/[[...stepIndex]]/page.tsx
-
175src/app/custom-trip/page.tsx
-
3src/app/page.tsx
-
4src/app/signup/methodes/page.tsx
-
10src/app/signup/page.tsx
-
377src/app/tours/[slug]/page.tsx
-
53src/components/CardCategory1.tsx
-
2src/components/StayCard2.tsx
-
252src/components/TourSuggestion.tsx
-
11src/hooks/FormValidation.ts
-
26src/hooks/passengerValidation.ts
-
2src/shared/Navigation/Navigation.tsx
-
74src/shared/popUp.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; |
|
@ -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; |
@ -1,92 +1,177 @@ |
|||||
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 ( |
||||
<> |
|
||||
<h2 className="text-2xl font-semibold">Choosing listing categories</h2> |
|
||||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|
||||
{/* FORM */} |
|
||||
<div className="space-y-8"> |
|
||||
{/* ITEM */} |
|
||||
|
!selectedPassenger ? ( |
||||
|
<> |
||||
|
<h2 className="text-2xl font-semibold">Choosing listing categories</h2> |
||||
|
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
||||
|
{/* FORM */} |
||||
|
<ConfirmModal |
||||
|
lable={ |
||||
|
<div className="flex items-center space-x-2 text-orange-500 cursor-pointer hover:text-orange-600"> |
||||
|
<IoPersonAddOutline className="text-xl" /> {/* Adjust icon size */} |
||||
|
<p className="text-sm font-medium">Add from passenger list</p> |
||||
|
</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="flex items-center space-x-2 text-orange-500 cursor-pointer hover:text-orange-600"> |
|
||||
<IoPersonAddOutline className="text-xl" /> {/* Adjust icon size */} |
|
||||
<p className="text-sm font-medium">Add From passengers List</p> |
|
||||
</div> |
|
||||
|
<div className="space-y-8"> |
||||
|
{/* ITEM */} |
||||
|
|
||||
<FormItem label="Full Name" desc=""> |
|
||||
<Input |
|
||||
onChange={(e) => |
|
||||
setNewPassenger((prev) => ({ ...prev, fullName: e.target.value })) |
|
||||
} |
|
||||
value={fullName} |
|
||||
placeholder="Full Name" |
|
||||
/> |
|
||||
</FormItem> |
|
||||
<FormItem label="Passport Number" desc=""> |
|
||||
<Input |
|
||||
type="number" |
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" |
|
||||
onChange={(e) => |
|
||||
setNewPassenger((prev) => ({ ...prev, passport: e.target.value })) |
|
||||
} |
|
||||
value={passport} |
|
||||
placeholder="Passport Number" |
|
||||
/> |
|
||||
</FormItem> |
|
||||
<FormItem label="Date of Birth" desc=""> |
|
||||
<Input |
|
||||
type="date" |
|
||||
value={date} |
|
||||
onChange={(e) => |
|
||||
setNewPassenger((prev) => ({ ...prev, date: e.target.value })) |
|
||||
} |
|
||||
placeholder="Date of Birth" |
|
||||
/> |
|
||||
</FormItem> |
|
||||
<FormItem label="Phone Number" desc=""> |
|
||||
<Input |
|
||||
type="number" |
|
||||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" |
|
||||
value={number} |
|
||||
onChange={(e) => |
|
||||
setNewPassenger((prev) => ({ ...prev, number: e.target.value })) |
|
||||
} |
|
||||
placeholder="Phone Number" |
|
||||
/> |
|
||||
</FormItem> |
|
||||
<FormItem label="Upload Passport image Here" desc=""> |
|
||||
<Input |
|
||||
type="file" |
|
||||
onChange={handleFileChange} |
|
||||
placeholder="Passport" |
|
||||
/> |
|
||||
</FormItem> |
|
||||
</div> |
|
||||
</> |
|
||||
|
<FormItem label="Full Name" desc=""> |
||||
|
<Input |
||||
|
onChange={(e) => |
||||
|
setNewPassenger((prev) => ({ ...prev, fullname: e.target.value })) |
||||
|
} |
||||
|
value={fullName} |
||||
|
placeholder="Full Name" |
||||
|
/> |
||||
|
{errors.fullName && ( |
||||
|
<p className="text-xs text-red-500"> {errors.fullName} </p> |
||||
|
)} |
||||
|
</FormItem> |
||||
|
<FormItem label="Passport Number" desc=""> |
||||
|
<Input |
||||
|
type="number" |
||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" |
||||
|
onChange={(e) => |
||||
|
setNewPassenger((prev) => ({ ...prev, passport_number: e.target.value })) |
||||
|
} |
||||
|
value={passport} |
||||
|
placeholder="Passport Number" |
||||
|
/> |
||||
|
{errors.passport && ( |
||||
|
<p className="text-xs text-red-500"> {errors.passport} </p> |
||||
|
)} |
||||
|
</FormItem> |
||||
|
<FormItem label="Date of Birth" desc=""> |
||||
|
<Input |
||||
|
type="date" |
||||
|
value={date} |
||||
|
onChange={(e) => |
||||
|
setNewPassenger((prev) => ({ ...prev, birthdate: e.target.value })) |
||||
|
} |
||||
|
placeholder="Date of Birth" |
||||
|
/> |
||||
|
{errors.date && ( |
||||
|
<p className="text-xs text-red-500"> {errors.date} </p> |
||||
|
)} |
||||
|
</FormItem> |
||||
|
<FormItem label="Phone Number" desc=""> |
||||
|
<Input |
||||
|
type="number" |
||||
|
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" |
||||
|
value={number} |
||||
|
onChange={(e) => |
||||
|
setNewPassenger((prev) => ({ ...prev, phone_number: e.target.value })) |
||||
|
} |
||||
|
placeholder="Phone Number" |
||||
|
/> |
||||
|
{errors.number && ( |
||||
|
<p className="text-xs text-red-500"> {errors.number} </p> |
||||
|
)} |
||||
|
</FormItem> |
||||
|
<FormItem label="Upload Passport image Here" desc=""> |
||||
|
<Input |
||||
|
type="file" |
||||
|
onChange={handleFileChange} |
||||
|
placeholder="Passport" |
||||
|
/> |
||||
|
{errors.image && ( |
||||
|
<p className="text-xs text-red-500"> {errors.image} </p> |
||||
|
)} |
||||
|
</FormItem> |
||||
|
</div> |
||||
|
</> |
||||
|
) : ( |
||||
|
<p className="text-xs text-red-500"> {selectedPassenger.fullname}</p> |
||||
|
) |
||||
); |
); |
||||
}; |
}; |
||||
|
|
||||
|
@ -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; |
53
src/components/CardCategory1.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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; |
@ -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 |
||||
|
|
@ -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; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue