sina_sajjadi
3 months ago
66 changed files with 3511 additions and 1026 deletions
-
4.env.local.example
-
10next.config.js
-
358package-lock.json
-
4package.json
-
11public/لوگو3 1.svg
-
2src/app/(account-pages)/(components)/Nav.tsx
-
32src/app/(account-pages)/account-billing/page.tsx
-
196src/app/(account-pages)/account/page.tsx
-
55src/app/(account-pages)/passengers-list/PassengerTable.tsx
-
52src/app/(account-pages)/passengers-list/page.tsx
-
22src/app/(client-components)/(Header)/Header.tsx
-
42src/app/(client-components)/(Header)/MainNav1.tsx
-
13src/app/(client-components)/(Header)/SiteHeader.tsx
-
95src/app/(client-components)/(HeroSearchForm)/(stay-search-form)/StayDatesRangeInput.tsx
-
2src/app/(client-components)/(HeroSearchForm)/ButtonSubmit.tsx
-
62src/app/(client-components)/(HeroSearchForm)/GuestsInput.tsx
-
20src/app/(client-components)/(HeroSearchForm)/HeroSearchForm.tsx
-
70src/app/(client-components)/(HeroSearchForm)/LocationInput.tsx
-
2src/app/(listing-detail)/SectionDateRange.tsx
-
7src/app/(listing-detail)/layout.tsx
-
98src/app/add-listing/[[...stepIndex]]/PageAddListing1.tsx
-
50src/app/add-listing/[[...stepIndex]]/layout.tsx
-
126src/app/add-listing/[[...stepIndex]]/page.tsx
-
227src/app/add-new-passenger/page.tsx
-
26src/app/api/hello/auth/[...nextauth].ts
-
3src/app/api/hello/route.ts
-
151src/app/forgot-password/page.tsx
-
5src/app/globals.css
-
21src/app/layout.tsx
-
170src/app/login/page.tsx
-
55src/app/page.tsx
-
150src/app/signup/methodes/page.tsx
-
132src/app/signup/otp-code/page.tsx
-
191src/app/signup/page.tsx
-
94src/app/tours/[slug]/GuestsInput.tsx
-
69src/app/tours/[slug]/StayDatesRangeInput.tsx
-
71src/app/tours/[slug]/constant.ts
-
648src/app/tours/[slug]/page.tsx
-
70src/app/tours/layout.tsx
-
9src/components/CardCategory3.tsx
-
27src/components/HeaderFilter.tsx
-
69src/components/SectionCustomTour.tsx
-
73src/components/SectionGridFeaturePlaces.tsx
-
55src/components/SectionSliderNewCategories.tsx
-
54src/components/StayCard2.tsx
-
31src/components/api/axios.tsx
-
36src/components/api/getImageURL.tsx
-
73src/components/contexts/tourDetails.tsx
-
26src/components/contexts/userContext.tsx
-
21src/data/jsons/__stayListing.json
-
368src/data/navigation.ts
-
49src/hooks/FormValidation.ts
-
BINsrc/images/hero-right-orginal.png
-
BINsrc/images/hero-right.png
-
BINsrc/images/logos/لوگو3 1.png
-
11src/images/logos/لوگو3 1.svg
-
38src/shared/Avatar.tsx
-
65src/shared/Badge.tsx
-
2src/shared/ButtonPrimary.tsx
-
1src/shared/Input.tsx
-
23src/shared/Logo.tsx
-
2src/shared/LogoSvgLight.tsx
-
3src/shared/Navigation/Navigation.tsx
-
0src/shared/PhoneNumberInput.tsx
-
3tailwind.config.js
-
82yarn.lock
@ -1,4 +0,0 @@ |
|||
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=dwi7o19nn |
|||
CLOUDINARY_API_KEY=549144472596919 |
|||
CLOUDINARY_API_SECRET=AnNiPOszr1R0YCelomUmi9IyuBM |
|||
CLOUDINARY_FOLDER=test |
11
public/لوگو3 1.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,32 +0,0 @@ |
|||
import React from "react"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
|
|||
const AccountBilling = () => { |
|||
return ( |
|||
<div className="space-y-6 sm:space-y-8"> |
|||
{/* HEADING */} |
|||
<h2 className="text-3xl font-semibold">Payments & payouts</h2> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
<div className="max-w-2xl"> |
|||
<span className="text-xl font-semibold block">Payout methods</span> |
|||
<br /> |
|||
<span className="text-neutral-700 dark:text-neutral-300 block"> |
|||
{` When you receive a payment for a reservation, we call that payment
|
|||
to you a "payout." Our secure payment system supports several |
|||
payout methods, which can be set up below. Go to FAQ.`}
|
|||
<br /> |
|||
<br /> |
|||
To get paid, you need to set up a payout method Airbnb releases |
|||
payouts about 24 hours after a guest’s scheduled check-in time. The |
|||
time it takes for the funds to appear in your account depends on your |
|||
payout method. Learn more |
|||
</span> |
|||
<div className="pt-10"> |
|||
<ButtonPrimary>Add payout mothod</ButtonPrimary> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default AccountBilling; |
@ -0,0 +1,55 @@ |
|||
import axiosInstance from "@/components/api/axios"; |
|||
import React, { useState } from "react"; |
|||
import { IoMdTrash } from "react-icons/io"; |
|||
import { MdEdit } from "react-icons/md"; |
|||
|
|||
const PassengerTable = ({data}) => { |
|||
const user = JSON.parse(localStorage.getItem("user")) |
|||
|
|||
const [show , setShow] =useState(true) |
|||
|
|||
const deletHandler = async ()=>{ |
|||
try{ |
|||
const response = await axiosInstance.delete(`/api/account/passengers/${data.id}/` ,{ |
|||
headers :{ |
|||
Authorization : `token ${user.token}` |
|||
} |
|||
}) |
|||
console.log(response); |
|||
|
|||
setShow(false) |
|||
|
|||
}catch (error){ |
|||
console.log(error); |
|||
|
|||
|
|||
} |
|||
} |
|||
return ( |
|||
<div className= {`${!show && "hidden" } sm:w-[500px] flex items-center justify-between p-4 bg-white rounded-xl shadow-sm border border-neutral-200 dark:bg-neutral-800`}> |
|||
{/* Passenger Information */} |
|||
<div className="flex flex-col"> |
|||
<p className="text-sm text-neutral-500 dark:text-neutral-400"> |
|||
Passenger information |
|||
</p> |
|||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white"> |
|||
{data.fullname} |
|||
</h3> |
|||
</div> |
|||
|
|||
{/* Action Icons */} |
|||
<div className="flex space-x-6 text-neutral-500"> |
|||
<button className="hover:text-red-500 transition-colors"> |
|||
<button type="submit"> |
|||
<IoMdTrash onClick={deletHandler} className="text-2xl" /> {/* Increased size using text-2xl */} |
|||
</button> |
|||
</button> |
|||
<button className="hover:text-blue-500 transition-colors"> |
|||
<MdEdit className="text-2xl" /> {/* Increased size using text-2xl */} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default PassengerTable; |
@ -0,0 +1,52 @@ |
|||
"use client" |
|||
|
|||
import React, { useEffect, useState } from "react"; |
|||
import PassengerTable from "./PassengerTable"; |
|||
import { IoPersonAddOutline } from "react-icons/io5"; |
|||
import axiosInstance from "@/components/api/axios"; |
|||
import Link from "next/link"; |
|||
|
|||
const PassengersList = () => { |
|||
const user = JSON.parse(localStorage.getItem("user")) |
|||
|
|||
const [passengers , setPassenger ] = useState([]) |
|||
|
|||
|
|||
useEffect(()=>{ |
|||
axiosInstance.get("/api/account/passengers/" ,{ |
|||
headers :{ |
|||
Authorization : `token ${user.token}` |
|||
} |
|||
}) |
|||
.then((response)=>{ |
|||
setPassenger(response.data.results); |
|||
console.log(response); |
|||
|
|||
|
|||
|
|||
}).catch((error)=>{ |
|||
console.error(error); |
|||
|
|||
}) |
|||
console.log(passengers); |
|||
} , []) |
|||
|
|||
|
|||
return ( |
|||
<div className="flex flex-col items-start space-y-6 sm:space-y-8"> |
|||
{/* Add New Passenger Section */} |
|||
<Link href={"/add-new-passenger"} 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 new passenger</p> |
|||
</Link> |
|||
|
|||
{/* Passenger Table */} |
|||
{passengers.map((item)=>( |
|||
|
|||
<PassengerTable key={item.id} data={item} /> |
|||
))} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default PassengersList; |
@ -1,50 +0,0 @@ |
|||
import React from "react"; |
|||
import { FC } from "react"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
import ButtonSecondary from "@/shared/ButtonSecondary"; |
|||
import { Route } from "@/routers/types"; |
|||
|
|||
export interface CommonLayoutProps { |
|||
children: React.ReactNode; |
|||
params: { |
|||
stepIndex: string; |
|||
}; |
|||
} |
|||
|
|||
const CommonLayout: FC<CommonLayoutProps> = ({ children, params }) => { |
|||
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"; |
|||
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"> |
|||
<div> |
|||
<span className="text-4xl font-semibold">{index}</span>{" "} |
|||
<span className="text-lg text-neutral-500 dark:text-neutral-400"> |
|||
/ 10 |
|||
</span> |
|||
</div> |
|||
|
|||
{/* --------------------- */} |
|||
<div className="listingSection__wrap ">{children}</div> |
|||
|
|||
{/* --------------------- */} |
|||
<div className="flex justify-end space-x-5"> |
|||
<ButtonSecondary href={backtHref}>Go back</ButtonSecondary> |
|||
<ButtonPrimary href={nextHref}> |
|||
{nextBtnText || "Continue"} |
|||
</ButtonPrimary> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default CommonLayout; |
@ -1,61 +1,71 @@ |
|||
import React from "react"; |
|||
"use client" |
|||
|
|||
import React, { useState } 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 PageAddListing10 from "./PageAddListing10"; |
|||
import PageAddListing2 from "./PageAddListing2"; |
|||
import PageAddListing3 from "./PageAddListing3"; |
|||
import PageAddListing4 from "./PageAddListing4"; |
|||
import PageAddListing5 from "./PageAddListing5"; |
|||
import PageAddListing6 from "./PageAddListing6"; |
|||
import PageAddListing7 from "./PageAddListing7"; |
|||
import PageAddListing8 from "./PageAddListing8"; |
|||
import PageAddListing9 from "./PageAddListing9"; |
|||
|
|||
const Page = ({ |
|||
params, |
|||
searchParams, |
|||
}: { |
|||
params: { stepIndex: string }; |
|||
searchParams?: { [key: string]: string | string[] | undefined }; |
|||
}) => { |
|||
let ContentComponent = PageAddListing1; |
|||
switch (Number(params.stepIndex)) { |
|||
case 1: |
|||
ContentComponent = PageAddListing1; |
|||
break; |
|||
case 2: |
|||
ContentComponent = PageAddListing2; |
|||
break; |
|||
case 3: |
|||
ContentComponent = PageAddListing3; |
|||
break; |
|||
case 4: |
|||
ContentComponent = PageAddListing4; |
|||
break; |
|||
case 5: |
|||
ContentComponent = PageAddListing5; |
|||
break; |
|||
case 6: |
|||
ContentComponent = PageAddListing6; |
|||
break; |
|||
case 7: |
|||
ContentComponent = PageAddListing7; |
|||
break; |
|||
case 8: |
|||
ContentComponent = PageAddListing8; |
|||
break; |
|||
case 9: |
|||
ContentComponent = PageAddListing9; |
|||
break; |
|||
case 10: |
|||
ContentComponent = PageAddListing10; |
|||
break; |
|||
|
|||
default: |
|||
ContentComponent = PageAddListing1; |
|||
break; |
|||
} |
|||
|
|||
return <ContentComponent />; |
|||
import { setHttpClientAndAgentOptions } from "next/dist/server/config"; |
|||
|
|||
export interface CommonLayoutProps { |
|||
children: React.ReactNode; |
|||
params: { |
|||
stepIndex: string; |
|||
}; |
|||
} |
|||
|
|||
const CommonLayout: FC<CommonLayoutProps> = ({ params }) => { |
|||
|
|||
const [passengers , setPassengers] =useState([]) |
|||
const [newPassenger , setNewPassenger] =useState({ |
|||
fullName :"", |
|||
date : "" , |
|||
number : "", |
|||
passport : "", |
|||
image : "" |
|||
}) |
|||
|
|||
const nextHandler =()=>{ |
|||
setPassengers((prev) => [...prev, newPassenger]); |
|||
|
|||
} |
|||
console.log(newPassenger); |
|||
console.log(passengers); |
|||
|
|||
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"; |
|||
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"> |
|||
<div> |
|||
<span className="text-4xl font-semibold">{index}</span>{" "} |
|||
<span className="text-lg text-neutral-500 dark:text-neutral-400"> |
|||
/ 10 |
|||
</span> |
|||
</div> |
|||
|
|||
{/* --------------------- */} |
|||
<div className="listingSection__wrap "><PageAddListing1 Passenger={newPassenger} setNewPassenger ={setNewPassenger} /></div> |
|||
|
|||
{/* --------------------- */} |
|||
<div className="flex justify-end space-x-5"> |
|||
{index > 1 && <ButtonSecondary href={backtHref}>Go back</ButtonSecondary>} |
|||
<ButtonPrimary onClick={nextHandler} href={nextHref}> |
|||
{nextBtnText || "Continue"} |
|||
</ButtonPrimary> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Page; |
|||
export default CommonLayout; |
@ -0,0 +1,227 @@ |
|||
"use client"; |
|||
|
|||
import React, { useState } from "react"; |
|||
import { FC } from "react"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
import Input from "@/shared/Input"; |
|||
import FormItem from "../add-listing/FormItem"; |
|||
import getImageURL from "@/components/api/getImageURL"; |
|||
import axiosInstance from "@/components/api/axios"; |
|||
import { useRouter } from "next/navigation"; |
|||
|
|||
export interface CommonLayoutProps { |
|||
params: { |
|||
stepIndex: string; |
|||
}; |
|||
} |
|||
|
|||
const CommonLayout: FC<CommonLayoutProps> = () => { |
|||
const user = JSON.parse(localStorage.getItem("user")); |
|||
|
|||
const router = useRouter(); |
|||
const [passenger, setPassenger] = useState({ |
|||
name: "", |
|||
passport: "", |
|||
number: "", |
|||
date: "", |
|||
image: "", |
|||
}); |
|||
const [errors, setErrors] = useState({ |
|||
name: "", |
|||
passport: "", |
|||
number: "", |
|||
date: "", |
|||
image: "", |
|||
}); |
|||
|
|||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { |
|||
const file = e.target.files[0]; |
|||
if (file) { |
|||
const image = await getImageURL(file); |
|||
setPassenger((prev) => ({ ...prev, image: image.url })); |
|||
setErrors((prev) => ({ ...prev, image: "" })); // Clear image error
|
|||
} |
|||
}; |
|||
|
|||
const validateForm = () => { |
|||
let formIsValid = true; |
|||
let errors = { |
|||
name: "", |
|||
passport: "", |
|||
number: "", |
|||
date: "", |
|||
image: "", |
|||
}; |
|||
|
|||
if (!passenger.name) { |
|||
formIsValid = false; |
|||
errors.name = "Full Name is required."; |
|||
} |
|||
|
|||
if (!passenger.passport) { |
|||
formIsValid = false; |
|||
errors.passport = "Passport Number is required."; |
|||
} else if (!/^\d+$/.test(passenger.passport)) { |
|||
formIsValid = false; |
|||
errors.passport = "Passport Number must be numeric."; |
|||
} |
|||
|
|||
if (!passenger.date) { |
|||
formIsValid = false; |
|||
errors.date = "Date of Birth is required."; |
|||
} |
|||
|
|||
if (!passenger.number) { |
|||
formIsValid = false; |
|||
errors.number = "Phone Number is required."; |
|||
} else if (!/^\d+$/.test(passenger.number)) { |
|||
formIsValid = false; |
|||
errors.number = "Phone Number must be numeric."; |
|||
} |
|||
|
|||
if (!passenger.image) { |
|||
formIsValid = false; |
|||
errors.image = "Passport image is required."; |
|||
} |
|||
|
|||
setErrors(errors); |
|||
return formIsValid; |
|||
}; |
|||
|
|||
const handleSavePassenger = async (passenger) => { |
|||
if (!validateForm()) return; // Validate before saving
|
|||
|
|||
try { |
|||
const response = await axiosInstance.post( |
|||
`/api/account/passengers/`, |
|||
{ |
|||
fullname: passenger.name, |
|||
passport_number: passenger.passport, |
|||
birthdate: passenger.date, |
|||
phone_number: passenger.number, |
|||
passport_image: passenger.image, |
|||
}, |
|||
{ |
|||
headers: { |
|||
Authorization: `token ${user.token}`, |
|||
}, |
|||
} |
|||
); |
|||
|
|||
console.log(response.data); |
|||
router.push("/passengers-list"); |
|||
} catch (error) { |
|||
console.error("Error saving passenger:", error); |
|||
} |
|||
}; |
|||
|
|||
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">Passengers Information</h2> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
{/* FORM */} |
|||
<div className="space-y-8"> |
|||
{/* ITEM */} |
|||
|
|||
<FormItem label="Full Name" desc=""> |
|||
<Input |
|||
required |
|||
value={passenger.name} |
|||
onChange={(e) => { |
|||
setPassenger((prev) => ({ |
|||
...prev, |
|||
name: e.target.value, |
|||
})); |
|||
setErrors((prev) => ({ ...prev, name: "" })); // Clear error on input change
|
|||
}} |
|||
placeholder="Full Name" |
|||
/> |
|||
{errors.name && <p className="text-red-500 text-xs">{errors.name}</p>} |
|||
</FormItem> |
|||
|
|||
<FormItem label="Passport Number" desc=""> |
|||
<Input |
|||
required |
|||
value={passenger.passport} |
|||
onChange={(e) => { |
|||
setPassenger((prev) => ({ |
|||
...prev, |
|||
passport: e.target.value, |
|||
})); |
|||
setErrors((prev) => ({ ...prev, passport: "" })); // Clear error on input change
|
|||
}} |
|||
type="number" |
|||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" |
|||
placeholder="Passport Number" |
|||
/> |
|||
{errors.passport && <p className="text-red-500 text-xs">{errors.passport}</p>} |
|||
</FormItem> |
|||
|
|||
<FormItem label="Date of Birth" desc=""> |
|||
<Input |
|||
required |
|||
value={passenger.date} |
|||
onChange={(e) => { |
|||
setPassenger((prev) => ({ |
|||
...prev, |
|||
date: e.target.value, |
|||
})); |
|||
setErrors((prev) => ({ ...prev, date: "" })); // Clear error on input change
|
|||
}} |
|||
type="date" |
|||
placeholder="Date of Birth" |
|||
/> |
|||
{errors.date && <p className="text-red-500 text-xs">{errors.date}</p>} |
|||
</FormItem> |
|||
|
|||
<FormItem label="Phone Number" desc=""> |
|||
<Input |
|||
required |
|||
value={passenger.number} |
|||
onChange={(e) => { |
|||
setPassenger((prev) => ({ |
|||
...prev, |
|||
number: e.target.value, |
|||
})); |
|||
setErrors((prev) => ({ ...prev, number: "" })); // Clear error on input change
|
|||
}} |
|||
type="number" |
|||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" |
|||
placeholder="Phone Number" |
|||
/> |
|||
{errors.number && <p className="text-red-500 text-xs">{errors.number}</p>} |
|||
</FormItem> |
|||
|
|||
<FormItem label="Upload Passport Image Here" desc=""> |
|||
<Input |
|||
required |
|||
onChange={handleFileChange} |
|||
type="file" |
|||
placeholder="Passport" |
|||
/> |
|||
{errors.image && <p className="text-red-500 text-xs">{errors.image}</p>} |
|||
</FormItem> |
|||
</div> |
|||
</> |
|||
</div> |
|||
|
|||
{/* --------------------- */} |
|||
<div className="flex justify-end space-x-5"> |
|||
<ButtonPrimary onClick={(e) => { e.preventDefault(); handleSavePassenger(passenger); }}> |
|||
Continue |
|||
</ButtonPrimary> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default CommonLayout; |
@ -1,26 +0,0 @@ |
|||
import NextAuth from "next-auth"; |
|||
import GithubProvider from "next-auth/providers/github"; |
|||
import GoogleProvider from "next-auth/providers/google"; |
|||
if (!process.env.GOOGLE_ID || !process.env.GOOGLE_SECRET || !process.env.GITHUB_ID || !process.env.GITHUB_SECRET) { |
|||
throw new Error("The environment variables GOOGLE_ID, GOOGLE_SECRET, GITHUB_ID, and GITHUB_SECRET must be set."); |
|||
} |
|||
export const authOptions = { |
|||
// Configure one or more authentication providers
|
|||
providers: [ |
|||
GithubProvider({ |
|||
clientId: process.env.GITHUB_ID, |
|||
clientSecret: process.env.GITHUB_SECRET, |
|||
}), |
|||
GoogleProvider({ |
|||
clientId: process.env.GOOGLE_ID, |
|||
clientSecret: process.env.GOOGLE_SECRET, |
|||
}), |
|||
// ...add more providers here
|
|||
], |
|||
|
|||
pages: { |
|||
signIn: "/login", |
|||
}, |
|||
}; |
|||
|
|||
export default NextAuth(authOptions); |
@ -1,3 +0,0 @@ |
|||
export async function GET(request: Request) { |
|||
return new Response('Hello, Next.js!') |
|||
} |
@ -0,0 +1,151 @@ |
|||
"use client"; |
|||
|
|||
import React, { FC, useContext, useState } from "react"; |
|||
import facebookSvg from "@/images/Facebook.svg"; |
|||
import twitterSvg from "@/images/Twitter.svg"; |
|||
import googleSvg from "@/images/Google.svg"; |
|||
import Input from "@/shared/Input"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
import Image from "next/image"; |
|||
import Link from "next/link"; |
|||
import axiosInstance from "@/components/api/axios"; |
|||
import { user } from "@/components/contexts/userContext"; |
|||
import useFormValidation from "@/hooks/FormValidation"; |
|||
import { useRouter } from "next/navigation"; |
|||
|
|||
export interface PageSignUpProps {} |
|||
|
|||
const PageSignUp: FC<PageSignUpProps> = ({}) => { |
|||
|
|||
const router = useRouter() |
|||
|
|||
const { setForm , setMethod } = useContext(user); |
|||
|
|||
const [name, setName] = useState(''); |
|||
const [countryCode, setCountryCode] = useState(''); |
|||
const [phoneNumber, setPhoneNumber] = useState(''); |
|||
const [password, setPassword] = useState(''); |
|||
const [confirmPassword, setConfirmPassword] = useState(''); |
|||
|
|||
const { errors, validateForm } = useFormValidation(); |
|||
|
|||
const countryCodeHandler = (e) => { |
|||
if (e.target.value.length <= 3) { |
|||
setCountryCode(e.target.value); |
|||
} |
|||
}; |
|||
|
|||
const submitHandler = async ( |
|||
name, |
|||
countryCode, |
|||
phoneNumber, |
|||
password, |
|||
confirmPassword |
|||
) => { |
|||
const form = { |
|||
name, |
|||
countryCode, |
|||
phoneNumber, |
|||
password, |
|||
confirmPassword, |
|||
}; |
|||
|
|||
if (validateForm(form)) { |
|||
setForm(form); |
|||
console.log('Form is valid, submitting:', form); |
|||
await axiosInstance |
|||
.get(`/api/account/verfication/?range_phone=${countryCode}&phone_number=${phoneNumber}`) |
|||
.then((response) => { |
|||
setMethod(response.data.verification_method) |
|||
console.log(response.data.verification_method); |
|||
}) |
|||
.catch((error) => { |
|||
console.error("Error fetching data:", error); |
|||
}); |
|||
router.push("/signup/methodes") |
|||
} else { |
|||
console.log('Form has errors:', errors); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className={`nc-PageSignUp`}> |
|||
<div className="container mb-24 lg:mb-32"> |
|||
<h2 className="my-20 flex items-center text-3xl leading-[115%] md:text-5xl md:leading-[115%] font-semibold text-neutral-900 dark:text-neutral-100 justify-center"> |
|||
Change Password |
|||
</h2> |
|||
<div className="max-w-md mx-auto space-y-6"> |
|||
{/* FORM */} |
|||
<form className="grid grid-cols-1 gap-6" action="#" method="post"> |
|||
<label className="block"> |
|||
<span className="text-neutral-800 dark:text-neutral-200">Phone Number</span> |
|||
<div className="flex items-center mt-1 rounded-2xl border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:ring-0"> |
|||
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">+</span> |
|||
<input |
|||
value={countryCode} |
|||
onChange={countryCodeHandler} |
|||
type="text" |
|||
placeholder="98" |
|||
maxLength={3} |
|||
className="w-[50px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none p-2 mr-[-10px] text-center border-none outline-none focus:ring-0 focus:border-none bg-transparent text-neutral-800 dark:text-neutral-200" |
|||
/> |
|||
<span className="px-2 text-neutral-500">|</span> |
|||
<input |
|||
value={phoneNumber} |
|||
onChange={(e) => setPhoneNumber(e.target.value)} |
|||
type="text" |
|||
placeholder="26363687" |
|||
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 p-2 border-none outline-none focus:ring-0 focus:border-none bg-transparent text-neutral-800 dark:text-neutral-200" |
|||
/> |
|||
</div> |
|||
{errors.countryCode && <p className="text-xs text-red-600">{errors.countryCode}</p>} |
|||
{errors.phoneNumber && <p className="text-xs text-red-600">{errors.phoneNumber}</p>} |
|||
</label> |
|||
<label className="block"> |
|||
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200"> |
|||
Password |
|||
</span> |
|||
<Input |
|||
value={password} |
|||
onChange={(e) => setPassword(e.target.value)} |
|||
type="text" |
|||
className="secure-input unselectable-input mt-1" |
|||
onCopy={(e)=>{ |
|||
e.preventDefault() |
|||
}} |
|||
/> |
|||
{errors.password && <p className="text-xs text-red-600">{errors.password}</p>} |
|||
</label> |
|||
<label className="block"> |
|||
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200"> |
|||
Confirm Password |
|||
</span> |
|||
<Input |
|||
value={confirmPassword} |
|||
onChange={(e) => setConfirmPassword(e.target.value)} |
|||
placeholder="Password" |
|||
type="text" |
|||
className="secure-input unselectable-input mt-1" |
|||
onCopy={(e)=>{ |
|||
e.preventDefault() |
|||
}} |
|||
/> |
|||
{errors.confirmPassword && <p className="text-xs text-red-600">{errors.confirmPassword}</p>} |
|||
</label> |
|||
<ButtonPrimary |
|||
onClick={(e) => { |
|||
e.preventDefault(); |
|||
submitHandler(name, countryCode, phoneNumber, password, confirmPassword); |
|||
}} |
|||
> |
|||
Continue |
|||
</ButtonPrimary> |
|||
</form> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default PageSignUp; |
@ -1,3 +1,8 @@ |
|||
@tailwind base; |
|||
@tailwind components; |
|||
@tailwind utilities; |
|||
.secure-input { |
|||
font-family: 'PasswordDots'; |
|||
font-size: 16px; |
|||
-webkit-text-security: disc; |
|||
} |
55
src/app/page.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,150 @@ |
|||
"use client"; |
|||
|
|||
import axiosInstance from "@/components/api/axios"; |
|||
import { user } from "@/components/contexts/userContext"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
import { useRouter } from "next/navigation"; |
|||
import { useContext, useState } from "react"; |
|||
import { FaWhatsapp } from "react-icons/fa"; |
|||
import { MdOutlineTextsms } from "react-icons/md"; |
|||
|
|||
function SelectMethods() { |
|||
const router = useRouter(); |
|||
let user = JSON.parse(localStorage.getItem("user")); |
|||
if (user) { |
|||
return router.replace("/"); |
|||
} |
|||
const { method, form } = useContext(user); |
|||
const [selectedMethod, setSelectedMethod] = useState(""); |
|||
const [error, setError] = useState(""); |
|||
const [loading, setLoading] = useState(""); |
|||
|
|||
const enabled = { |
|||
watsapp: method.join().includes("watsapp"), |
|||
sms: method.join().includes("sms"), |
|||
}; |
|||
|
|||
const handleMethodChange = (e) => { |
|||
if (!enabled.watsapp && !enabled.sms) { |
|||
setSelectedMethod(""); |
|||
} else if (!enabled.watsapp) { |
|||
setSelectedMethod("sms"); |
|||
} else if (!enabled.sms) { |
|||
setSelectedMethod("whatsapp"); |
|||
} else { |
|||
setSelectedMethod(e.target.value); |
|||
} |
|||
}; |
|||
console.log(method); |
|||
|
|||
const handleSubmit = async () => { |
|||
setLoading(true); |
|||
try { |
|||
const response = await axiosInstance.post( |
|||
`/api/account/register/`, |
|||
{ |
|||
fullname: form.name, |
|||
phone_number: form.phoneNumber, |
|||
verification_method: selectedMethod, |
|||
range_phone: form.countryCode, |
|||
password: form.password, |
|||
password_confirmation: form.confirmPassword, |
|||
}, |
|||
{ |
|||
headers: { |
|||
Accept: "application/json", |
|||
}, |
|||
} |
|||
); |
|||
console.log(response); |
|||
response.status === 202 && |
|||
(console.log("sucsses"), |
|||
setLoading(false), |
|||
router.replace("signup/otp-code")); |
|||
} catch (error) { |
|||
setError(error); |
|||
console.log(error); |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className="w-[550px] container mb-24 lg:mb-32 p-4 space-y-4"> |
|||
<h2 className="my-20 flex items-center text-3xl leading-[115%] md:text-5xl md:leading-[115%] font-semibold text-neutral-900 dark:text-neutral-100 justify-center"> |
|||
Verification Method |
|||
</h2> |
|||
|
|||
<form className="grid grid-cols-1 gap-6"> |
|||
<div |
|||
className={`${ |
|||
!enabled.watsapp ? "opacity-40" : "" |
|||
} h-15 flex items-center justify-between p-3 border rounded-xl shadow-sm bg-white dark:bg-neutral-800`}
|
|||
> |
|||
<div className="flex items-center"> |
|||
<FaWhatsapp className="text-xl mr-3" /> |
|||
<label |
|||
htmlFor="whatsapp" |
|||
className={`${ |
|||
!enabled.watsapp ? "cursor-not-allowed" : "cursor-pointer" |
|||
} text-neutral-800 dark:text-neutral-200 font-medium`}
|
|||
> |
|||
Send via WhatsApp |
|||
</label> |
|||
</div> |
|||
<input |
|||
type="radio" |
|||
id="whatsapp" |
|||
name="method" |
|||
value="whatsapp" |
|||
checked={selectedMethod === "whatsapp"} |
|||
onChange={(e) => handleMethodChange(e)} |
|||
disabled={!enabled.watsapp} |
|||
className="cursor-pointer form-radio accent-black text-black focus:ring-primary-500 focus:ring-2" |
|||
/> |
|||
</div> |
|||
|
|||
<div |
|||
className={`${ |
|||
!enabled.sms ? "opacity-40" : "" |
|||
} h-15 flex items-center justify-between p-3 border rounded-xl shadow-sm bg-white dark:bg-neutral-800`}
|
|||
> |
|||
<div className="flex items-center"> |
|||
<MdOutlineTextsms className="text-xl mr-3" /> |
|||
<label |
|||
htmlFor="sms" |
|||
className={`${ |
|||
!enabled.sms ? "cursor-not-allowed" : "cursor-pointer" |
|||
} text-neutral-800 dark:text-neutral-200 font-medium`}
|
|||
> |
|||
Send via SMS |
|||
</label> |
|||
</div> |
|||
<input |
|||
type="radio" |
|||
id="sms" |
|||
name="method" |
|||
value="sms" |
|||
checked={selectedMethod === "sms"} |
|||
onChange={(e) => handleMethodChange(e)} |
|||
disabled={!enabled.sms} |
|||
className="cursor-pointer form-radio accent-black text-black focus:ring-primary-500 focus:ring-2" |
|||
/> |
|||
</div> |
|||
|
|||
{/* Continue Button */} |
|||
{error && <p className="text-xs text-red-500">{error.message}</p>} |
|||
<ButtonPrimary |
|||
loading={loading} |
|||
onClick={(e) => { |
|||
e.preventDefault(); |
|||
handleSubmit(); |
|||
}} |
|||
> |
|||
Continue |
|||
</ButtonPrimary> |
|||
</form> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default SelectMethods; |
@ -0,0 +1,132 @@ |
|||
"use client"; |
|||
|
|||
import React, { FC, useContext, useState, useEffect, useRef } from "react"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
import axiosInstance from "@/components/api/axios"; |
|||
import { user as userContext } from "@/components/contexts/userContext"; |
|||
import useFormValidation from "@/hooks/FormValidation"; |
|||
import { useRouter } from "next/navigation"; |
|||
|
|||
export interface PageSignUpProps {} |
|||
|
|||
const PageSignUp: FC<PageSignUpProps> = () => { |
|||
const router = useRouter(); |
|||
const user = JSON.parse(localStorage.getItem("user")); |
|||
const { form, setMethod, setStatus } = useContext(userContext); |
|||
|
|||
|
|||
const [otp, setOtp] = useState(["", "", "", "", ""]); |
|||
const [time, setTime] = useState(30); |
|||
const [error, setError] = useState(""); |
|||
const otpRefs = useRef<(HTMLInputElement | null)[]>([]); |
|||
|
|||
useEffect(() => { |
|||
if (user) { |
|||
router.replace("/"); |
|||
} |
|||
}, [user, router]); |
|||
|
|||
const handleOtpChange = (value: string, index: number) => { |
|||
if (/^[0-9]?$/.test(value)) { |
|||
const newOtp = [...otp]; |
|||
newOtp[index] = value; |
|||
setOtp(newOtp); |
|||
if (value && index < otpRefs.current.length - 1) { |
|||
otpRefs.current[index + 1]?.focus(); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => { |
|||
if (e.key === "Backspace" && !otp[index] && index > 0) { |
|||
otpRefs.current[index - 1]?.focus(); |
|||
} |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
if (time > 0) { |
|||
const timer = setInterval(() => setTime((prevTime) => prevTime - 1), 1000); |
|||
return () => clearInterval(timer); |
|||
} |
|||
}, [time]); |
|||
|
|||
const handleResend = () => { |
|||
if (time === 0) { |
|||
setTime(30); |
|||
// Add logic to resend OTP here if needed
|
|||
} |
|||
}; |
|||
|
|||
const submitHandler = async () => { |
|||
setError(""); |
|||
try { |
|||
const response = await axiosInstance.post("/api/account/verify/", { |
|||
method: "register", |
|||
phone_number: form.phoneNumber, |
|||
code: otp.join(""), |
|||
}); |
|||
|
|||
if (response.status === 201) { |
|||
localStorage.setItem("user", JSON.stringify(response.data)); |
|||
setStatus(true); |
|||
router.replace("/"); |
|||
} else { |
|||
setError("Something went wrong. Please try again."); |
|||
} |
|||
} catch (error) { |
|||
setError(error.response?.data?.message || "An error occurred."); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className={`nc-PageSignUp`}> |
|||
<div className="container mb-24 lg:mb-32"> |
|||
<h2 className="my-10 text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100"> |
|||
Verification Code |
|||
</h2> |
|||
<p className="text-center text-sm text-neutral-500 mb-4"> |
|||
Enter the 5-digit code that we sent to complete your account registration |
|||
</p> |
|||
<div className="max-w-sm mx-auto space-y-6"> |
|||
<div className="flex justify-center space-x-2 mb-4"> |
|||
{otp.map((value, index) => ( |
|||
<input |
|||
key={index} |
|||
ref={(el) => (otpRefs.current[index] = el)} |
|||
type="text" |
|||
maxLength={1} |
|||
value={value} |
|||
onChange={(e) => handleOtpChange(e.target.value, index)} |
|||
onKeyDown={(e) => handleKeyDown(e, index)} |
|||
className="w-12 h-12 border rounded-lg text-center text-lg font-semibold border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-2 focus:ring-primary-500" |
|||
/> |
|||
))} |
|||
</div> |
|||
<p className="text-center text-sm text-neutral-500 mb-4"> |
|||
Haven't got the confirmation code yet?{" "} |
|||
<button |
|||
className={`text-primary-600 hover:underline ${time > 0 ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`} |
|||
onClick={handleResend} |
|||
disabled={time > 0} |
|||
> |
|||
Resend |
|||
</button> |
|||
{time > 0 && <span className="text-xs text-neutral-400">({time} Seconds)</span>} |
|||
</p> |
|||
{error && <p className="text-red-500 text-xs">{error}</p>} |
|||
<ButtonPrimary |
|||
className="w-full h-12 text-white font-semibold rounded-full" |
|||
onClick={(e) => { |
|||
e.preventDefault(); |
|||
submitHandler(); |
|||
}} |
|||
> |
|||
Confirm |
|||
</ButtonPrimary> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default PageSignUp; |
@ -0,0 +1,94 @@ |
|||
"use client"; |
|||
|
|||
import React, { Fragment, FC, useState, useContext } from "react"; |
|||
import { Popover, Transition } from "@headlessui/react"; |
|||
import NcInputNumber from "@/components/NcInputNumber"; |
|||
import { UserPlusIcon } from "@heroicons/react/24/outline"; |
|||
import ClearDataButton from "@/app/(client-components)/(HeroSearchForm)/ClearDataButton"; |
|||
import { GuestsObject } from "@/app/(client-components)/type"; |
|||
import { Context } from "@/components/contexts/tourDetails"; |
|||
|
|||
export interface GuestsInputProps { |
|||
className?: string; |
|||
} |
|||
|
|||
const GuestsInput: FC<GuestsInputProps> = ({ className = "flex-1" }) => { |
|||
const { setPassengers, passengers } = useContext(Context); |
|||
|
|||
const [guestAdultsInputValue, setGuestAdultsInputValue] = |
|||
useState(passengers); |
|||
|
|||
const handleChangeData = (value: number) => { |
|||
let newValue = { |
|||
guestAdults: guestAdultsInputValue, |
|||
}; |
|||
setGuestAdultsInputValue(value); |
|||
newValue.guestAdults = value; |
|||
setPassengers(value); |
|||
}; |
|||
|
|||
const totalGuests = guestAdultsInputValue; |
|||
|
|||
return ( |
|||
<Popover className={`flex relative ${className}`}> |
|||
{({ open }) => ( |
|||
<> |
|||
<div |
|||
className={`flex-1 flex items-center focus:outline-none rounded-b-3xl ${ |
|||
open ? "shadow-lg" : "" |
|||
}`}
|
|||
> |
|||
<Popover.Button |
|||
className={`relative z-10 flex-1 flex text-left items-center p-3 space-x-3 focus:outline-none`} |
|||
> |
|||
<div className="text-neutral-300 dark:text-neutral-400"> |
|||
<UserPlusIcon className="w-5 h-5 lg:w-7 lg:h-7" /> |
|||
</div> |
|||
<div className="flex-grow"> |
|||
<span className="block xl:text-lg font-semibold"> |
|||
{totalGuests || ""} Guests |
|||
</span> |
|||
<span className="block mt-1 text-sm text-neutral-400 leading-none font-light"> |
|||
{totalGuests ? "Guests" : "Add guests"} |
|||
</span> |
|||
</div> |
|||
|
|||
{!!totalGuests && open && ( |
|||
<ClearDataButton |
|||
onClick={() => { |
|||
setGuestAdultsInputValue(0); |
|||
setPassengers(0); |
|||
}} |
|||
/> |
|||
)} |
|||
</Popover.Button> |
|||
</div> |
|||
|
|||
<Transition |
|||
as={Fragment} |
|||
enter="transition ease-out duration-200" |
|||
enterFrom="opacity-0 translate-y-1" |
|||
enterTo="opacity-100 translate-y-0" |
|||
leave="transition ease-in duration-150" |
|||
leaveFrom="opacity-100 translate-y-0" |
|||
leaveTo="opacity-0 translate-y-1" |
|||
> |
|||
<Popover.Panel className="absolute right-0 z-10 w-full sm:min-w-[340px] max-w-sm bg-white dark:bg-neutral-800 top-full mt-3 py-5 sm:py-6 px-4 sm:px-8 rounded-3xl shadow-xl ring-1 ring-black ring-opacity-5 "> |
|||
<NcInputNumber |
|||
className="w-full" |
|||
defaultValue={guestAdultsInputValue} |
|||
onChange={(value) => handleChangeData(value)} |
|||
max={10} |
|||
min={1} |
|||
label="Passsengers" |
|||
desc="Ages 13 or above" |
|||
/> |
|||
</Popover.Panel> |
|||
</Transition> |
|||
</> |
|||
)} |
|||
</Popover> |
|||
); |
|||
}; |
|||
|
|||
export default GuestsInput; |
@ -0,0 +1,69 @@ |
|||
"use client"; |
|||
|
|||
import React, { Fragment, useState, FC } from "react"; |
|||
import { Popover, Transition } from "@headlessui/react"; |
|||
import { CalendarIcon } from "@heroicons/react/24/outline"; |
|||
import DatePickerCustomHeaderTwoMonth from "@/components/DatePickerCustomHeaderTwoMonth"; |
|||
import DatePickerCustomDay from "@/components/DatePickerCustomDay"; |
|||
import DatePicker from "react-datepicker"; |
|||
import ClearDataButton from "@/app/(client-components)/(HeroSearchForm)/ClearDataButton"; |
|||
|
|||
export interface StayDatesRangeInputProps { |
|||
className?: string; |
|||
} |
|||
|
|||
const StayDatesRangeInput: FC<StayDatesRangeInputProps> = ({ |
|||
className = "flex-1", |
|||
details |
|||
|
|||
}) => { |
|||
const [startDate, setStartDate] = useState<Date | null>( |
|||
new Date("2023/02/06") |
|||
); |
|||
const [endDate, setEndDate] = useState<Date | null>(new Date("2023/02/23")); |
|||
//
|
|||
|
|||
const onChangeDate = (dates: [Date | null, Date | null]) => { |
|||
const [start, end] = dates; |
|||
setStartDate(start); |
|||
setEndDate(end); |
|||
}; |
|||
|
|||
const renderInput = () => { |
|||
return ( |
|||
<> |
|||
<div className="text-neutral-300 dark:text-neutral-400"> |
|||
<CalendarIcon className="w-5 h-5 lg:w-7 lg:h-7" /> |
|||
</div> |
|||
<div className="flex-grow text-left"> |
|||
<span className="block xl:text-lg font-semibold"> |
|||
{details?.started_at.replaceAll("-", "/") || "Tour period"} |
|||
{details?.ended_at && |
|||
" - " + details?.ended_at.replaceAll("-", "/")} |
|||
</span> |
|||
<span className="block mt-1 text-sm text-neutral-400 leading-none font-light"> |
|||
{"Starts - End"} |
|||
</span> |
|||
</div> |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
return ( |
|||
<Popover className={`StayDatesRangeInput z-10 relative flex ${className}`}> |
|||
{({ open }) => ( |
|||
<> |
|||
<div |
|||
className={`flex-1 flex relative p-3 items-center space-x-3 focus:outline-none `} |
|||
> |
|||
{renderInput()} |
|||
|
|||
</div> |
|||
|
|||
</> |
|||
)} |
|||
</Popover> |
|||
); |
|||
}; |
|||
|
|||
export default StayDatesRangeInput; |
@ -0,0 +1,71 @@ |
|||
import { ListingGalleryImage } from "@/components/listing-image-gallery/utils/types"; |
|||
|
|||
export const PHOTOS: string[] = [ |
|||
"https://images.pexels.com/photos/6129967/pexels-photo-6129967.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260", |
|||
"https://images.pexels.com/photos/7163619/pexels-photo-7163619.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/6527036/pexels-photo-6527036.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/6969831/pexels-photo-6969831.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/6438752/pexels-photo-6438752.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/1320686/pexels-photo-1320686.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/261394/pexels-photo-261394.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/2861361/pexels-photo-2861361.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/2677398/pexels-photo-2677398.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260", |
|||
"https://images.pexels.com/photos/1365425/pexels-photo-1365425.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/914128/pexels-photo-914128.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/840667/pexels-photo-840667.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/732632/pexels-photo-732632.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/450062/pexels-photo-450062.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/917510/pexels-photo-917510.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/1194233/pexels-photo-1194233.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/236973/pexels-photo-236973.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/1392099/pexels-photo-1392099.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/547116/pexels-photo-547116.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/1002272/pexels-photo-1002272.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/917511/pexels-photo-917511.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/771079/pexels-photo-771079.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
"https://images.pexels.com/photos/13461077/pexels-photo-13461077.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load", |
|||
"https://images.pexels.com/photos/9074921/pexels-photo-9074921.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load", |
|||
"https://images.pexels.com/photos/9336042/pexels-photo-9336042.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load", |
|||
"https://images.pexels.com/photos/5418318/pexels-photo-5418318.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load", |
|||
"https://images.pexels.com/photos/4815278/pexels-photo-4815278.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load", |
|||
"https://images.pexels.com/photos/1365425/pexels-photo-1365425.jpeg?auto=compress&cs=tinysrgb&w=1600", |
|||
]; |
|||
|
|||
export const Amenities_demos = [ |
|||
{ name: "la-key", icon: "la-key" }, |
|||
{ name: "la-luggage-cart", icon: "la-luggage-cart" }, |
|||
{ name: "la-shower", icon: "la-shower" }, |
|||
{ name: "la-smoking", icon: "la-smoking" }, |
|||
{ name: "la-snowflake", icon: "la-snowflake" }, |
|||
{ name: "la-spa", icon: "la-spa" }, |
|||
{ name: "la-suitcase", icon: "la-suitcase" }, |
|||
{ name: "la-suitcase-rolling", icon: "la-suitcase-rolling" }, |
|||
{ name: "la-swimmer", icon: "la-swimmer" }, |
|||
{ name: "la-swimming-pool", icon: "la-swimming-pool" }, |
|||
{ name: "la-tv", icon: "la-tv" }, |
|||
{ name: "la-umbrella-beach", icon: "la-umbrella-beach" }, |
|||
{ name: "la-utensils", icon: "la-utensils" }, |
|||
{ name: "la-wheelchair", icon: "la-wheelchair" }, |
|||
{ name: "la-wifi", icon: "la-wifi" }, |
|||
{ name: "la-baby-carriage", icon: "la-baby-carriage" }, |
|||
{ name: "la-bath", icon: "la-bath" }, |
|||
{ name: "la-bed", icon: "la-bed" }, |
|||
{ name: "la-briefcase", icon: "la-briefcase" }, |
|||
{ name: "la-car", icon: "la-car" }, |
|||
{ name: "la-cocktail", icon: "la-cocktail" }, |
|||
{ name: "la-coffee", icon: "la-coffee" }, |
|||
{ name: "la-concierge-bell", icon: "la-concierge-bell" }, |
|||
{ name: "la-dice", icon: "la-dice" }, |
|||
{ name: "la-dumbbell", icon: "la-dumbbell" }, |
|||
{ name: "la-hot-tub", icon: "la-hot-tub" }, |
|||
{ name: "la-infinity", icon: "la-infinity" }, |
|||
]; |
|||
|
|||
export const imageGallery: ListingGalleryImage[] = [...PHOTOS].map( |
|||
(item, index): ListingGalleryImage => { |
|||
return { |
|||
id: index, |
|||
url: item, |
|||
}; |
|||
} |
|||
); |
@ -0,0 +1,648 @@ |
|||
"use client"; |
|||
|
|||
import React, { FC, Fragment, useContext, useEffect, useState } from "react"; |
|||
import { Dialog, Transition } from "@headlessui/react"; |
|||
import { ArrowRightIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; |
|||
import CommentListing from "@/components/CommentListing"; |
|||
import FiveStartIconForRate from "@/components/FiveStartIconForRate"; |
|||
import StartRating from "@/components/StartRating"; |
|||
import Avatar from "@/shared/Avatar"; |
|||
import Badge from "@/shared/Badge"; |
|||
import ButtonCircle from "@/shared/ButtonCircle"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
import ButtonSecondary from "@/shared/ButtonSecondary"; |
|||
import ButtonClose from "@/shared/ButtonClose"; |
|||
import Input from "@/shared/Input"; |
|||
import LikeSaveBtns from "@/components/LikeSaveBtns"; |
|||
import Image from "next/image"; |
|||
import { useParams, usePathname, useRouter } from "next/navigation"; |
|||
import { Amenities_demos, PHOTOS } from "./constant"; |
|||
import StayDatesRangeInput from "./StayDatesRangeInput"; |
|||
import GuestsInput from "./GuestsInput"; |
|||
import { Route } from "next"; |
|||
import { Context } from "@/components/contexts/tourDetails"; |
|||
|
|||
export interface ListingStayDetailPageProps {} |
|||
|
|||
const ListingStayDetailPage: FC<ListingStayDetailPageProps> = ({}) => { |
|||
//
|
|||
|
|||
const { getTourData, details, passengers } = useContext(Context); |
|||
const r = /-?(\d+)$/; |
|||
const id: number = useParams().slug.match(r)[1]; |
|||
|
|||
useEffect(() => { |
|||
getTourData(id); |
|||
}, []); |
|||
|
|||
console.log(details); |
|||
|
|||
let [isOpenModalAmenities, setIsOpenModalAmenities] = useState(false); |
|||
|
|||
const thisPathname = usePathname(); |
|||
const router = useRouter(); |
|||
|
|||
function closeModalAmenities() { |
|||
setIsOpenModalAmenities(false); |
|||
} |
|||
|
|||
function openModalAmenities() { |
|||
setIsOpenModalAmenities(true); |
|||
} |
|||
|
|||
const handleOpenModalImageGallery = () => { |
|||
router.push(`${thisPathname}/?modal=PHOTO_TOUR_SCROLLABLE` as Route); |
|||
}; |
|||
|
|||
const renderSection1 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap !space-y-6"> |
|||
{/* 1 */} |
|||
<div className="flex justify-between items-center"> |
|||
<Badge name="Wooden house" /> |
|||
<LikeSaveBtns /> |
|||
</div> |
|||
|
|||
{/* 2 */} |
|||
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-semibold"> |
|||
{details?.title} |
|||
</h2> |
|||
|
|||
{/* 3 */} |
|||
<div className="flex items-center space-x-4"> |
|||
<StartRating /> |
|||
<span>·</span> |
|||
<span> |
|||
<i className="las la-map-marker-alt"></i> |
|||
<span className="ml-1"> Tokyo, Jappan</span> |
|||
</span> |
|||
</div> |
|||
|
|||
{/* 4 */} |
|||
<div className="flex items-center"> |
|||
<Avatar hasChecked sizeClass="h-10 w-10" radius="rounded-full" /> |
|||
<span className="ml-2.5 text-neutral-500 dark:text-neutral-400"> |
|||
Hosted by{" "} |
|||
<span className="text-neutral-900 dark:text-neutral-200 font-medium"> |
|||
Kevin Francis |
|||
</span> |
|||
</span> |
|||
</div> |
|||
|
|||
{/* 5 */} |
|||
<div className="w-full border-b border-neutral-100 dark:border-neutral-700" /> |
|||
|
|||
{/* 6 */} |
|||
<div className="flex items-center justify-between xl:justify-start space-x-8 xl:space-x-12 text-sm text-neutral-700 dark:text-neutral-300"> |
|||
<div className="flex items-center space-x-3 "> |
|||
<i className=" las la-user text-2xl "></i> |
|||
<span className=""> |
|||
6 <span className="hidden sm:inline-block">guests</span> |
|||
</span> |
|||
</div> |
|||
<div className="flex items-center space-x-3"> |
|||
<i className=" las la-bed text-2xl"></i> |
|||
<span className=" "> |
|||
6 <span className="hidden sm:inline-block">beds</span> |
|||
</span> |
|||
</div> |
|||
<div className="flex items-center space-x-3"> |
|||
<i className=" las la-bath text-2xl"></i> |
|||
<span className=" "> |
|||
3 <span className="hidden sm:inline-block">baths</span> |
|||
</span> |
|||
</div> |
|||
<div className="flex items-center space-x-3"> |
|||
<i className=" las la-door-open text-2xl"></i> |
|||
<span className=" "> |
|||
2 <span className="hidden sm:inline-block">bedrooms</span> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
const renderSection2 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
<h2 className="text-2xl font-semibold">Stay information</h2> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
<div className="text-neutral-6000 dark:text-neutral-300"> |
|||
<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> |
|||
); |
|||
}; |
|||
|
|||
const renderSection3 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
<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> |
|||
</div> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
{/* 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 key={item.id} className="flex items-center space-x-3"> |
|||
<span className=" ">{item.title}</span> |
|||
</div> |
|||
))} |
|||
</div> |
|||
|
|||
{console.log(details)} |
|||
<div className="w-14 border-b border-neutral-200"></div> |
|||
<div> |
|||
<ButtonSecondary onClick={openModalAmenities}> |
|||
View more 20 amenities |
|||
</ButtonSecondary> |
|||
</div> |
|||
{renderMotalAmenities()} |
|||
</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" |
|||
> |
|||
​ |
|||
</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 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
{/* HEADING */} |
|||
<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> |
|||
</div> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
{/* CONTENT */} |
|||
<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> |
|||
<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> |
|||
); |
|||
}; |
|||
|
|||
const renderSection5 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
{/* HEADING */} |
|||
<h2 className="text-2xl font-semibold">Host Information</h2> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
|
|||
{/* host */} |
|||
<div className="flex items-center space-x-4"> |
|||
<Avatar |
|||
hasChecked |
|||
hasCheckedClass="w-4 h-4 -top-0.5 right-0.5" |
|||
sizeClass="h-14 w-14" |
|||
radius="rounded-full" |
|||
/> |
|||
<div> |
|||
<a className="block text-xl font-medium" href="##"> |
|||
Kevin Francis |
|||
</a> |
|||
<div className="mt-1.5 flex items-center text-sm text-neutral-500 dark:text-neutral-400"> |
|||
<StartRating /> |
|||
<span className="mx-2">·</span> |
|||
<span> 12 places</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* desc */} |
|||
<span className="block text-neutral-6000 dark:text-neutral-300"> |
|||
{details?.description} |
|||
</span> |
|||
|
|||
{/* info */} |
|||
<div className="block text-neutral-500 dark:text-neutral-400 space-y-2.5"> |
|||
<div className="flex items-center space-x-3"> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="h-6 w-6" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
strokeWidth={1.5} |
|||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" |
|||
/> |
|||
</svg> |
|||
<span>Joined in March 2016</span> |
|||
</div> |
|||
<div className="flex items-center space-x-3"> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="h-6 w-6" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
strokeWidth={1.5} |
|||
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" |
|||
/> |
|||
</svg> |
|||
<span>Response rate - 100%</span> |
|||
</div> |
|||
<div className="flex items-center space-x-3"> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="h-6 w-6" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
strokeWidth={1.5} |
|||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" |
|||
/> |
|||
</svg> |
|||
|
|||
<span>Fast response - within a few hours</span> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* == */} |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
<div> |
|||
<ButtonSecondary href="/author">See host profile</ButtonSecondary> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
const renderSection6 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
{/* HEADING */} |
|||
<h2 className="text-2xl font-semibold">Reviews (23 reviews)</h2> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
|
|||
{/* Content */} |
|||
<div className="space-y-5"> |
|||
<FiveStartIconForRate iconClass="w-6 h-6" className="space-x-0.5" /> |
|||
<div className="relative"> |
|||
<Input |
|||
fontClass="" |
|||
sizeClass="h-16 px-4 py-3" |
|||
rounded="rounded-3xl" |
|||
placeholder="Share your thoughts ..." |
|||
/> |
|||
<ButtonCircle |
|||
className="absolute right-2 top-1/2 transform -translate-y-1/2" |
|||
size=" w-12 h-12 " |
|||
> |
|||
<ArrowRightIcon className="w-5 h-5" /> |
|||
</ButtonCircle> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* comment */} |
|||
<div className="divide-y divide-neutral-100 dark:divide-neutral-800"> |
|||
<CommentListing className="py-8" /> |
|||
<CommentListing className="py-8" /> |
|||
<CommentListing className="py-8" /> |
|||
<CommentListing className="py-8" /> |
|||
<div className="pt-8"> |
|||
<ButtonSecondary>View more 20 reviews</ButtonSecondary> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
const renderSection7 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
{/* HEADING */} |
|||
<div> |
|||
<h2 className="text-2xl font-semibold">Location</h2> |
|||
<span className="block mt-2 text-neutral-500 dark:text-neutral-400"> |
|||
San Diego, CA, United States of America (SAN-San Diego Intl.) |
|||
</span> |
|||
</div> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700" /> |
|||
|
|||
{/* MAP */} |
|||
<div className="aspect-w-5 aspect-h-5 sm:aspect-h-3 ring-1 ring-black/10 rounded-xl z-0"> |
|||
<div className="rounded-xl overflow-hidden z-0"> |
|||
<iframe |
|||
width="100%" |
|||
height="100%" |
|||
loading="lazy" |
|||
allowFullScreen |
|||
referrerPolicy="no-referrer-when-downgrade" |
|||
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyAGVJfZMAKYfZ71nzL_v5i3LjTTWnCYwTY&q=Iran+Mashhad+Imam Reza Holy Shrine" |
|||
></iframe> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
const renderSection8 = () => { |
|||
return ( |
|||
<div className="listingSection__wrap"> |
|||
{/* 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> |
|||
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700" /> |
|||
|
|||
{/* CONTENT */} |
|||
<div> |
|||
<h4 className="text-lg font-semibold">Special Note</h4> |
|||
<div className="prose sm:prose"> |
|||
<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> |
|||
</> |
|||
)) |
|||
} |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
const renderSidebar = () => { |
|||
return ( |
|||
<div className="listingSectionSidebar__wrap shadow-xl"> |
|||
{/* PRICE */} |
|||
<div className="flex justify-between"> |
|||
<span className="text-3xl font-semibold"> |
|||
{details?.price} |
|||
{/* <span className="ml-1 text-base font-normal text-neutral-500 dark:text-neutral-400"> |
|||
/night |
|||
</span> */} |
|||
</span> |
|||
<StartRating /> |
|||
</div> |
|||
|
|||
{/* FORM */} |
|||
<form className="flex flex-col border border-neutral-200 dark:border-neutral-700 rounded-3xl "> |
|||
<StayDatesRangeInput details={details} className="flex-1 z-[11]" /> |
|||
<div className="w-full border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
<GuestsInput className="flex-1" /> |
|||
</form> |
|||
|
|||
{/* SUM */} |
|||
<div className="flex flex-col space-y-4"> |
|||
<div className="flex justify-between text-neutral-6000 dark:text-neutral-300"> |
|||
<span> |
|||
{details?.price} x {passengers} passengers |
|||
</span> |
|||
<span>{details?.price * passengers}</span> |
|||
</div> |
|||
<div className="flex justify-between text-neutral-6000 dark:text-neutral-300"> |
|||
<span>Service charge</span> |
|||
<span>$0</span> |
|||
</div> |
|||
<div className="border-b border-neutral-200 dark:border-neutral-700"></div> |
|||
<div className="flex justify-between font-semibold"> |
|||
<span>Total</span> |
|||
<span>{details?.price * passengers}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* SUBMIT */} |
|||
<ButtonPrimary href={"/checkout"}>Reserve</ButtonPrimary> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
return ( |
|||
<div className="nc-ListingStayDetailPage"> |
|||
{/* HEADER */} |
|||
<header className="rounded-md sm:rounded-xl"> |
|||
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2"> |
|||
<div |
|||
className="col-span-2 row-span-3 sm:row-span-2 relative rounded-md sm:rounded-xl overflow-hidden cursor-pointer" |
|||
onClick={handleOpenModalImageGallery} |
|||
> |
|||
<Image |
|||
fill |
|||
className="object-cover rounded-md sm:rounded-xl" |
|||
src={details?.images[0]?.image_url.lg} |
|||
alt="" |
|||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 50vw" |
|||
/> |
|||
|
|||
<div className="absolute inset-0 bg-neutral-900 bg-opacity-20 opacity-0 hover:opacity-100 transition-opacity"></div> |
|||
</div> |
|||
{details?.images.filter((_, i) => i >= 1 && i < 5).map((item, index) => ( |
|||
<div |
|||
key={index} |
|||
className={`relative rounded-md sm:rounded-xl overflow-hidden ${ |
|||
index >= 3 ? "hidden sm:block" : "" |
|||
}`}
|
|||
> |
|||
<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" |
|||
/> |
|||
{console.log(details)} |
|||
</div> |
|||
|
|||
{/* OVERLAY */} |
|||
<div |
|||
className="absolute inset-0 bg-neutral-900 bg-opacity-20 opacity-0 hover:opacity-100 transition-opacity cursor-pointer" |
|||
onClick={handleOpenModalImageGallery} |
|||
/> |
|||
</div> |
|||
))} |
|||
|
|||
{/* <button |
|||
className="absolute hidden md:flex md:items-center md:justify-center left-3 bottom-3 px-4 py-2 rounded-xl bg-neutral-100 text-neutral-500 hover:bg-neutral-200 z-10" |
|||
onClick={handleOpenModalImageGallery} |
|||
> |
|||
<Squares2X2Icon className="w-5 h-5" /> |
|||
<span className="ml-2 text-neutral-800 text-sm font-medium"> |
|||
Show all photos |
|||
</span> |
|||
</button> */} |
|||
</div> |
|||
</header> |
|||
|
|||
{/* MAIN */} |
|||
<main className=" relative z-10 mt-11 flex flex-col lg:flex-row "> |
|||
{/* CONTENT */} |
|||
<div className="w-full lg:w-3/5 xl:w-2/3 space-y-8 lg:space-y-10 lg:pr-10"> |
|||
{renderSection1()} |
|||
{renderSection2()} |
|||
{renderSection3()} |
|||
{renderSection4()} |
|||
{renderSection5()} |
|||
{/* {renderSection6()} */} |
|||
{renderSection7()} |
|||
{renderSection8()} |
|||
</div> |
|||
|
|||
{/* SIDEBAR */} |
|||
<div className="hidden lg:block flex-grow mt-14 lg:mt-0"> |
|||
<div className="sticky top-28">{renderSidebar()}</div> |
|||
</div> |
|||
</main> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ListingStayDetailPage; |
@ -0,0 +1,70 @@ |
|||
"use client"; |
|||
|
|||
import BackgroundSection from "@/components/BackgroundSection"; |
|||
import ListingImageGallery from "@/components/listing-image-gallery/ListingImageGallery"; |
|||
import SectionSliderNewCategories from "@/components/SectionSliderNewCategories"; |
|||
import SectionSubscribe2 from "@/components/SectionSubscribe2"; |
|||
import { usePathname, useRouter, useSearchParams } from "next/navigation"; |
|||
import React, { ReactNode } from "react"; |
|||
import MobileFooterSticky from "../(listing-detail)/(components)/MobileFooterSticky"; |
|||
import { imageGallery as listingStayImageGallery } from "./[slug]/constant"; |
|||
import { Route } from "next"; |
|||
|
|||
const DetailtLayout = ({ children }: { children: ReactNode }) => { |
|||
const router = useRouter(); |
|||
const thisPathname = usePathname(); |
|||
const searchParams = useSearchParams(); |
|||
const modal = searchParams?.get("modal"); |
|||
|
|||
const handleCloseModalImageGallery = () => { |
|||
let params = new URLSearchParams(document.location.search); |
|||
params.delete("modal"); |
|||
router.push(`${thisPathname}/?${params.toString()}` as Route); |
|||
}; |
|||
|
|||
const getImageGalleryListing = () => { |
|||
if (thisPathname?.includes("/listing-stay-detail")) { |
|||
return listingStayImageGallery; |
|||
} |
|||
if (thisPathname?.includes("/listing-car-detail")) { |
|||
return listingCarImageGallery; |
|||
} |
|||
if (thisPathname?.includes("/listing-experiences-detail")) { |
|||
return listingExperienceImageGallery; |
|||
} |
|||
|
|||
return []; |
|||
}; |
|||
|
|||
return ( |
|||
<div className="ListingDetailPage"> |
|||
<ListingImageGallery |
|||
isShowModal={modal === "PHOTO_TOUR_SCROLLABLE"} |
|||
onClose={handleCloseModalImageGallery} |
|||
images={getImageGalleryListing()} |
|||
/> |
|||
|
|||
<div className="container ListingDetailPage__content">{children}</div> |
|||
|
|||
{/* OTHER SECTION */} |
|||
<div className="container py-24 lg:py-32"> |
|||
<div className="relative py-16"> |
|||
<BackgroundSection /> |
|||
<SectionSliderNewCategories |
|||
heading="Explore by types of stays" |
|||
subHeading="Explore houses based on 10 types of stays" |
|||
categoryCardType="card5" |
|||
itemPerRow={5} |
|||
sliderStyle="style2" |
|||
/> |
|||
</div> |
|||
{/* <SectionSubscribe2 className="pt-24 lg:pt-32" /> */} |
|||
</div> |
|||
|
|||
{/* STICKY FOOTER MOBILE */} |
|||
<MobileFooterSticky /> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default DetailtLayout; |
9
src/components/CardCategory3.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,69 @@ |
|||
import React, { FC } from "react"; |
|||
import rightImgPng from "@/images/our-features.png"; |
|||
import Image, { StaticImageData } from "next/image"; |
|||
import Badge from "@/shared/Badge"; |
|||
import ButtonPrimary from "@/shared/ButtonPrimary"; |
|||
|
|||
export interface SectionCustomTourProps { |
|||
className?: string; |
|||
rightImg?: StaticImageData; |
|||
type?: "type1" | "type2"; |
|||
} |
|||
console.log("fsafs"); |
|||
|
|||
const SectionCustomTour: FC<SectionCustomTourProps> = ({ |
|||
className = "lg:py-14", |
|||
rightImg = rightImgPng, |
|||
type = "type1", |
|||
}) => { |
|||
return ( |
|||
<div |
|||
className={`nc-SectionCustomTour relative flex flex-col items-center ${ |
|||
type === "type1" ? "lg:flex-row" : "lg:flex-row-reverse" |
|||
} ${className}`}
|
|||
data-nc-id="SectionCustomTour" |
|||
> |
|||
<div className="flex-grow"> |
|||
<Image src={rightImg} alt="" /> |
|||
</div> |
|||
<div |
|||
className={`max-w-2xl flex-shrink-0 mt-10 lg:mt-0 lg:w-2/5 ${ |
|||
type === "type1" ? "lg:pl-16" : "lg:pr-16" |
|||
}`}
|
|||
> |
|||
|
|||
<h2 className="font-semibold text-4xl mt-5">Custoum Tour </h2> |
|||
|
|||
<ul className="space-y-10 mt-16"> |
|||
<li className="space-y-4"> |
|||
<span className="block text-xl font-semibold"> |
|||
Cost-effective advertising |
|||
</span> |
|||
<span className="block mt-5 text-neutral-500 dark:text-neutral-400"> |
|||
With a free listing, you can advertise your rental with no upfront |
|||
costs |
|||
</span> |
|||
</li> |
|||
<li className="space-y-4"> |
|||
|
|||
<span className="block mt-5 text-neutral-500 dark:text-neutral-400"> |
|||
Millions of people are searching for unique places to stay around |
|||
the world |
|||
</span> |
|||
</li> |
|||
<li className="space-y-4"> |
|||
<span className="block mt-5 text-neutral-500 dark:text-neutral-400"> |
|||
A Holiday Lettings listing gives you a secure and easy way to take |
|||
bookings and payments online |
|||
</span> |
|||
</li> |
|||
<li className="space-y-4"> |
|||
<ButtonPrimary>Custoum Tour</ButtonPrimary> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default SectionCustomTour; |
@ -0,0 +1,31 @@ |
|||
import axios from "axios"; |
|||
|
|||
const axiosInstance= axios.create({ |
|||
baseURL : "https://aqila.nwhco.ir/" |
|||
}) |
|||
|
|||
axiosInstance.interceptors.request.use( |
|||
(config) => { |
|||
// Log the request headers
|
|||
console.log('Request Headers:', config); |
|||
return config; |
|||
}, |
|||
(error) => { |
|||
// Handle the request error
|
|||
console.error('Request Error:', error); |
|||
return Promise.reject(error); |
|||
} |
|||
); |
|||
|
|||
// You can also add a response interceptor if needed
|
|||
axiosInstance.interceptors.response.use( |
|||
(response) => response, |
|||
(error) => { |
|||
// Handle response errors
|
|||
console.error('Response Error:', error); |
|||
return Promise.reject(error); |
|||
} |
|||
); |
|||
|
|||
|
|||
export default axiosInstance; |
@ -0,0 +1,36 @@ |
|||
import axiosInstance from "./axios"; |
|||
|
|||
const getImageURL = async (file) => { |
|||
let image ; |
|||
if (file) { |
|||
const modifiedFileName = file.name.replaceAll(" ", ""); |
|||
const modifiedFile = new File([file], modifiedFileName, { |
|||
type: file.type, |
|||
lastModified: file.lastModified, |
|||
}); |
|||
|
|||
const formData = new FormData(); |
|||
formData.append("file", modifiedFile); |
|||
|
|||
try { |
|||
const response = await axiosInstance.post( |
|||
`/api/upload-tmp-media/`, |
|||
formData, |
|||
{ |
|||
headers: { |
|||
"Content-Type": "multipart/form-data", |
|||
}, |
|||
} |
|||
); |
|||
if (response.status === 200) { |
|||
image = response.data; |
|||
} else { |
|||
console.log("Something went wrong during file upload"); |
|||
} |
|||
} catch (error) { |
|||
console.log(error.message); |
|||
} |
|||
} |
|||
return image |
|||
}; |
|||
export default getImageURL; |
@ -0,0 +1,73 @@ |
|||
"use client"; |
|||
import axiosInstance from "../api/axios"; |
|||
import React, { createContext, useEffect, useState } from "react"; |
|||
|
|||
export const Context = createContext(); |
|||
|
|||
export const ContextProvider = ({ children }) => { |
|||
const [details, setDetails] = useState(); |
|||
const [passengers, setPassengers] = useState(0); |
|||
const [tours, setTours] = useState([]); |
|||
const [countries, setCountries] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
axiosInstance |
|||
.get("/api/cityguide/countries/") |
|||
.then((response) => { |
|||
setCountries(response.data.results); |
|||
}) |
|||
.catch((error) => { |
|||
console.error("Error fetching data:", error); |
|||
}); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
axiosInstance |
|||
.get("/api/tours/") |
|||
.then((response) => { |
|||
setTours(response.data); |
|||
}) |
|||
.catch((error) => { |
|||
console.error("Error fetching data:", error); |
|||
}); |
|||
}, []); |
|||
|
|||
console.log(tours); |
|||
|
|||
const getTourData = async (item) => { |
|||
await axiosInstance |
|||
.get(`/api/tours/${item}/`) |
|||
.then((response) => { |
|||
setDetails(response.data); |
|||
}) |
|||
.catch((error) => { |
|||
console.error("Error fetching data:", error); |
|||
}); |
|||
// console.log(details);
|
|||
// await axiosInstance
|
|||
// .get(`/api/tours/${item}/passengers/`)
|
|||
// .then((response) => {
|
|||
// setPassengers(response.data);
|
|||
// })
|
|||
// .catch((error) => {
|
|||
// console.error("Error fetching data:", error);
|
|||
// });
|
|||
// console.log(passengers);
|
|||
}; |
|||
|
|||
return ( |
|||
<Context.Provider |
|||
value={{ |
|||
details, |
|||
passengers, |
|||
getTourData, |
|||
setPassengers, |
|||
setDetails, |
|||
tours, |
|||
countries, |
|||
}} |
|||
> |
|||
{children} |
|||
</Context.Provider> |
|||
); |
|||
}; |
@ -0,0 +1,26 @@ |
|||
"use client"; |
|||
import axiosInstance from "../api/axios"; |
|||
import React, { createContext, useEffect, useState } from "react"; |
|||
|
|||
export const user = createContext(); |
|||
|
|||
export const UserProvider = ({ children }) => { |
|||
const [status, setStatus] = useState(false); |
|||
const [form, setForm] = useState({}); |
|||
const [method, setMethod] = useState([]); |
|||
|
|||
return ( |
|||
<user.Provider |
|||
value={{ |
|||
status, |
|||
setStatus, |
|||
form, |
|||
setForm, |
|||
method, |
|||
setMethod, |
|||
}} |
|||
> |
|||
{children} |
|||
</user.Provider> |
|||
); |
|||
}; |
21
src/data/jsons/__stayListing.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,49 @@ |
|||
// hooks/FormValidation.js
|
|||
import { useState } from 'react'; |
|||
|
|||
// Validation function for the signup form
|
|||
const useFormValidation = () => { |
|||
const [errors, setErrors] = useState({}); |
|||
|
|||
// Validate form fields
|
|||
const validateForm = (form) => { |
|||
let newErrors = {}; |
|||
|
|||
// Full Name validation
|
|||
if (!form.name) { |
|||
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)) { |
|||
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)) { |
|||
newErrors.phoneNumber = 'Phone Number is required and must be a number'; |
|||
} |
|||
|
|||
// Password validation
|
|||
if (!form.password) { |
|||
newErrors.password = 'Password is required'; |
|||
} else if (form.password.length < 6) { |
|||
newErrors.password = 'Password must be at least 6 characters'; |
|||
} |
|||
|
|||
// Confirm Password validation
|
|||
if (!form.confirmPassword) { |
|||
newErrors.confirmPassword = 'Confirm Password is required'; |
|||
} else if (form.password !== form.confirmPassword) { |
|||
newErrors.confirmPassword = 'Passwords do not match'; |
|||
} |
|||
|
|||
setErrors(newErrors); |
|||
// Return true if no errors
|
|||
return Object.keys(newErrors).length === 0; |
|||
}; |
|||
|
|||
return { errors, validateForm }; |
|||
}; |
|||
|
|||
export default useFormValidation; |
After Width: 1001 | Height: 1031 | Size: 1.3 MiB |
Before Width: 1001 | Height: 1031 | Size: 1.3 MiB After Width: 1001 | Height: 1029 | Size: 1.9 MiB |
After Width: 113 | Height: 151 | Size: 15 KiB |
11
src/images/logos/لوگو3 1.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue