diff --git a/src/app/(account-pages)/(components)/Nav.tsx b/src/app/(account-pages)/(components)/Nav.tsx index d548f4a..1633962 100644 --- a/src/app/(account-pages)/(components)/Nav.tsx +++ b/src/app/(account-pages)/(components)/Nav.tsx @@ -10,7 +10,7 @@ export const Nav = () => { const listNav: Route[] = [ "/account", - "/account-savelists", + "/my-trips", "/account-password", "/passengers-list", ]; diff --git a/src/app/(account-pages)/account-savelists/page.tsx b/src/app/(account-pages)/account-savelists/page.tsx deleted file mode 100644 index 05905d4..0000000 --- a/src/app/(account-pages)/account-savelists/page.tsx +++ /dev/null @@ -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 ( -
-
-

Save lists

-
-
- -
- - - {categories.map((item) => ( - - {({ selected }) => ( - - )} - - ))} - - - -
- {DEMO_STAY_LISTINGS.filter((_, i) => i < 8).map((stay) => ( - - ))} -
-
- Show me more -
-
- -
- {DEMO_EXPERIENCES_LISTINGS.filter((_, i) => i < 8).map( - (stay) => ( - - ) - )} -
-
- Show me more -
-
- -
- {DEMO_CAR_LISTINGS.filter((_, i) => i < 8).map((stay) => ( - - ))} -
-
- Show me more -
-
-
-
-
-
- ); - }; - - return renderSection1(); -}; - -export default AccountSavelists; diff --git a/src/app/(account-pages)/account/page.tsx b/src/app/(account-pages)/account/page.tsx index 297e3d6..4d9a106 100644 --- a/src/app/(account-pages)/account/page.tsx +++ b/src/app/(account-pages)/account/page.tsx @@ -15,7 +15,7 @@ export interface AccountPageProps {} const AccountPage = () => { const router = useRouter(); - const User = JSON.parse(localStorage.getItem("user")) + const User = JSON.parse(localStorage.getItem("user")); let user = JSON.parse(localStorage.getItem("user")); if (!user) { return router.replace("/"); @@ -29,12 +29,11 @@ const AccountPage = () => { const [image, setImage] = useState(null); const [imageURL, setImageURL] = useState(null); const [error, setError] = useState(""); + const [loading, setLoading] = useState({}); const deleteHandler = async () => { - setStatus(false); - setError(""); - + setLoading({delete : true}) try { const response = await axiosInstance.delete( `/api/account/profile/delete/`, @@ -46,13 +45,20 @@ const AccountPage = () => { ); if (response.status === 204) { localStorage.removeItem("user"); + setStatus(false); + router.replace("/"); } else { setError("Something went wrong"); } } catch (error) { setError(error.message); - } + + } finally{ + setLoading({delete : false}) + + + } }; const signOutHandler = () => { localStorage.removeItem("user"); @@ -63,6 +69,8 @@ const AccountPage = () => { const changeHandler = async () => { setError(""); + setLoading({change : true}) + const formData = new FormData(); formData.append("fullname", name); formData.append("email", email); @@ -83,17 +91,25 @@ const AccountPage = () => { } ); if (response.status === 200) { + console.log(response); + user.avatar = response.data.avatar; user.email = response.data.email; user.fullname = response.data.fullname; user.phone_number = response.data.phone_number; localStorage.setItem("user", JSON.stringify(user)); + } else { setError("Something went wrong"); + } } catch (error) { setError(error.message); + + } finally{ + setLoading({change : false}) + } }; @@ -103,14 +119,11 @@ const AccountPage = () => { const uploadedFile = await getImageURL(file); setImageURL(uploadedFile.url); - }; - - setImage(file); - - - } + setImage(file); + }; + return (

Account information

@@ -186,9 +199,9 @@ const AccountPage = () => { className="mt-1.5" />
- {error &&

{error}

} + {error &&

{error}

}
- + Update info @@ -196,6 +209,7 @@ const AccountPage = () => { Sign out diff --git a/src/app/(account-pages)/my-trips/page.tsx b/src/app/(account-pages)/my-trips/page.tsx new file mode 100644 index 0000000..47f47bb --- /dev/null +++ b/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 ( +
+
+

Save lists

+
+
+ +
+ + + +
+ {tours.length ? (tours?.filter((_, i) => i < 8).map((stay) => ( + + ))) : (

You have no trips

)} +
+
+
+
+
+
+
+
+ ); + }; + + return renderSection1(); +}; + +export default MyTrips; diff --git a/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx b/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx index 4647b19..4dabc4d 100644 --- a/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx +++ b/src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx @@ -1,92 +1,177 @@ -import React, { FC } from "react"; +import React, { FC, useEffect, useState } from "react"; import Input from "@/shared/Input"; import Select from "@/shared/Select"; import FormItem from "../FormItem"; import { IoPersonAddOutline } from "react-icons/io5"; 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 = ({ Passenger, setNewPassenger, + errors, + passengerID, + setPassengerID, + selectedPassenger, + setSelectedPassenger, // Receive the function to reset selectedPassenger }) => { - 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) => { const file = e.target.files?.[0]; if (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 ( - <> -

Choosing listing categories

-
- {/* FORM */} -
- {/* ITEM */} + !selectedPassenger ? ( + <> +

Choosing listing categories

+
+ {/* FORM */} + + {/* Adjust icon size */} +

Add from passenger list

+
+ } + > + {(closeModal) => ( +
    + {savedPassengers?.map((item, index) => ( +
  • { 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`} + > +

    {item.fullname}

    +
  • + ))} +
+ )} + -
- {/* Adjust icon size */} -

Add From passengers List

-
+
+ {/* ITEM */} - - - setNewPassenger((prev) => ({ ...prev, fullName: e.target.value })) - } - value={fullName} - placeholder="Full Name" - /> - - - - setNewPassenger((prev) => ({ ...prev, passport: e.target.value })) - } - value={passport} - placeholder="Passport Number" - /> - - - - setNewPassenger((prev) => ({ ...prev, date: e.target.value })) - } - placeholder="Date of Birth" - /> - - - - setNewPassenger((prev) => ({ ...prev, number: e.target.value })) - } - placeholder="Phone Number" - /> - - - - -
- + + + setNewPassenger((prev) => ({ ...prev, fullname: e.target.value })) + } + value={fullName} + placeholder="Full Name" + /> + {errors.fullName && ( +

{errors.fullName}

+ )} +
+ + + setNewPassenger((prev) => ({ ...prev, passport_number: e.target.value })) + } + value={passport} + placeholder="Passport Number" + /> + {errors.passport && ( +

{errors.passport}

+ )} +
+ + + setNewPassenger((prev) => ({ ...prev, birthdate: e.target.value })) + } + placeholder="Date of Birth" + /> + {errors.date && ( +

{errors.date}

+ )} +
+ + + setNewPassenger((prev) => ({ ...prev, phone_number: e.target.value })) + } + placeholder="Phone Number" + /> + {errors.number && ( +

{errors.number}

+ )} +
+ + + {errors.image && ( +

{errors.image}

+ )} +
+
+ + ) : ( +

{selectedPassenger.fullname}

+ ) ); }; diff --git a/src/app/add-listing/[[...stepIndex]]/page.tsx b/src/app/add-listing/[[...stepIndex]]/page.tsx index f768db2..0edab37 100644 --- a/src/app/add-listing/[[...stepIndex]]/page.tsx +++ b/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 ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonSecondary from "@/shared/ButtonSecondary"; -import { Route } from "@/routers/types"; 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 { children: React.ReactNode; @@ -16,48 +18,114 @@ export interface CommonLayoutProps { } const CommonLayout: FC = ({ params }) => { + const router = useRouter(); + const user = JSON.parse(localStorage.getItem("user")); - const [passengers , setPassengers] =useState([]) - const [newPassenger , setNewPassenger] =useState({ - fullName :"", - date : "" , - number : "", - passport : "", - image : "" - }) - - const nextHandler =()=>{ - setPassengers((prev) => [...prev, newPassenger]); - - } - 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"; + const [index, setIndex] = useState(1); + const [passengers, setPassengers] = useState([]); + const [errors, setErrors] = useState({}); + const [newPassenger, setNewPassenger] = useState({ + 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 validationErrors = validatePassenger(newPassenger); + + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + console.log("Validation errors:", validationErrors); + return; + } + + setSelectedPassenger(null); + + setPassengers((prevPassengers) => [...prevPassengers, newPassenger]); + setNewPassenger({ + fullname: "", + passport_number: "", + birthdate: "", + phone_number: "", + passport_image: "", + }); + setErrors({}); + + nextHref(); + + }; + return ( -
+
{index}{" "} - / 10 + / {totalPassengers}
- {/* --------------------- */} -
+ {/* Passenger Form */} +
+ +
- {/* --------------------- */}
- {index > 1 && Go back} - - {nextBtnText || "Continue"} + {/* {index > 1 && ( + Go back + )} */} + + {nextBtnText}
diff --git a/src/app/custom-trip/page.tsx b/src/app/custom-trip/page.tsx new file mode 100644 index 0000000..680fc46 --- /dev/null +++ b/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 = () => { + const [countries, setCountries] = useState([]); + const [startCity, setStartCity] = useState(""); + const [endCity, setEndCity] = useState(""); + const [transport, setTransport] = useState([]); + const [hotel, setHotel] = useState([]); + const [selectedHotel, setSelectedHotel] = useState(""); + const [selectedTransport, setSelectedTransport] = useState(""); + 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 ( +
+
+
+
+

Custom Trip

+
+ +

Guide

+

+ 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. +

+
+ +
+

Begin your trip

+ + + +
+ + + + + + +
+
+
+ + {destinations.map((destination, index) => ( +
+

Destination {index + 1}

+ + + + + + + + + +
+ + + + + + +
+
+ ))} +
+ +
+ Add Destination + Continue +
+
+
+
+ ); +}; + +export default CommonLayout; diff --git a/src/app/page.tsx b/src/app/page.tsx index 3392b92..b1ce7c3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,6 +15,7 @@ import SectionClientSay from "@/components/SectionClientSay"; import SectionCustomTour from "@/components/SectionCustomTour"; import axios from "axios"; import axiosInstance from "@/components/api/axios"; +import TourSuggestion from "@/components/TourSuggestion"; const DEMO_CATS: TaxonomyType[] = [ { @@ -175,7 +176,7 @@ function PageHome() {
- = () => { const [confirmPassword, setConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); + const [failed , setFailed ] =useState("") + const { errors, validateForm } = useFormValidation(); const countryCodeHandler = (e) => { @@ -51,17 +53,20 @@ const PageSignUp: FC = () => { setForm(form); try { 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); - router.push("/signup/methods"); + router.push("/signup/methodes"); } catch (error) { console.error("Error fetching data:", error); + setFailed(error.message) + } finally { setLoading(false); } } else { console.log("Form has errors:", errors); + } }; @@ -137,6 +142,7 @@ const PageSignUp: FC = () => {

{errors.confirmPassword}

)} + {failed && (

{failed}

)} = ({}) => { // const { getTourData, details, passengers } = useContext(Context); + const [iteneries, setIteneries] = useState(); const r = /-?(\d+)$/; const id: number = useParams().slug.match(r)[1]; + const totalIteneries = iteneries?.length; + + console.log(details); useEffect(() => { 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); const thisPathname = usePathname(); @@ -125,22 +141,10 @@ const ListingStayDetailPage: FC = ({}) => { const renderSection2 = () => { return (
-

Stay information

+

{details?.title}

{details?.description} -
-
- - There is a private bathroom with bidet in all units, along with a - hairdryer and free toiletries. - -

- - 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. -
); @@ -150,142 +154,133 @@ const ListingStayDetailPage: FC = ({}) => { return (
-

Amenities

- - {` About the property's amenities and services`} - +

Tour Features

{/* 6 */} -
- {details && details.tour_features?.map((item) => ( -
- {item.title} -
- ))} +
+ {details && + details.tour_features?.map((item) => ( +
+ + {item.title} +
+ ))}
-
+ {/*
View more 20 amenities
- {renderMotalAmenities()} + {renderMotalAmenities()} */}
); }; - const renderMotalAmenities = () => { - return ( - - -
- - - - - {/* This element is to trick the browser into centering the modal contents. */} - - -
-
-
-

- Amenities -

- - - -
-
- {Amenities_demos.filter((_, i) => i < 1212).map((item) => ( -
- - {item.name} -
- ))} -
-
-
-
-
-
-
- ); - }; + // const renderMotalAmenities = () => { + // return ( + // + // + //
+ // + // + // + + // {/* This element is to trick the browser into centering the modal contents. */} + // + // + //
+ //
+ //
+ //

+ // Amenities + //

+ // + // + // + //
+ //
+ // {Amenities_demos.filter((_, i) => i < 1212).map((item) => ( + //
+ // + // {item.name} + //
+ // ))} + //
+ //
+ //
+ //
+ //
+ //
+ //
+ // ); + // }; const renderSection4 = () => { return (
{/* HEADING */}
-

Room Rates

- - Prices may increase on weekends or holidays - +

Itinerary

{/* CONTENT */}
-
-
- Monday - Thursday - $199 -
-
- Monday - Thursday - $199 -
-
- Friday - Sunday - $219 -
-
- Rent by month - -8.34 % -
-
- Minimum number of nights - 1 night -
-
- Max number of nights - 90 nights -
+
+ {iteneries?.map((item, index) => ( +
+
+
+ {totalIteneries !== index + 1 && ( +
+ )} +
+
+

{item?.title}

+ + {item?.started_at + .replaceAll("-", " ") + .replaceAll("T", " | ")} + +
{item.summary}
+
+
+ ))}
@@ -462,36 +457,7 @@ const ListingStayDetailPage: FC = ({}) => { return (
{/* HEADING */} -

Things to know

-
- - {/* CONTENT */} -
-

Cancellation policy

- - 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.
- Then, cancel the room 14 days before the check-in time, get a 50% - refund of the total amount paid (minus the service fee). -
-
-
- - {/* CONTENT */} -
-

Check-in time

-
-
- Check-in - 08:00 am - 12:00 am -
-
- Check-out - 02:00 pm - 04:00 pm -
-
-
+

Travel guide and tips

{/* CONTENT */} @@ -499,17 +465,13 @@ const ListingStayDetailPage: FC = ({}) => {

Special Note

    - {details && details.travel_tips.map((item)=>( - <> -

    - {item.title} -

    -

    - {item.description} -

    - - )) - } + {details && + details.travel_tips.map((item) => ( +
  • +

    {item.title}

    +

    {item.description}

    +
  • + ))}
@@ -558,7 +520,7 @@ const ListingStayDetailPage: FC = ({}) => {
{/* SUBMIT */} - Reserve + Reserve
); }; @@ -570,8 +532,8 @@ const ListingStayDetailPage: FC = ({}) => {
+ = ({}) => { alt="" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 50vw" /> - + {/*
+ +
*/}
- {details?.images.filter((_, i) => i >= 1 && i < 5).map((item, index) => ( -
= 3 ? "hidden sm:block" : "" - }`} - > -
- -
- - {/* OVERLAY */} + {details?.images + .filter((_, i) => i >= 1 && i < 5) + .map((item, index) => (
-
- ))} + key={index} + className={`relative rounded-md sm:rounded-xl overflow-hidden ${ + index >= 3 ? "hidden sm:block" : "" + }`} + > + + +
+ } + > + {(closeModal) => ( +
+ +
+ )} + + + {/* OVERLAY */} +
+ ))} {/* +
+ + {children(closeModal)} +
+
+ )} +
+ ); +}; + +export default ConfirmModal;