Browse Source

feat(localization): add new translations for navigation and form fields in multiple languages

main
sina_sajjadi 2 weeks ago
parent
commit
634c40ff93
  1. 11
      public/locales/ar/common.json
  2. 4
      public/locales/ar/form.json
  3. 6
      public/locales/ar/navigation.json
  4. 11
      public/locales/en/common.json
  5. 3
      public/locales/en/form.json
  6. 6
      public/locales/en/navigation.json
  7. 11
      public/locales/id/common.json
  8. 4
      public/locales/id/form.json
  9. 6
      public/locales/id/navigation.json
  10. 11
      public/locales/ru/common.json
  11. 7
      public/locales/ru/navigation.json
  12. 17
      src/app/[locale]/(account-pages)/bills/BillCard.tsx
  13. 231
      src/app/[locale]/(account-pages)/bills/[slug]/page.tsx
  14. 2
      src/app/[locale]/(account-pages)/bills/page.tsx
  15. 301
      src/app/[locale]/(account-pages)/passengers-list/page.tsx
  16. 12
      src/app/[locale]/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx
  17. 731
      src/app/[locale]/(stay-listings)/TabFilters.tsx
  18. 6
      src/app/[locale]/add-listing/[[...stepIndex]]/PageAddListing1.tsx
  19. 4
      src/app/[locale]/custom-trip/page.tsx
  20. 8
      src/app/[locale]/layout.tsx
  21. 124
      src/app/[locale]/tours/TabFilters.tsx
  22. 30
      src/app/[locale]/tours/[slug]/page.tsx
  23. 6
      src/components/Footer.tsx
  24. 2
      src/components/contexts/tourDetails.tsx
  25. 18
      src/shared/Navigation/NavMobile.tsx
  26. 2
      src/shared/SocialsList1.tsx

11
public/locales/ar/common.json

@ -218,5 +218,14 @@
"noChanges": "لم يتم اكتشاف أي تغييرات.",
"errorGeneric": "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
"errorUnknown": "حدث خطأ غير معروف.",
"benefits": "الفوائد"
"benefits": "الفوائد",
"Type of place": "نوع المكان",
"Rooms and Beds": "الغرف والأسِرَّة",
"Beds": "أسِرَّة",
"Bedrooms": "غرف النوم",
"Bathrooms": "الحمامات",
"$": "$",
"apply": "تطبيق",
"moreFilters": "المزيد من الفلاتر",
"clear": "مسح"
}

4
public/locales/ar/form.json

@ -87,5 +87,7 @@
"uploadedImage": "الصورة المرفوعة",
"delete": "حذف",
"currentReceipt": "الإيصال الحالي",
"copy": "نسخ"
"copy": "نسخ",
"uploadPassword" : "قم برفع صورة جواز السفر هنا"
}

6
public/locales/ar/navigation.json

@ -11,5 +11,9 @@
"navAccount": "الحساب",
"navMyTrips": "رحلاتي",
"navPassengersList": "قائمة الركاب",
"navBills": "الفواتير"
"navBills": "الفواتير",
"discover-articles": "اكتشف أبرز المقالات حول جميع مواضيع الحياة. اكتب قصصك وشاركها",
"custom-trip": "رحلة مخصصة",
"Where to?": "إلى أين؟",
"Anywhere • Any week • Add guests": "في أي مكان • أي أسبوع • أضف الضيوف"
}

11
public/locales/en/common.json

@ -219,5 +219,14 @@
"noChanges": "No changes detected.",
"errorGeneric": "Something went wrong. Please try again.",
"errorUnknown": "An unknown error occurred.",
"benefits" : "BENEFITS"
"benefits" : "BENEFITS",
"Type of place": "Type of place",
"Rooms and Beds": "Rooms and Beds",
"Beds": "Beds",
"Bedrooms": "Bedrooms",
"Bathrooms": "Bathrooms",
"$": "$",
"apply" : "Apply",
"moreFilters": "More Filters",
"clear" : "Clear"
}

3
public/locales/en/form.json

@ -87,5 +87,6 @@
"uploadedImage": "Uploaded Image",
"delete": "Delete",
"currentReceipt": "Current Receipt",
"copy": "Copy"
"copy": "Copy",
"uploadPassword" : "Upload Passport Image Here"
}

6
public/locales/en/navigation.json

@ -11,5 +11,9 @@
"navAccount": "Account",
"navMyTrips": "My Trips",
"navPassengersList": "Passengers List",
"navBills": "Bills"
"navBills": "Bills",
"discover-articles": "Discover the most outstanding articles on all topics of life. Write your stories and share them",
"custom-trip": "Custom Trip",
"Where to?": "Where to?",
"Anywhere • Any week • Add guests": "Anywhere • Any week • Add guests"
}

11
public/locales/id/common.json

@ -218,5 +218,14 @@
"noChanges": "Tidak ada perubahan yang terdeteksi.",
"errorGeneric": "Terjadi kesalahan. Silakan coba lagi.",
"errorUnknown": "Terjadi kesalahan yang tidak diketahui.",
"benefits": "MANFAAT"
"benefits": "MANFAAT",
"Type of place": "Jenis tempat",
"Rooms and Beds": "Kamar dan Tempat Tidur",
"Beds": "Tempat Tidur",
"Bedrooms": "Kamar Tidur",
"Bathrooms": "Kamar Mandi",
"$": "$",
"apply": "Terapkan",
"moreFilters": "Lebih Banyak Filter",
"clear": "Bersihkan"
}

4
public/locales/id/form.json

@ -87,5 +87,7 @@
"uploadedImage": "Gambar yang diunggah",
"delete": "Hapus",
"currentReceipt": "Tanda Terima Saat Ini",
"copy": "Salin"
"copy": "Salin",
"uploadPassword" : "Unggah Gambar Paspor Di Sini"
}

6
public/locales/id/navigation.json

@ -11,5 +11,9 @@
"navAccount": "Akun",
"navMyTrips": "Perjalanan Saya",
"navPassengersList": "Daftar Penumpang",
"navBills": "Tagihan"
"navBills": "Tagihan",
"discover-articles": "Temukan artikel paling menonjol tentang semua topik kehidupan. Tulis cerita Anda dan bagikan",
"custom-trip": "Perjalanan Khusus",
"Where to?": "Kemana?",
"Anywhere • Any week • Add guests": "Dimana saja • Minggu apa saja • Tambahkan tamu"
}

11
public/locales/ru/common.json

@ -218,5 +218,14 @@
"noChanges": "Изменения не обнаружены.",
"errorGeneric": "Произошла ошибка. Пожалуйста, попробуйте снова.",
"errorUnknown": "Произошла неизвестная ошибка.",
"benefits": "ПРЕИМУЩЕСТВА"
"benefits": "ПРЕИМУЩЕСТВА",
"Type of place": "Тип места",
"Rooms and Beds": "Комнаты и кровати",
"Beds": "Кровати",
"Bedrooms": "Спальни",
"Bathrooms": "Ванные комнаты",
"$": "$",
"apply": "Применить",
"moreFilters": "Больше фильтров",
"clear": "Очистить"
}

7
public/locales/ru/navigation.json

@ -11,5 +11,10 @@
"navAccount": "Аккаунт",
"navMyTrips": "Мои поездки",
"navPassengersList": "Список пассажиров",
"navBills": "Счета"
"navBills": "Счета",
"uploadPassword": "Загрузите изображение паспорта здесь",
"discover-articles": "Откройте для себя самые выдающиеся статьи на все темы жизни. Пишите свои истории и делитесь ими",
"custom-trip": "Индивидуальная поездка",
"Where to?": "Куда?",
"Anywhere • Any week • Add guests": "В любом месте • Любая неделя • Добавить гостей"
}

17
src/app/[locale]/(account-pages)/bills/BillCard.tsx

@ -37,7 +37,14 @@ interface BillCardProps {
bill: Bill;
onViewDetail: (bill: Bill) => void;
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
});
};
const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
const { t } = useTranslation("form");
@ -65,7 +72,7 @@ const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
};
return (
<div className="bg-white shadow-md rounded-lg p-4 mb-4 dark:bg-neutral-800">
<div onClick={() => onViewDetail(bill)} className="bg-white shadow-md rounded-lg p-4 mb-4 dark:bg-neutral-800">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center space-x-2">
{bill.service.includes("tour") && <BiSolidPlaneAlt size={25} />}
@ -80,11 +87,11 @@ const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
<div className="mt-4 text-sm text-gray-600">
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>{t("issuedDate")}:</span>
<span>{bill.created_at}</span>
<span>{formatDate(bill.created_at)}</span>
</div>
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>{t("expirationDate")}:</span>
<span>{bill.expiration_date}</span>
<span>{formatDate(bill.expiration_date)}</span>
</div>
<div className="flex justify-between mt-2 font-semibold dark:text-neutral-400">
<span>{t("tourInvoiceAmount")}:</span>
@ -92,7 +99,7 @@ const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
</div>
</div>
<ButtonPrimary onClick={() => onViewDetail(bill)} className="mt-4">
<ButtonPrimary className="mt-4">
{t("viewBill")}
</ButtonPrimary>
</div>

231
src/app/[locale]/(account-pages)/bills/[slug]/page.tsx

@ -1,6 +1,6 @@
"use client";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import axiosInstance from "@/components/api/axios";
import ButtonPrimary from "@/shared/ButtonPrimary";
import Button from "@/shared/Button";
@ -10,6 +10,9 @@ import getImageURL from "@/components/api/getImageURL";
import { toast, ToastContainer } from "react-toastify";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import { BiCloudUpload } from "react-icons/bi";
import { HiOutlineTrash } from "react-icons/hi";
import { useRouter } from "next/router";
export type BillStatus =
| "awaiting_payment"
@ -33,44 +36,59 @@ export interface Bill {
src: string;
};
detail_service: {
passenger_counts: { adults: string; children: string; infants: string };
passenger_data: {
adults: { count: number; names: string[] };
children: { count: number; names: string[] };
infants: { count: number; names: string[] };
};
};
transaction_receipt: string;
transaction_receipt: string | null;
card_number: string | number;
}
interface BillDetailCardProps {
bill: Bill;
billId: number;
}
const statusStyles: { [key in BillStatus]: JSX.Element } = {
awaiting_payment: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-yellow-100 text-yellow-700">
{"t('awaitingPayment')"}
</span>
),
approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
{"t('approved')"}
</span>
),
rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
{"t('rejected')"}
</span>
),
pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
{"t('pending')"}
</span>
),
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
day: "2-digit",
month: "short",
year: "numeric",
});
};
const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const BillDetailCard: React.FC<BillDetailCardProps> = ({ billId }) => {
const [bill, setBill] = useState<Bill | null>(null);
const [uploadedFile, setUploadedFile] = useState<string | null>(null);
const [loadingUpload, setLoadingUpload] = useState(false);
const [loading, setLoading] = useState(false);
const { user } = useUserContext();
const { t } = useTranslation("form");
const statusStyles: { [key in BillStatus]: JSX.Element } = {
awaiting_payment: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-yellow-100 text-yellow-700">
{t("awaitingPayment")}
</span>
),
approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
{t("approved")}
</span>
),
rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
{t("rejected")}
</span>
),
pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
{t("pending")}
</span>
),
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoadingUpload(true);
const file = e.target.files?.[0];
@ -80,13 +98,30 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
setLoadingUpload(false);
}
};
const getBillData = async () => {
try {
const response = await axiosInstance(`/api/factors/${billId}/`, {
headers: {
Authorization: `token ${user.token}`,
},
});
setBill(response.data);
} catch (error) {
console.error(error);
toast.error(t("errorFetchingBillData"));
}
};
useEffect(() => {
getBillData();
}, [billId]);
const handleSubmit = () => {
setLoading(true);
if (uploadedFile) {
axiosInstance
.patch(
`/api/factors/update/${bill.id}/`,
`/api/factors/update/${billId}/`,
{
transaction_receipt: uploadedFile,
},
@ -99,10 +134,10 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
.then(() => {
toast.success(t("transactionReceiptUpdated"));
setLoading(false);
setUploadedFile(null);
})
.catch((error) => {
console.log(error);
console.error(error);
toast.error(t("updateFailed"));
setLoading(false);
});
@ -111,45 +146,90 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
}
};
const handleDeleteFile = () => {
setLoading(true);
if (uploadedFile) {
setUploadedFile(null);
setLoading(false);
} else {
axiosInstance
.post(
`/api/factors/clear-receipt/${billId}/`,
{},
{
headers: {
Authorization: `token ${user.token}`,
},
}
)
.then(() => {
toast.success(t("transactionReceiptDeleted"));
})
.catch((error) => {
console.error(error);
toast.error(t("errorOccurred"));
})
.finally(() => {
setLoading(false);
useRouter().reload();
});
}
};
if (!bill) {
return <p>{t("loadingBillData")}</p>;
}
return (
<div className="bg-white shadow-md rounded-lg p-4 mb-4 dark:bg-neutral-800">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{bill.title}</h2>
<h2 className="text-lg font-semibold">
{bill.title || t("billDetails")}
</h2>
{statusStyles[bill.status]}
</div>
{bill.message && (
{bill.rejected_description && (
<div className="bg-red-100 text-red-700 p-3 mt-4 rounded-md">
<h3 className="font-semibold">{t("whyRejected")}</h3>
<p className="text-sm mt-1">{bill.message}</p>
<p className="text-sm mt-1">{bill.rejected_description}</p>
</div>
)}
<div className="mt-4 text-sm text-gray-600 dark:text-neutral-400">
<div className="flex justify-between mb-2">
<span>{t("issuedDate")}:</span>
<span>{bill.created_at}</span>
<span>{formatDate(bill.created_at)}</span>
</div>
<div className="flex justify-between mb-2">
<span>{t("expirationDate")}:</span>
<span>{bill.expiration_date}</span>
<span>{formatDate(bill.expiration_date)}</span>
</div>
<div className="mt-4">
<h3 className="font-semibold">{t("numberOfPassengers")}</h3>
<div className="flex justify-between">
{bill.detail_service.passenger_counts.adults && (
<span># {t("adult")}: {bill.detail_service.passenger_counts.adults}</span>
{!!bill.detail_service.passenger_data.adults.count && (
<span>
{bill.detail_service.passenger_data.adults.names.join(", ")} (
{t("adult")})
</span>
)}
</div>
<div className="flex justify-between">
{bill.detail_service.passenger_counts.children && (
<span># {t("children")}: {bill.detail_service.passenger_counts.children}</span>
{!!bill.detail_service.passenger_data.children.count && (
<span>
{bill.detail_service.passenger_data.children.names.join(", ")} (
{t("child")})
</span>
)}
</div>
<div className="flex justify-between">
{bill.detail_service.passenger_counts.infants && (
<span># {t("infant")}: {bill.detail_service.passenger_counts.infants}</span>
{!!bill.detail_service.passenger_data.infants.count && (
<span>
{bill.detail_service.passenger_data.infants.names.join(", ")} (
{t("infant")})
</span>
)}
</div>
</div>
@ -162,7 +242,9 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
<div className="mt-4">
<h3 className="font-semibold">{t("accountNumber")}</h3>
<div className="flex items-center bg-gray-100 p-2 rounded-md mt-2 dark:bg-neutral-900">
<span className="flex-grow text-gray text-lg">{bill.card_number}</span>
<span className="flex-grow text-gray text-lg">
{bill.card_number}
</span>
<Button className="text-blue-500">{t("copy")}</Button>
</div>
</div>
@ -177,8 +259,12 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
/>
<div className="flex justify-between mt-2">
<span>{bill.uploadedImage.name}</span>
<span className="text-gray-500 text-sm">{bill.uploadedImage.size}</span>
<Button className="text-red-500">{t("delete")}</Button>
<span className="text-gray-500 text-sm">
{bill.uploadedImage.size}
</span>
<Button className="text-red-500" onClick={handleDeleteFile}>
{t("delete")}
</Button>
</div>
</div>
) : (
@ -186,33 +272,48 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
<label className="block font-semibold mb-2">
{t("uploadPassportImage")}
</label>
<div className="flex items-center bg-gray-100 p-2 rounded-md dark:bg-neutral-900">
<Input
className="w-1 bg-white"
type="file"
onChange={handleFileChange}
/>
<div className="flex flex-col-reverse items-center bg-gray-100 p-2 rounded-md dark:bg-neutral-900 justify-between">
<div className="flex bg-white w-full items-center p-2 rounded-xl">
<Input
className="w-1 bg-white"
type="file"
onChange={handleFileChange}
/>
{(bill.transaction_receipt || uploadedFile) && (
<button onClick={handleDeleteFile}>
<HiOutlineTrash color="#99A1A4" size={30} />
</button>
)}
</div>
{loadingUpload && <p>{t("loading")}</p>}
{bill.transaction_receipt ? (
<span className="flex-grow text-gray-500 text-sm ml-4">
<Image
width={65}
height={65}
src={bill.transaction_receipt}
alt={t("currentReceipt")}
/>
</span>
) : (
<span className="flex-grow text-gray-500 text-sm ml-4">
{t("noFileSelected")}
</span>
)}
<div className="flex flex-col h-52">
{bill.transaction_receipt || uploadedFile ? (
<span className="flex-grow text-gray-500 text-sm ml-4">
<Image
width={200}
height={200}
src={
!!uploadedFile
? uploadedFile
: bill.transaction_receipt || ""
}
alt={t("currentReceipt")}
/>
</span>
) : (
<div className="flex flex-grow flex-col items-center text-gray-500 text-sm mt-14">
<BiCloudUpload size={36} />
<p>{t("uploadPassword")}</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
<ButtonPrimary
disabled={loading || !uploadedFile}
loading={loading}
className="w-full mt-6"
onClick={handleSubmit}
@ -235,4 +336,4 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
);
};
export default BillDetailCard;
export default BillDetailCard;

2
src/app/[locale]/(account-pages)/bills/page.tsx

@ -83,7 +83,7 @@ const BillsPage: React.FC = () => {
<button onClick={handleBackToList} className="text-blue-500 mb-4">
{t("backToBills")}
</button>
<BillDetailCard bill={selectedBill} />
<BillDetailCard billId={selectedBill.id} />
</div>
) : bills.length > 0 ? (
bills.map((bill, index) => (

301
src/app/[locale]/(account-pages)/passengers-list/page.tsx

@ -1,269 +1,68 @@
"use client";
"use client"
import React, { useEffect, useState } from "react";
import { FC } from "react";
import ButtonPrimary from "@/shared/ButtonPrimary";
import Input from "@/shared/Input";
import FormItem from "@/app/[locale]/add-listing/FormItem";
import getImageURL from "@/components/api/getImageURL";
import PassengerTable from "./PassengerTable";
import { IoPersonAddOutline } from "react-icons/io5";
import axiosInstance from "@/components/api/axios";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useUserContext } from "@/components/contexts/userContext";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
export interface CommonLayoutProps {
params: {
id: string;
};
}
const EditPassenger: FC<CommonLayoutProps> = ({ params }) => {
const { user } = useUserContext();
const router = useRouter();
const { t } = useTranslation("form");
const [passenger, setPassenger] = useState({
name: "",
passport: "",
number: "",
date: "",
image: "",
});
const [originalPassenger, setOriginalPassenger] = useState({
name: "",
passport: "",
number: "",
date: "",
image: "",
});
const [loading, setLoading] = useState(false);
// Fetch passenger data on component mount
useEffect(() => {
if (Object.keys(user).length) {
axiosInstance
.get(`/api/account/passengers/${params.id}/`, {
headers: {
Authorization: `token ${user.token}`,
},
})
.then((response) => {
const passengerData = {
name: response.data.fullname,
passport: response.data.passport_number,
date: response.data.birthdate,
number: response.data.phone_number.replace(/\D/g, ""),
image: response.data.passport_image,
};
setPassenger(passengerData);
setOriginalPassenger(passengerData); // Save original data for comparison
})
.catch((error) => {
toast.error(error.message || t("errorOccurred"));
});
}
}, [user, params.id]);
// Handle file change for uploading passport image
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoading(true);
const file = e.target.files?.[0];
if (file) {
try {
const image = await getImageURL(file);
setPassenger((prev) => ({ ...prev, image: image.url }));
toast.success(t("imageUploaded"));
} catch (error) {
toast.error(t("imageUploadError"));
} finally {
setLoading(false);
}
}
};
// Validate form inputs before saving
const validateForm = () => {
let formIsValid = true;
if (!passenger.name) {
formIsValid = false;
toast.error(t("fullNameRequired"));
}
if (!passenger.passport) {
formIsValid = false;
toast.error(t("passportRequired"));
} else if (!/^\d+$/.test(passenger.passport)) {
formIsValid = false;
toast.error(t("passportNumeric"));
}
if (!passenger.date) {
formIsValid = false;
toast.error(t("dobRequired"));
}
if (!passenger.number) {
formIsValid = false;
toast.error(t("phoneRequired"));
} else if (!/^\d+$/.test(passenger.number)) {
formIsValid = false;
toast.error(t("phoneNumeric"));
}
if (!passenger.image) {
formIsValid = false;
toast.error(t("passportImageRequired"));
}
return formIsValid;
};
// Save updated passenger details
const handleSavePassenger = async () => {
if (!validateForm()) return;
interface data {
birthdate: string;
fullname: string;
id: number;
passport_image: string;
phone_number: string;
passport_number: string;
const updatedFields: Partial<{
fullname: string;
passport_number: string;
birthdate: string;
phone_number: string;
passport_image: string;
}> = {};
// Only include changed fields
if (passenger.name !== originalPassenger.name) {
updatedFields.fullname = passenger.name;
}
if (passenger.passport !== originalPassenger.passport) {
updatedFields.passport_number = passenger.passport;
}
if (passenger.date !== originalPassenger.date) {
updatedFields.birthdate = passenger.date;
}
if (passenger.number !== originalPassenger.number) {
updatedFields.phone_number = passenger.number;
}
if (passenger.image !== originalPassenger.image) {
updatedFields.passport_image = passenger.image;
}
}
if (Object.keys(updatedFields).length === 0) {
toast.info(t("noChanges"));
return;
const PassengersList = () => {
const [passengers , setPassenger ] = useState([])
const {user} = useUserContext()
const router = useRouter()
const {t} = useTranslation("form")
useEffect(() => {
if (!Object.keys(user).length) {
router.replace("/signup");
}
}, [user, router]);
useEffect(()=>{
axiosInstance.get("/api/account/passengers/" ,{
headers :{
Authorization : `token ${user.token}`
}
})
.then((response)=>{
setPassenger(response.data.results);
try {
const response = await axiosInstance.patch(
`/api/account/passengers/${params.id}/`,
updatedFields,
{
headers: {
Authorization: `token ${user.token}`,
"Content-Type": "application/json",
},
}
);
}).catch((error)=>{
console.error(error);
})
} , [])
if (response.status === 200) {
toast.success(t("detailsUpdated"));
router.push("/passengers-list");
}
} catch (error) {
toast.error(t("updateFailed"));
}
};
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">{t("editPassengerInfo")}</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="space-y-8">
<FormItem label={t("fullName")} desc="">
<Input
required
value={passenger.name}
onChange={(e) =>
setPassenger((prev) => ({ ...prev, name: e.target.value }))
}
placeholder={t("enterFullName")}
/>
</FormItem>
<FormItem label={t("passportNumber")} desc="">
<Input
required
value={passenger.passport}
onChange={(e) =>
setPassenger((prev) => ({
...prev,
passport: e.target.value,
}))
}
type="text"
placeholder={t("enterPassportNumber")}
/>
</FormItem>
<FormItem label={t("dateOfBirth")} desc="">
<Input
required
value={passenger.date}
onChange={(e) =>
setPassenger((prev) => ({ ...prev, date: e.target.value }))
}
type="date"
placeholder={t("dobPlaceholder")}
/>
</FormItem>
<FormItem label={t("phoneNumber")} desc="">
<Input
required
value={passenger.number}
onChange={(e) =>
setPassenger((prev) => ({
...prev,
number: e.target.value.replace(/\D/g, ""),
}))
}
type="text"
placeholder={t("enterPhoneNumber")}
/>
</FormItem>
<FormItem label={t("passportImage")} desc="">
<Input
required
onChange={handleFileChange}
type="file"
placeholder={t("uploadImage")}
/>
{loading && <p>{t("loading")}</p>}
</FormItem>
</div>
</div>
<div className="flex justify-end space-x-5">
<ButtonPrimary
onClick={(e) => {
e.preventDefault();
handleSavePassenger();
}}
>
{t("saveChanges")}
</ButtonPrimary>
</div>
</form>
</div>
<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">{t("addNewPassenger")}</p>
</Link>
{/* Passenger Table */}
{passengers.map((item : data)=>(
<PassengerTable key={item.id} data={item} />
))}
</div>
);
};
export default EditPassenger;
export default PassengersList;

12
src/app/[locale]/(client-components)/(HeroSearchForm2Mobile)/HeroSearchForm2Mobile.tsx

@ -10,8 +10,10 @@ import StaySearchForm from "./(stay-search-form)/StaySearchForm";
import CarsSearchForm from "./(car-search-form)/CarsSearchForm";
import FlightSearchForm from "./(flight-search-form)/FlightSearchForm";
import { usePathname, useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
const HeroSearchForm2Mobile = () => {
const { t } = useTranslation("navigation");
const [showModal, setShowModal] = useState(false);
// FOR RESET ALL DATA WHEN CLICK CLEAR BUTTON
@ -39,10 +41,10 @@ useEffect(()=>{
<MagnifyingGlassIcon className="flex-shrink-0 w-5 h-5" />
<div className="ml-3 flex-1 text-left overflow-hidden">
<span className="block font-medium text-sm">Where to?</span>
<span className="block font-medium text-sm">{t("Where to?")}</span>
<span className="block mt-0.5 text-xs font-light text-neutral-500 dark:text-neutral-400 ">
<span className="line-clamp-1">
Anywhere Any week Add guests
{t("Anywhere • Any week • Add guests")}
</span>
</span>
</div>
@ -93,7 +95,7 @@ useEffect(()=>{
</div>
<Tab.List className="pt-12 flex w-full justify-center font-semibold text-sm sm:text-base text-neutral-500 dark:text-neutral-400 space-x-6 sm:space-x-8">
{/* {["Stay", "Experiences", "Cars", "Flights"].map(
{/* {t(["Stay", "Experiences", "Cars", "Flights"]).map(
(item, index) => (
<Tab key={index} as={Fragment}>
{({ selected }) => (
@ -149,7 +151,7 @@ useEffect(()=>{
resetIsShowingDialog();
}}
>
Clear all
{t("Clear all")}
</button>
<ButtonSubmit
onClick={() => {
@ -169,4 +171,4 @@ useEffect(()=>{
);
};
export default HeroSearchForm2Mobile;
export default HeroSearchForm2Mobile;

731
src/app/[locale]/(stay-listings)/TabFilters.tsx

@ -9,682 +9,117 @@ import ButtonClose from "@/shared/ButtonClose";
import Checkbox from "@/shared/Checkbox";
import Slider from "rc-slider";
import convertNumbThousand from "@/utils/convertNumbThousand";
import { useTranslation } from "react-i18next";
// DEMO DATA
const typeOfPaces = [
{
name: "Entire place",
description: "Have a place to yourself",
},
{
name: "Private room",
description: "Have your own room and share some common spaces",
},
{
name: "Hotel room",
description:
"Have a private or shared room in a boutique hotel, hostel, and more",
},
{
name: "Shared room",
description: "Stay in a shared space, like a common room",
},
];
const moreFilter1 = [
{ name: "Kitchen", defaultChecked: true },
{ name: "Air conditioning", defaultChecked: true },
{ name: "Heating" },
{ name: "Dryer" },
{ name: "Washer" },
{ name: "Wifi" },
{ name: "Indoor fireplace" },
{ name: "Breakfast" },
{ name: "Hair dryer" },
{ name: " Dedicated workspace" },
];
const moreFilter2 = [
{ name: " Free parking on premise" },
{ name: "Hot tub" },
{ name: "Gym" },
{ name: " Pool" },
{ name: " EV charger" },
];
const moreFilter3 = [
{ name: " House" },
{ name: "Bed and breakfast" },
{ name: "Apartment", defaultChecked: true },
{ name: " Boutique hotel" },
{ name: " Bungalow" },
{ name: " Chalet", defaultChecked: true },
{ name: " Condominium", defaultChecked: true },
{ name: " Cottage" },
{ name: " Guest suite" },
{ name: " Guesthouse" },
];
const moreFilter4 = [{ name: " Pets allowed" }, { name: "Smoking allowed" }];
const TabFilters = ({data ,
onChangeCountry = (item)=>{}
}) => {
const TabFilters = ({ data, onChangeCountry = (item) => {} }) => {
const { t } = useTranslation("common");
const [isOpenMoreFilter, setisOpenMoreFilter] = useState(false);
const [isOpenMoreFilterMobile, setisOpenMoreFilterMobile] = useState(false);
const [rangePrices, setRangePrices] = useState([0, 1000]);
const [checkedItems, setCheckedItems] = useState({});
useEffect(()=>{
onChangeCountry(checkedItems)
} , [checkedItems])
useEffect(() => {
onChangeCountry(checkedItems);
}, [checkedItems]);
//
const closeModalMoreFilter = () => setisOpenMoreFilter(false);
const openModalMoreFilter = () => setisOpenMoreFilter(true);
//
const closeModalMoreFilterMobile = () => setisOpenMoreFilterMobile(false);
const openModalMoreFilterMobile = () => setisOpenMoreFilterMobile(true);
console.log(data);
const renderXClear = () => {
return (
<span className="w-4 h-4 rounded-full bg-primary-500 text-white flex items-center justify-center ml-3 cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-3 w-3"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</span>
);
};
const renderTabsTypeOfPlace = () => {
return (
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-neutral-300 dark:border-neutral-700 hover:border-neutral-400 dark:hover:border-neutral-6000 focus:outline-none ${
open ? "!border-primary-500 " : ""
}`}
>
<span>Type of place</span>
<i className="las la-angle-down ml-2"></i>
</Popover.Button>
<Transition
as={Fragment}
// ... transition props
>
<Popover.Panel className="absolute z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 lg:max-w-md">
<div className="overflow-hidden rounded-2xl shadow-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
<div className="relative flex flex-col px-5 py-6 space-y-5">
{data.map((item) => (
<div key={item.id} className="">
<Checkbox
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
</div>
))}
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
};
const renderTabsRoomAndBeds = () => {
return (
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-neutral-300 dark:border-neutral-700 hover:border-neutral-400 dark:hover:border-neutral-6000 focus:outline-none ${
open ? "!border-primary-500 " : ""
}`}
>
<span>Rooms of Beds</span>
<i className="las la-angle-down ml-2"></i>
</Popover.Button>
<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 z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 lg:max-w-md">
<div className="overflow-hidden rounded-2xl shadow-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
<div className="relative flex flex-col px-5 py-6 space-y-5">
<NcInputNumber label="Beds" max={10} />
<NcInputNumber label="Bedrooms" max={10} />
<NcInputNumber label="Bathrooms" max={10} />
</div>
<div className="p-5 bg-neutral-50 dark:bg-neutral-900 dark:border-t dark:border-neutral-800 flex items-center justify-between">
<ButtonThird onClick={close} sizeClass="px-4 py-2 sm:px-5">
Clear
</ButtonThird>
<ButtonPrimary
onClick={close}
sizeClass="px-4 py-2 sm:px-5"
>
Apply
</ButtonPrimary>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
};
const renderTabsPriceRage = () => {
return (
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-primary-500 bg-primary-50 text-primary-700 focus:outline-none `}
>
<span>
{`$${convertNumbThousand(
rangePrices[0]
)} - $${convertNumbThousand(rangePrices[1])}`}{" "}
</span>
{renderXClear()}
</Popover.Button>
<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 z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 ">
<div className="overflow-hidden rounded-2xl shadow-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
<div className="relative flex flex-col px-5 py-6 space-y-8">
<div className="space-y-5">
<span className="font-medium">Price per day</span>
<Slider
range
className="text-red-400"
min={0}
max={2000}
defaultValue={[rangePrices[0], rangePrices[1]]}
allowCross={false}
onChange={(e) => setRangePrices(e as number[])}
const renderTabsTypeOfPlace = () => (
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-neutral-300 dark:border-neutral-700 hover:border-neutral-400 dark:hover:border-neutral-6000 focus:outline-none ${
open ? "!border-primary-500 " : ""
}`}
>
<span>{t("TypeOfPlace")}</span>
<i className="las la-angle-down ml-2"></i>
</Popover.Button>
<Transition as={Fragment}>
<Popover.Panel className="absolute z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 lg:max-w-md">
<div className="overflow-hidden rounded-2xl shadow-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
<div className="relative flex flex-col px-5 py-6 space-y-5">
{data.map((item) => (
<div key={item.id} className="">
<Checkbox
name={item.name}
label={t(item.name)}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
</div>
<div className="flex justify-between space-x-5">
<div>
<label
htmlFor="minPrice"
className="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Min price
</label>
<div className="mt-1 relative rounded-md">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-neutral-500 sm:text-sm">
$
</span>
</div>
<input
type="text"
name="minPrice"
disabled
id="minPrice"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-3 sm:text-sm border-neutral-200 rounded-full text-neutral-900"
value={rangePrices[0]}
/>
</div>
</div>
<div>
<label
htmlFor="maxPrice"
className="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Max price
</label>
<div className="mt-1 relative rounded-md">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-neutral-500 sm:text-sm">
$
</span>
</div>
<input
type="text"
disabled
name="maxPrice"
id="maxPrice"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-3 sm:text-sm border-neutral-200 rounded-full text-neutral-900"
value={rangePrices[1]}
/>
</div>
</div>
</div>
</div>
<div className="p-5 bg-neutral-50 dark:bg-neutral-900 dark:border-t dark:border-neutral-800 flex items-center justify-between">
<ButtonThird onClick={close} sizeClass="px-4 py-2 sm:px-5">
Clear
</ButtonThird>
<ButtonPrimary
onClick={close}
sizeClass="px-4 py-2 sm:px-5"
>
Apply
</ButtonPrimary>
</div>
))}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
};
// Inside TabFilters component
const renderMoreFilterItem = (
data: {
name: string;
defaultChecked?: boolean;
}[]
) => {
const list1 = data.filter((_, i) => i < data.length / 2);
const list2 = data.filter((_, i) => i >= data.length / 2);
return (
<div className="grid grid-cols-2 gap-8">
<div className="flex flex-col space-y-5">
{list1.map((item) => (
<Checkbox
key={item.name}
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
))}
</div>
<div className="flex flex-col space-y-5">
{list2.map((item) => (
<Checkbox
key={item.name}
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
))}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
};
const renderTabMoreFilter = () => {
return (
<div>
<div
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-primary-500 bg-primary-50 text-primary-700 focus:outline-none cursor-pointer`}
onClick={openModalMoreFilter}
>
<span>More filters (3)</span>
{renderXClear()}
</div>
<Transition appear show={isOpenMoreFilter} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-50 overflow-y-auto"
onClose={closeModalMoreFilter}
const renderTabsRoomAndBeds = () => (
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-neutral-300 dark:border-neutral-700 hover:border-neutral-400 dark:hover:border-neutral-6000 focus:outline-none ${
open ? "!border-primary-500 " : ""
}`}
>
<div className="min-h-screen 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 dark:bg-opacity-60" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
className="inline-block py-8 px-2 h-screen w-full max-w-4xl"
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-flex flex-col w-full max-w-4xl 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">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
More filters
</Dialog.Title>
<span className="absolute left-3 top-3">
<ButtonClose onClick={closeModalMoreFilter} />
</span>
</div>
<div className="flex-grow overflow-y-auto">
<div className="px-10 divide-y divide-neutral-200 dark:divide-neutral-800">
<div className="py-7">
<h3 className="text-xl font-medium">Amenities</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter1)}
</div>
</div>
<div className="py-7">
<h3 className="text-xl font-medium">Facilities</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter2)}
</div>
</div>
<div className="py-7">
<h3 className="text-xl font-medium">Property type</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter3)}
</div>
</div>
<div className="py-7">
<h3 className="text-xl font-medium">House rules</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter4)}
</div>
</div>
</div>
</div>
<div className="p-6 flex-shrink-0 bg-neutral-50 dark:bg-neutral-900 dark:border-t dark:border-neutral-800 flex items-center justify-between">
<ButtonThird
onClick={closeModalMoreFilter}
sizeClass="px-4 py-2 sm:px-5"
>
Clear
</ButtonThird>
<ButtonPrimary
onClick={closeModalMoreFilter}
sizeClass="px-4 py-2 sm:px-5"
>
Apply
</ButtonPrimary>
</div>
<span>{t("Rooms and Beds")}</span>
<i className="las la-angle-down ml-2"></i>
</Popover.Button>
<Transition as={Fragment}>
<Popover.Panel className="absolute z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 lg:max-w-md">
<div className="overflow-hidden rounded-2xl shadow-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
<div className="relative flex flex-col px-5 py-6 space-y-5">
<NcInputNumber label={t("Beds")} max={10} />
<NcInputNumber label={t("Bedrooms")} max={10} />
<NcInputNumber label={t("Bathrooms")} max={10} />
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
</div>
);
};
const renderTabMoreFilterMobile = () => {
return (
<div>
<div
className={`flex lg:hidden items-center justify-center px-4 py-2 text-sm rounded-full border border-primary-500 bg-primary-50 text-primary-700 focus:outline-none cursor-pointer`}
onClick={openModalMoreFilterMobile}
>
<span>Countries</span>
{renderXClear()}
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
<Transition appear show={isOpenMoreFilterMobile} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-50 overflow-y-auto"
onClose={closeModalMoreFilterMobile}
const renderTabsPriceRage = () => (
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button
className={`flex items-center justify-center px-4 py-2 text-sm rounded-full border border-primary-500 bg-primary-50 text-primary-700 focus:outline-none `}
>
<div className="min-h-screen 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 dark:bg-opacity-60" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
className="inline-block py-8 px-2 h-screen w-full max-w-4xl"
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-flex flex-col w-full max-w-4xl 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">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
More filters
</Dialog.Title>
<span className="absolute left-3 top-3">
<ButtonClose onClick={closeModalMoreFilterMobile} />
</span>
</div>
<div className="flex-grow overflow-y-auto">
<div className="px-4 sm:px-6 divide-y divide-neutral-200 dark:divide-neutral-800">
{/* ---- */}
<div className="py-7">
<h3 className="text-xl font-medium">Type of place</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(data)}
</div>
</div>
{/* ---- */}
{/* <div className="py-7">
<h3 className="text-xl font-medium">Range Prices</h3>
<div className="mt-6 relative ">
<div className="relative flex flex-col space-y-8">
<div className="space-y-5">
<Slider
range
className="text-red-400"
min={0}
max={2000}
defaultValue={[0, 1000]}
allowCross={false}
onChange={(e) => setRangePrices(e as number[])}
/>
</div>
<div className="flex justify-between space-x-5">
<div>
<label
htmlFor="minPrice"
className="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Min price
</label>
<div className="mt-1 relative rounded-md">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-neutral-500 sm:text-sm">
$
</span>
</div>
<input
type="text"
name="minPrice"
disabled
id="minPrice"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-3 sm:text-sm border-neutral-200 rounded-full text-neutral-900"
value={rangePrices[0]}
/>
</div>
</div>
<div>
<label
htmlFor="maxPrice"
className="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Max price
</label>
<div className="mt-1 relative rounded-md">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-neutral-500 sm:text-sm">
$
</span>
</div>
<input
type="text"
disabled
name="maxPrice"
id="maxPrice"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-3 sm:text-sm border-neutral-200 rounded-full text-neutral-900"
value={rangePrices[1]}
/>
</div>
</div>
</div>
</div>
</div>
</div> */}
{/* ---- */}
{/* <div className="py-7">
<h3 className="text-xl font-medium">Rooms and beds</h3>
<div className="mt-6 relative flex flex-col space-y-5">
<NcInputNumber label="Beds" max={10} />
<NcInputNumber label="Bedrooms" max={10} />
<NcInputNumber label="Bathrooms" max={10} />
</div>
</div> */}
{/* ---- */}
{/* <div className="py-7">
<h3 className="text-xl font-medium">Amenities</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter1)}
</div>
</div> */}
{/* ---- */}
{/* <div className="py-7">
<h3 className="text-xl font-medium">Facilities</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter2)}
</div>
</div> */}
{/* ---- */}
{/* <div className="py-7">
<h3 className="text-xl font-medium">Property type</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter3)}
</div>
</div> */}
{/* ---- */}
{/* <div className="py-7">
<h3 className="text-xl font-medium">House rules</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(moreFilter4)}
</div>
</div> */}
</div>
</div>
<div className="p-4 sm:p-6 flex-shrink-0 bg-neutral-50 dark:bg-neutral-900 dark:border-t dark:border-neutral-800 flex items-center justify-between">
<ButtonThird
onClick={closeModalMoreFilterMobile}
sizeClass="px-4 py-2 sm:px-5"
>
Clear
</ButtonThird>
<ButtonPrimary
onClick={closeModalMoreFilterMobile}
sizeClass="px-4 py-2 sm:px-5"
>
Apply
</ButtonPrimary>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
</div>
);
};
<span>
{`${t("$")}${convertNumbThousand(rangePrices[0])} - ${t("$")}${convertNumbThousand(rangePrices[1])}`} {" "}
</span>
</Popover.Button>
</>
)}
</Popover>
);
return (
<div className="flex lg:space-x-4">
<div className="hidden lg:flex space-x-4">
{renderTabsTypeOfPlace()}
{/* {renderTabsPriceRage()} */}
{/* {renderTabsRoomAndBeds()} */}
{/* {renderTabMoreFilter()} */}
{renderTabsRoomAndBeds()}
{renderTabsPriceRage()}
</div>
{renderTabMoreFilterMobile()}
</div>
);
};
export default TabFilters;
export default TabFilters;

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

@ -186,7 +186,7 @@ const PageAddListing1: FC<PageAddListing1Props> = ({
{/* Passport Number Field */}
<FormItem label="Passport Number" desc="">
<Input
type="number"
type="text"
className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
onChange={(e) =>
setNewPassenger((prev: any) => ({
@ -206,13 +206,13 @@ const PageAddListing1: FC<PageAddListing1Props> = ({
<FormItem label="Date of Birth" desc="">
<div className="relative">
<Input
className={`rounded-lg border ${dateError ? "border-red-500" : ""} pr-20`}
className={`rounded-lg border ${dateError ? "border-red-500" : ""} ${!!Passenger.birthdate && "pr-20"}`}
type="date"
value={Passenger.birthdate || ""}
onChange={handleBirthdateChange}
placeholder="Date of Birth"
/>
{ageLabel && (
{(ageLabel && Passenger.birthdate ) && (
<span className={`absolute right-2 top-2 px-2 py-1 rounded-md text-sm ${dateError ?"bg-red-100 text-red-600" : "bg-green-100 text-green-600"} `}>
{passengerAgeLable}
</span>

4
src/app/[locale]/custom-trip/page.tsx

@ -454,10 +454,10 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
handleDestinationChange(
index,
"duration",
Math.max(1, Number(e.target.value))
e.target.value
)
}
min="1"
min="0"
/>
</FormItem>
<FormItem className="w-[50%] ml-4" label={t("finishDate")}>

8
src/app/[locale]/layout.tsx

@ -42,9 +42,9 @@ export default function LocaleLayout({
<ContextProvider>
<UserProvider>
<ClientCommons />
<SiteHeader />
<I18nProvider locale={locale}>
<SiteHeader />
<ToastContainer
position="top-right"
autoClose={5000}
@ -58,9 +58,9 @@ export default function LocaleLayout({
theme="light"
/>
{children}
<FooterNav />
<Footer />
</I18nProvider>
<FooterNav />
<Footer />
</UserProvider>
</ContextProvider>
</body>

124
src/app/[locale]/tours/TabFilters.tsx

@ -9,6 +9,7 @@ import ButtonClose from "@/shared/ButtonClose";
import Checkbox from "@/shared/Checkbox";
import Slider from "rc-slider";
import convertNumbThousand from "@/utils/convertNumbThousand";
import { useTranslation } from "react-i18next";
// DEMO DATA
const typeOfPaces = [
@ -67,18 +68,16 @@ const moreFilter3 = [
const moreFilter4 = [{ name: " Pets allowed" }, { name: "Smoking allowed" }];
const TabFilters = ({data ,
onChangeCountry = (item)=>{}
}) => {
const TabFilters = ({ data, onChangeCountry = (item) => {} }) => {
const [isOpenMoreFilter, setisOpenMoreFilter] = useState(false);
const [isOpenMoreFilterMobile, setisOpenMoreFilterMobile] = useState(false);
const [rangePrices, setRangePrices] = useState([0, 1000]);
const [checkedItems, setCheckedItems] = useState({});
const { t } = useTranslation("common");
useEffect(()=>{
onChangeCountry(checkedItems)
} , [checkedItems])
useEffect(() => {
onChangeCountry(checkedItems);
}, [checkedItems]);
//
const closeModalMoreFilter = () => setisOpenMoreFilter(false);
@ -86,7 +85,7 @@ onChangeCountry(checkedItems)
//
const closeModalMoreFilterMobile = () => setisOpenMoreFilterMobile(false);
const openModalMoreFilterMobile = () => setisOpenMoreFilterMobile(true);
console.log(data);
console.log(data);
const renderXClear = () => {
return (
@ -117,14 +116,14 @@ console.log(data);
open ? "!border-primary-500 " : ""
}`}
>
<span>Type of place</span>
<span>{t("Type of place")}</span>
<i className="las la-angle-down ml-2"></i>
</Popover.Button>
<Transition
as={Fragment}
// ... transition props
>
<Popover.Panel className="absolute z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 lg:max-w-md">
<Popover.Panel className="absolute z-10 w-screen max-w-sm px-4 mt-3 left-0 sm:px-0 lg:max-w-md rtl:left-auto rtl:right-0">
<div className="overflow-hidden rounded-2xl shadow-xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700">
<div className="relative flex flex-col px-5 py-6 space-y-5">
{data.map((item) => (
@ -310,53 +309,52 @@ console.log(data);
);
};
// Inside TabFilters component
const renderMoreFilterItem = (
data: {
name: string;
defaultChecked?: boolean;
}[]
) => {
const list1 = data.filter((_, i) => i < data.length / 2);
const list2 = data.filter((_, i) => i >= data.length / 2);
return (
<div className="grid grid-cols-2 gap-8">
<div className="flex flex-col space-y-5">
{list1.map((item) => (
<Checkbox
key={item.name}
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
))}
</div>
<div className="flex flex-col space-y-5">
{list2.map((item) => (
<Checkbox
key={item.name}
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
))}
// Inside TabFilters component
const renderMoreFilterItem = (
data: {
name: string;
defaultChecked?: boolean;
}[]
) => {
const list1 = data.filter((_, i) => i < data.length / 2);
const list2 = data.filter((_, i) => i >= data.length / 2);
return (
<div className="grid grid-cols-2 gap-8">
<div className="flex flex-col space-y-5">
{list1.map((item) => (
<Checkbox
key={item.name}
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
))}
</div>
<div className="flex flex-col space-y-5">
{list2.map((item) => (
<Checkbox
key={item.name}
name={item.name}
label={item.name}
checked={!!checkedItems[item.name]}
onChange={(checked) => {
setCheckedItems((prev) => ({
...prev,
[item.name]: checked,
}));
}}
/>
))}
</div>
</div>
</div>
);
};
);
};
const renderTabMoreFilter = () => {
return (
@ -410,7 +408,7 @@ const renderMoreFilterItem = (
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
More filters
{t("moreFilters")}
</Dialog.Title>
<span className="absolute left-3 top-3">
<ButtonClose onClick={closeModalMoreFilter} />
@ -451,13 +449,13 @@ const renderMoreFilterItem = (
onClick={closeModalMoreFilter}
sizeClass="px-4 py-2 sm:px-5"
>
Clear
{t("clear")}
</ButtonThird>
<ButtonPrimary
onClick={closeModalMoreFilter}
sizeClass="px-4 py-2 sm:px-5"
>
Apply
{t("apply")}
</ButtonPrimary>
</div>
</div>
@ -476,7 +474,7 @@ const renderMoreFilterItem = (
className={`flex lg:hidden items-center justify-center px-4 py-2 text-sm rounded-full border border-primary-500 bg-primary-50 text-primary-700 focus:outline-none cursor-pointer`}
onClick={openModalMoreFilterMobile}
>
<span>Countries</span>
<span>{t("tourSuggestion.heading")}</span>
{renderXClear()}
</div>
@ -521,7 +519,7 @@ const renderMoreFilterItem = (
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
More filters
{t("moreFilters")}
</Dialog.Title>
<span className="absolute left-3 top-3">
<ButtonClose onClick={closeModalMoreFilterMobile} />
@ -532,7 +530,7 @@ const renderMoreFilterItem = (
<div className="px-4 sm:px-6 divide-y divide-neutral-200 dark:divide-neutral-800">
{/* ---- */}
<div className="py-7">
<h3 className="text-xl font-medium">Type of place</h3>
<h3 className="text-xl font-medium">{t("Type of place")}</h3>
<div className="mt-6 relative ">
{renderMoreFilterItem(data)}
</div>
@ -662,7 +660,7 @@ const renderMoreFilterItem = (
onClick={closeModalMoreFilterMobile}
sizeClass="px-4 py-2 sm:px-5"
>
Apply
{t("apply")}
</ButtonPrimary>
</div>
</div>

30
src/app/[locale]/tours/[slug]/page.tsx

@ -130,17 +130,20 @@ const ListingStayDetailPage: FC = () => {
const renderSidebar = () => {
const total =
details?.final_price && passengers
? (
passengers.guestAdults * Number(details.price) +
passengers.guestChildren * Number(details.price_child) +
passengers.guestInfants * Number(details?.price_infant)
).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})
: 0;
console.log(details);
details?.final_price && passengers
? (
(passengers.guestAdults * (Number(details.price) || 0)) +
(passengers.guestChildren * (Number(details.price_child) || 0)) +
(passengers.guestInfants * (Number(details.price_infant) || 0))
) * (1 - ((Number(details?.percent_off) || 0) / 100))
: 0;
// Format the result as currency
const formattedTotal = total.toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
return (
<div className="listingSectionSidebar__wrap shadow-xl">
@ -176,13 +179,13 @@ const ListingStayDetailPage: FC = () => {
{/* Total price */}
<div className="flex justify-between font-semibold">
<span>{t("total")}</span> {/* Translate total */}
<span>{total}</span>
<span>{formattedTotal}</span>
</div>
{/* Reserve Button */}
<ButtonPrimary
className={
details?.status === "AVAILABLE" && details.is_access && totalGuests
details?.status === "AVAILABLE" && totalGuests
? ""
: "opacity-60 pointer-events-none"
}
@ -193,6 +196,7 @@ const ListingStayDetailPage: FC = () => {
</div>
);
};
console.log(details);
return (
<div className="nc-ListingStayDetailPage">

6
src/components/Footer.tsx

@ -58,7 +58,7 @@ const Footer: React.FC = () => {
<div className="nc-Footer relative py-24 lg:py-28 border-t border-neutral-200 dark:border-neutral-700">
<div className="container grid grid-cols-2 gap-y-10 gap-x-5 sm:gap-x-8 md:grid-cols-4 lg:grid-cols-6 lg:gap-x-10 ">
<div className="grid grid-cols-4 gap-5 col-span-2 md:col-span-4 lg:md:col-span-2 lg:flex lg:flex-col">
<div className="grid grid-cols-1 gap-5 col-span-2 md:col-span-4 lg:md:col-span-2 lg:flex lg:flex-col">
<div className="">
<Logo />
</div>
@ -67,10 +67,10 @@ const Footer: React.FC = () => {
</p>
</div>
<div className="col-span-1 flex items-center md:col-span-1">
<SocialsList1 className="flex items-center space-x-3 lg:space-x-0 lg:flex-col lg:space-y-2.5 lg:items-start" />
<SocialsList1 className="flex space-x-0 flex-col space-y-2.5 items-start" />
</div>
{widgetMenus.map(renderWidgetMenuItem)}
<div className="grid grid-cols-4 gap-5 col-span-2 md:col-span-4 lg:md:col-span-2 lg:flex lg:flex-col">
<div className="grid grid-cols-1 gap-5 col-span-2 md:col-span-4 lg:md:col-span-2 lg:flex lg:flex-col">
<h1>{t("aboutUs.title")}</h1>
<p className="font-extralight text-sm text-[#A2ABB8]">
{t("aboutUs.description")}

2
src/components/contexts/tourDetails.tsx

@ -131,6 +131,8 @@ export const ContextProvider = ({ children }: ContextProviderProps) => {
try {
const response = await axiosInstance.get(`/api/tours/${item}/`);
setDetails(response.data);
console.log(response);
} catch (error) {
console.error("Error fetching tour data:", error);
}

18
src/shared/Navigation/NavMobile.tsx

@ -12,6 +12,7 @@ import { ChevronDownIcon } from "@heroicons/react/24/solid";
import SwitchDarkMode from "@/shared/SwitchDarkMode";
import Link from "next/link";
import LangDropdown from "@/app/[locale]/(client-components)/(Header)/LangDropdown";
import { useTranslation } from "react-i18next";
export interface NavMobileProps {
data?: NavItemType[];
@ -22,6 +23,8 @@ const NavMobile: React.FC<NavMobileProps> = ({
data = NAVIGATION_DEMO,
onClickClose,
}) => {
const { t } = useTranslation("navigation");
const _renderMenuChild = (item: NavItemType) => {
return (
<ul className="nav-mobile-sub-menu pl-6 pb-1 text-base">
@ -36,7 +39,7 @@ const NavMobile: React.FC<NavMobileProps> = ({
<span
className={`py-2.5 pr-3 ${!i.children ? "block w-full" : ""}`}
>
{i.name}
{t(i.name)}
</span>
{i.children && (
<span
@ -80,7 +83,7 @@ const NavMobile: React.FC<NavMobileProps> = ({
<span
className={`py-2.5 pr-3 ${!item.children ? "block w-full" : ""}`}
>
{item.name}
{t(item.name)}
</span>
{item.children && (
<span className="flex-1 flex" onClick={(e) => e.preventDefault()}>
@ -108,10 +111,7 @@ const NavMobile: React.FC<NavMobileProps> = ({
<div className="py-6 px-5">
<Logo />
<div className="flex flex-col mt-5 text-neutral-700 dark:text-neutral-300 text-sm">
<span>
Discover the most outstanding articles on all topics of life. Write
your stories and share them
</span>
<span>{t("discover-articles")}</span>
<div className="flex justify-between items-center mt-4">
<SocialsList itemClass="w-9 h-9 flex items-center justify-center rounded-full bg-neutral-100 text-xl dark:bg-neutral-800 dark:text-neutral-300" />
@ -128,15 +128,15 @@ const NavMobile: React.FC<NavMobileProps> = ({
{data.map(_renderItem)}
</ul>
<div className="flex items-center justify-between py-6 px-5">
<ButtonPrimary href="/custom-trip">Custom Tour</ButtonPrimary>
<ButtonPrimary href="/custom-trip">{t("customTrip")}</ButtonPrimary>
<LangDropdown
className="flex"
panelClassName="z-10 w-screen max-w-[280px] px-4 mb-3 right-3 bottom-full sm:px-0"
panelClassName="z-10 w-screen max-w-[280px] px-4 mb-3 right-3 bottom-full sm:px-0 rtl:right-auto rtl:left-3"
/>
</div>
</div>
);
};
export default NavMobile;
export default NavMobile;

2
src/shared/SocialsList1.tsx

@ -21,7 +21,7 @@ const SocialsList1: FC<SocialsList1Props> = ({ className = "space-y-2.5" }) => {
key={index}
>
<i className={item.icon}></i>
<span className="hidden lg:block text-sm">{item.name}</span>
<span className="block text-sm">{item.name}</span>
</a>
);
};

Loading…
Cancel
Save