Browse Source

fix (API) : changes related to passengers lable connected to API

fix(bills) : bills page connected to API
main
sina_sajjadi 3 days ago
parent
commit
4199c430d5
  1. 2
      next-i18next.config.js
  2. 1
      next.config.js
  3. 116
      public/locales/ar/common.json
  4. 4
      public/locales/en/common.json
  5. 181
      public/locales/fr/common.json
  6. 85
      src/app/[locale]/(account-pages)/bills/BillCard.tsx
  7. 181
      src/app/[locale]/(account-pages)/bills/[slug]/page.tsx
  8. 144
      src/app/[locale]/(account-pages)/bills/page.tsx
  9. 14
      src/app/[locale]/(client-components)/(Header)/LangDropdown.tsx
  10. 39
      src/app/[locale]/(client-components)/(Header)/MainNav1.tsx
  11. 4
      src/app/[locale]/(client-components)/(Header)/SearchDropdown.tsx
  12. 2
      src/app/[locale]/(client-components)/(HeroSearchForm)/(stay-search-form)/StaySearchForm.tsx
  13. 2
      src/app/[locale]/(client-components)/(HeroSearchForm)/LocationInput.tsx
  14. 2
      src/app/[locale]/add-listing/[[...stepIndex]]/page.tsx
  15. 75
      src/app/[locale]/add-new-passenger/page.tsx
  16. 25
      src/app/[locale]/blog/SectionMagazine5.tsx
  17. 40
      src/app/[locale]/layout.tsx
  18. 83
      src/app/[locale]/tours/[slug]/page.tsx
  19. 9
      src/components/MobileFooterSticky.tsx
  20. 2
      src/components/SearchCard.tsx
  21. 3
      src/components/SectionGridFeaturePlaces.tsx
  22. 42
      src/components/UserMenu.tsx
  23. 58
      src/components/api/axios.tsx
  24. 14
      src/components/contexts/tourDetails.tsx
  25. 5
      src/components/contexts/userContext.tsx
  26. 4
      src/i18n.ts
  27. 15
      src/middleware.ts
  28. 2
      src/shared/Navigation/Navigation.tsx

2
next-i18next.config.js

@ -2,7 +2,7 @@ const path = require("path");
module.exports = {
i18n: {
locales: ['en', 'vi', 'fr'], // List all supported locales
locales: ['en', 'vi', 'fr' , 'ar'], // List all supported locales
defaultLocale: 'en', // Set the default locale
},
};

1
next.config.js

@ -5,7 +5,6 @@ const { i18n } = require("./next-i18next.config");
module.exports = {
eslint: {
ignoreDuringBuilds: false,
},

116
public/locales/ar/common.json

@ -0,0 +1,116 @@
{
"home": "Accueil",
"allTours": "Tous les Tours",
"blogs": "Blogs",
"faq": "FAQ",
"aboutUs": "À Propos de Nous",
"customTour": "Tour Personnalisé",
"searchPlaceholder": "Où aller ?",
"searchDescription": "Partout • N'importe quelle semaine • Ajouter des invités",
"beginAdventure": "Commencez votre aventure spirituelle",
"planPilgrimage": "Planifiez votre pèlerinage facilement. Trouvez les meilleures accommodations, transports et expériences guidées vers les sanctuaires chiites à travers le monde",
"startJourney": "Commencez votre voyage",
"listOfTours": "Liste des Tours",
"exploreTours": "Explorez les tours et accommodations adaptées pour un voyage spirituel et mémorable",
"tourPeriod": "Période du Tour",
"tourPeriodDescription": "Début - Fin",
"guests": "Invités",
"addGuests": "Ajouter des invités",
"available": "Disponible",
"soldOut": "Épuisé",
"showMore": "Montrez-moi plus",
"happeningCities": "Villes Animées",
"costEffectiveAdvertising": "Publicité Rentable",
"costEffectiveDescription": "Avec une annonce gratuite, vous pouvez faire la publicité de votre location sans frais initiaux",
"reachMillions": "Atteignez des millions avec Chisfis",
"reachMillionsDescription": "Des millions de personnes recherchent des lieux uniques où séjourner à travers le monde",
"secureAndSimple": "Securisé et Simple",
"secureDescription": "Une annonce sur Holiday Lettings vous offre un moyen sécurisé et facile de prendre des réservations et des paiements en ligne",
"mobileApps": "Applications Mobiles",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
"installation": "Installation",
"releaseNotes": "Notes de Version",
"upgradeGuide": "Guide de Mise à Niveau",
"browserSupport": "Support des Navigateurs",
"editorSupport": "Support de l'Éditeur",
"designFeatures": "Caractéristiques de Design",
"prototyping": "Prototypage",
"designSystems": "Systèmes de Design",
"pricing": "Tarification",
"security": "Sécurité",
"bestPractices": "Meilleures Pratiques",
"support": "Support",
"developers": "Développeurs",
"learnDesign": "Apprendre le Design",
"releases": "Versions",
"discussionForums": "Forums de Discussion",
"codeOfConduct": "Code de Conduite",
"communityResources": "Ressources Communautaires",
"contributing": "Contribuer",
"concurrentMode": "Mode Concurrent",
"goodNews": "Bonnes Nouvelles d'Ailleurs",
"whatPeopleThink": "Voyons ce que les gens pensent de Chisfis",
"testimonial": "Cet endroit est exactement comme sur la photo publiée sur Chisfis. Excellent service, nous avons passé un très bon séjour !",
"clientName": "Tiana Abie",
"clientLocation": "Malaisie",
"myTrips": "Mes Voyages",
"account": "Compte",
"menu": "Menu",
"gettingStarted": "Premiers Pas",
"explore": "Explorer",
"resources": "Ressources",
"community": "Communauté",
"placeType": "Type de Lieu",
"noTours": "Aucun tour disponible",
"itinerary": "Itinéraire",
"itineraryTitle": "Itinéraire",
"total": "Total",
"reserve": "Réserver",
"tourFeatures": "Caractéristiques du Tour",
"tourFeaturesTitle": "Caractéristiques du Tour",
"startRating": "Évaluation de Début",
"listingDetails": "Détails",
"imageAlt": "Image du tour",
"loading": "Chargement...",
"adults": "Adultes",
"adultsDesc": "Âges 13 ou plus",
"children": "Enfants",
"childrenDesc": "Âges 2–12",
"infants": "Bébés",
"infantsDesc": "Âges 0–2",
"traveler": "Voyageur",
"responses": "Réponses ({{count}})",
"submit": "Soumettre",
"cancel": "Annuler",
"relatedPosts": "Articles Connexes",
"aboutUsHeading": "👋 À Propos de Nous.",
"aboutUsSubheading": "Nous sommes une équipe passionnée dédiée à la création d'expériences de voyage inoubliables pour les explorateurs et les rêveurs. Des escapades sereines sur des plages tropicales aux aventures à sensations fortes dans des lieux exotiques, nous concevons des voyages aussi uniques que vous. Rejoignez-nous et explorons le monde, une aventure à la fois !",
"statisticTitle": "🚀 Faits Rapides",
"statisticDescription": "Nous sommes impartiaux et indépendants, et chaque jour nous créons des programmes et du contenu distinctifs de classe mondiale.",
"statisticHeading1": "10 millions",
"statisticSubHeading1": "D'articles ont été publiés à travers le monde (au 30 septembre 2021)",
"statisticHeading2": "100 000",
"statisticSubHeading2": "Utilisateurs enregistrés (au 30 septembre 2021)",
"statisticHeading3": "220+",
"statisticSubHeading3": "Pays et régions où nous sommes présents (au 30 septembre 2021)",
"customTrip": "Voyage Personnalisé",
"guide": "Guide",
"guideDescription": "Tout d'abord, écrivez l'origine de votre départ, puis choisissez la première destination de votre voyage, le nombre de nuits de séjour et le moyen de transport, puis choisissez vos destinations de voyage si vous le souhaitez.",
"beginYourTrip": "Commencez Votre Voyage",
"startDate": "Date de Début",
"numberOfPassengers": "Nombre de Passagers",
"destination": "Destination",
"selectCity": "Sélectionner une Ville",
"transportation": "Transport",
"selectTransport": "Sélectionner le Transport",
"hotel": "Hôtel",
"selectHotel": "Sélectionner l'Hôtel",
"duration": "Durée",
"finishDate": "Date de Fin",
"addDestination": "Ajouter une Destination",
"continue": "Continuer",
"successMessage": "Enregistré avec succès",
"to": "à",
"login": "Se Connecter",
"signup": "S'inscrire"
}

4
public/locales/en/common.json

@ -110,5 +110,7 @@
"addDestination": "Add Destination",
"continue": "Continue",
"successMessage": "Successfully Registered",
"to": "to"
"to": "to",
"login": "LogIn",
"signup": "Sign Up"
}

181
public/locales/fr/common.json

@ -1,67 +1,116 @@
{
"home": "Accueil",
"allTours": "Tous les Tours",
"blogs": "Blogs",
"faq": "FAQ",
"aboutUs": "À propos de nous",
"customTour": "Visite sur mesure",
"searchPlaceholder": "Où aller ?",
"searchDescription": "Partout • À toute semaine • Ajouter des invités",
"beginAdventure": "Commencez votre aventure spirituelle",
"planPilgrimage": "Planifiez votre pèlerinage avec facilité. Trouvez les meilleures accommodations, le transport et les expériences guidées vers les sanctuaires chiites à travers le monde",
"startJourney": "Commencer votre voyage",
"listOfTours": "Liste des Tours",
"exploreTours": "Explorez les visites et les hébergements adaptés pour un voyage spirituel et mémorable",
"tourPeriod": "Période de visite",
"tourPeriodDescription": "Début - Fin",
"guests": "Invités",
"addGuests": "Ajouter des invités",
"available": "Disponible",
"soldOut": "Épuisé",
"showMore": "Montrez-moi plus",
"happeningCities": "Villes en vogue",
"costEffectiveAdvertising": "Publicité rentable",
"costEffectiveDescription": "Avec une annonce gratuite, vous pouvez promouvoir votre location sans frais initiaux",
"reachMillions": "Atteignez des millions avec Chisfis",
"reachMillionsDescription": "Des millions de personnes recherchent des lieux uniques où séjourner à travers le monde",
"secureAndSimple": "Sécurisé et simple",
"secureDescription": "Une annonce de Holiday Lettings vous offre un moyen sécurisé et facile de prendre des réservations et des paiements en ligne",
"mobileApps": "Applications Mobiles",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
"installation": "Installation",
"releaseNotes": "Notes de version",
"upgradeGuide": "Guide de mise à niveau",
"browserSupport": "Support des navigateurs",
"editorSupport": "Support de l'éditeur",
"designFeatures": "Fonctionnalités de conception",
"prototyping": "Prototypage",
"designSystems": "Systèmes de design",
"pricing": "Tarification",
"security": "Sécurité",
"bestPractices": "Meilleures pratiques",
"support": "Support",
"developers": "Développeurs",
"learnDesign": "Apprendre la conception",
"releases": "Versions",
"discussionForums": "Forums de discussion",
"codeOfConduct": "Code de conduite",
"communityResources": "Ressources communautaires",
"contributing": "Contribuer",
"concurrentMode": "Mode concurrent",
"goodNews": "Bonne nouvelle de loin",
"whatPeopleThink": "Voyons ce que les gens pensent de Chisfis",
"testimonial": "Cet endroit est exactement comme la photo publiée sur Chisfis. Excellent service, nous avons passé un excellent séjour !",
"clientName": "Tiana Abie",
"clientLocation": "Malaisie",
"myTrips": "Mes Voyages",
"account": "Compte",
"menu": "Menu",
"gettingStarted": "Commencer",
"explore": "Explorer",
"resources": "Ressources",
"community": "Communauté",
"placeType": "Type de lieu",
"noTours": "Aucun tour disponible"
}
"home": "Accueil",
"allTours": "Tous les Tours",
"blogs": "Blogs",
"faq": "FAQ",
"aboutUs": "À Propos de Nous",
"customTour": "Tour Personnalisé",
"searchPlaceholder": "Où aller ?",
"searchDescription": "Partout • N'importe quelle semaine • Ajouter des invités",
"beginAdventure": "Commencez votre aventure spirituelle",
"planPilgrimage": "Planifiez votre pèlerinage facilement. Trouvez les meilleures accommodations, transports et expériences guidées vers les sanctuaires chiites à travers le monde",
"startJourney": "Commencez votre voyage",
"listOfTours": "Liste des Tours",
"exploreTours": "Explorez les tours et accommodations adaptées pour un voyage spirituel et mémorable",
"tourPeriod": "Période du Tour",
"tourPeriodDescription": "Début - Fin",
"guests": "Invités",
"addGuests": "Ajouter des invités",
"available": "Disponible",
"soldOut": "Épuisé",
"showMore": "Montrez-moi plus",
"happeningCities": "Villes Animées",
"costEffectiveAdvertising": "Publicité Rentable",
"costEffectiveDescription": "Avec une annonce gratuite, vous pouvez faire la publicité de votre location sans frais initiaux",
"reachMillions": "Atteignez des millions avec Chisfis",
"reachMillionsDescription": "Des millions de personnes recherchent des lieux uniques où séjourner à travers le monde",
"secureAndSimple": "Securisé et Simple",
"secureDescription": "Une annonce sur Holiday Lettings vous offre un moyen sécurisé et facile de prendre des réservations et des paiements en ligne",
"mobileApps": "Applications Mobiles",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
"installation": "Installation",
"releaseNotes": "Notes de Version",
"upgradeGuide": "Guide de Mise à Niveau",
"browserSupport": "Support des Navigateurs",
"editorSupport": "Support de l'Éditeur",
"designFeatures": "Caractéristiques de Design",
"prototyping": "Prototypage",
"designSystems": "Systèmes de Design",
"pricing": "Tarification",
"security": "Sécurité",
"bestPractices": "Meilleures Pratiques",
"support": "Support",
"developers": "Développeurs",
"learnDesign": "Apprendre le Design",
"releases": "Versions",
"discussionForums": "Forums de Discussion",
"codeOfConduct": "Code de Conduite",
"communityResources": "Ressources Communautaires",
"contributing": "Contribuer",
"concurrentMode": "Mode Concurrent",
"goodNews": "Bonnes Nouvelles d'Ailleurs",
"whatPeopleThink": "Voyons ce que les gens pensent de Chisfis",
"testimonial": "Cet endroit est exactement comme sur la photo publiée sur Chisfis. Excellent service, nous avons passé un très bon séjour !",
"clientName": "Tiana Abie",
"clientLocation": "Malaisie",
"myTrips": "Mes Voyages",
"account": "Compte",
"menu": "Menu",
"gettingStarted": "Premiers Pas",
"explore": "Explorer",
"resources": "Ressources",
"community": "Communauté",
"placeType": "Type de Lieu",
"noTours": "Aucun tour disponible",
"itinerary": "Itinéraire",
"itineraryTitle": "Itinéraire",
"total": "Total",
"reserve": "Réserver",
"tourFeatures": "Caractéristiques du Tour",
"tourFeaturesTitle": "Caractéristiques du Tour",
"startRating": "Évaluation de Début",
"listingDetails": "Détails",
"imageAlt": "Image du tour",
"loading": "Chargement...",
"adults": "Adultes",
"adultsDesc": "Âges 13 ou plus",
"children": "Enfants",
"childrenDesc": "Âges 2–12",
"infants": "Bébés",
"infantsDesc": "Âges 0–2",
"traveler": "Voyageur",
"responses": "Réponses ({{count}})",
"submit": "Soumettre",
"cancel": "Annuler",
"relatedPosts": "Articles Connexes",
"aboutUsHeading": "👋 À Propos de Nous.",
"aboutUsSubheading": "Nous sommes une équipe passionnée dédiée à la création d'expériences de voyage inoubliables pour les explorateurs et les rêveurs. Des escapades sereines sur des plages tropicales aux aventures à sensations fortes dans des lieux exotiques, nous concevons des voyages aussi uniques que vous. Rejoignez-nous et explorons le monde, une aventure à la fois !",
"statisticTitle": "🚀 Faits Rapides",
"statisticDescription": "Nous sommes impartiaux et indépendants, et chaque jour nous créons des programmes et du contenu distinctifs de classe mondiale.",
"statisticHeading1": "10 millions",
"statisticSubHeading1": "D'articles ont été publiés à travers le monde (au 30 septembre 2021)",
"statisticHeading2": "100 000",
"statisticSubHeading2": "Utilisateurs enregistrés (au 30 septembre 2021)",
"statisticHeading3": "220+",
"statisticSubHeading3": "Pays et régions où nous sommes présents (au 30 septembre 2021)",
"customTrip": "Voyage Personnalisé",
"guide": "Guide",
"guideDescription": "Tout d'abord, écrivez l'origine de votre départ, puis choisissez la première destination de votre voyage, le nombre de nuits de séjour et le moyen de transport, puis choisissez vos destinations de voyage si vous le souhaitez.",
"beginYourTrip": "Commencez Votre Voyage",
"startDate": "Date de Début",
"numberOfPassengers": "Nombre de Passagers",
"destination": "Destination",
"selectCity": "Sélectionner une Ville",
"transportation": "Transport",
"selectTransport": "Sélectionner le Transport",
"hotel": "Hôtel",
"selectHotel": "Sélectionner l'Hôtel",
"duration": "Durée",
"finishDate": "Date de Fin",
"addDestination": "Ajouter une Destination",
"continue": "Continuer",
"successMessage": "Enregistré avec succès",
"to": "à",
"login": "Se Connecter",
"signup": "S'inscrire"
}

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

@ -1,60 +1,95 @@
// BillCard.tsx
import React from "react";
import ButtonPrimary from "@/shared/ButtonPrimary";
export type BillStatus = "Awaiting Payment" | "Approved" | "Rejected" | "Pending";
export type BillStatus =
| "awaiting_payment"
| "approved"
| "rejected"
| "pending";
import { BiSolidPlaneAlt } from "react-icons/bi";
import { FaSimCard } from "react-icons/fa";
import { BsCart3 } from "react-icons/bs";
import { MdCurrencyExchange } from "react-icons/md";
export interface Bill {
title: string;
issuedDate: string;
expirationDate: string;
service: string;
created_at: string;
expiration_date: string;
amount: number;
status: BillStatus;
}
interface BillCardProps {
bill: Bill;
onViewDetail: (bill: Bill) => void; // Add onViewDetail prop
}
const statusStyles: { [key in BillStatus]: string } = {
"Awaiting Payment": "bg-yellow-200 text-yellow-700",
Approved: "bg-green-200 text-green-700",
Rejected: "bg-red-200 text-red-700",
Pending: "bg-blue-200 text-blue-700",
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">
Awaiting Payment
</span>
),
approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
Approved
</span>
),
rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
Rejected
</span>
),
pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
Pending
</span>
),
};
const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
console.log(bill);
return (
<div className="bg-white shadow-md rounded-lg p-4 mb-4">
<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 flex items-center space-x-2">
{bill.title.includes("Tour") && <span><BiSolidPlaneAlt className="text-bronze" size={25} /></span>}
{bill.title.includes("SIM Card") && <span><FaSimCard className="text-bronze" size={25} /></span>}
{bill.title.includes("Shop") && <span><BsCart3 className="text-bronze" size={25} /></span>}
{bill.title.includes("Tasrif") && <span><MdCurrencyExchange className="text-bronze" size={25} /></span>}
<span>{bill.title}</span>
{bill.service.includes("Tour") && (
<span>
<BiSolidPlaneAlt className="text-bronze" size={25} />
</span>
)}
{bill.service.includes("SIM Card") && (
<span>
<FaSimCard className="text-bronze" size={25} />
</span>
)}
{bill.service.includes("Shop") && (
<span>
<BsCart3 className="text-bronze" size={25} />
</span>
)}
{bill.service.includes("Tasrif") && (
<span>
<MdCurrencyExchange className="text-bronze" size={25} />
</span>
)}
<span>{bill.service}</span>
</h2>
<span className={`px-2 py-1 text-sm rounded-full ${statusStyles[bill.status]}`}>
{bill.status}
</span>
{statusStyles[bill.status]}
</div>
<div className="mt-4 text-sm text-gray-600">
<div className="flex justify-between mb-2">
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>Issued Date:</span>
<span>{bill.issuedDate}</span>
<span>{bill.created_at}</span>
</div>
<div className="flex justify-between mb-2">
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>Expiration Date:</span>
<span>{bill.expirationDate}</span>
<span>{bill.expiration_date}</span>
</div>
<div className="flex justify-between mt-2 font-semibold">
<div className="flex justify-between mt-2 font-semibold dark:text-neutral-400">
<span>Tour Invoice Amount:</span>
<span className="text-orange-600">${bill.amount.toFixed(2)}</span>
<span className="text-orange-600">${bill.amount}</span>
</div>
</div>

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

@ -1,15 +1,24 @@
// BillDetailCard.tsx
import React from "react";
import React, { useState } from "react";
import axiosInstance from "@/components/api/axios";
import ButtonPrimary from "@/shared/ButtonPrimary";
import Button from "@/shared/Button";
export type BillStatus = "Awaiting Payment" | "Approved" | "Rejected" | "Pending";
import Input from "@/shared/Input";
import { useUserContext } from "@/components/contexts/userContext";
import getImageURL from "@/components/api/getImageURL";
import { toast, ToastContainer } from "react-toastify";
import Image from "next/image";
export type BillStatus =
| "awaiting_payment"
| "approved"
| "rejected"
| "pending";
export interface Bill {
id: number;
title: string;
issuedDate: string;
expirationDate: string;
created_at: string;
expiration_date: string;
amount: number;
status: BillStatus;
passengers: { type: string; count: number }[];
@ -20,29 +29,84 @@ export interface Bill {
size: string;
src: string;
};
detail_service: {
passenger_counts: { adults: string; children: string; infants: string };
};
transaction_receipt: string;
card_number: string | number;
}
interface BillDetailCardProps {
bill: Bill;
}
const statusStyles: { [key in BillStatus]: string } = {
"Awaiting Payment": "bg-yellow-200 text-yellow-700",
Approved: "bg-green-200 text-green-700",
Rejected: "bg-red-200 text-red-700",
Pending: "bg-blue-200 text-blue-700",
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">
Awaiting Payment
</span>
),
approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
Approved
</span>
),
rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
Rejected
</span>
),
pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
Pending
</span>
),
};
const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [loadingUpload, setLoadingUpload] = useState(false);
const [loading, setLoading] = useState(false);
const { user } = useUserContext();
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoadingUpload(true);
const file = e.target.files?.[0];
if (file) {
const uploadedFile = await getImageURL(file);
setUploadedFile(uploadedFile.url);
setLoadingUpload(false);
}
};
const handleSubmit = () => {
setLoading(true);
if (uploadedFile) {
const formData = new FormData();
formData.append("transaction_receipt", uploadedFile);
axiosInstance
.patch(`/api/factors/update/${bill.id}/`, formData, {
headers: {
Authorization: `token ${user.token}`,
},
})
.then(() => {
toast.success("Transaction receipt updated successfully");
setLoading(false);
})
.catch(() => {
toast.error("Error updating transaction receipt");
setLoading(false);
});
} else {
console.log("No new file to upload");
}
};
return (
<div className="bg-white shadow-md rounded-lg p-4 mb-4">
<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>
<span className={`px-2 py-1 text-sm rounded-full ${statusStyles[bill.status]}`}>
{bill.status}
</span>
{statusStyles[bill.status]}
</div>
{bill.message && (
@ -52,23 +116,33 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
</div>
)}
<div className="mt-4 text-sm text-gray-600">
<div className="mt-4 text-sm text-gray-600 dark:text-neutral-400">
<div className="flex justify-between mb-2">
<span>Issued Date:</span>
<span>{bill.issuedDate}</span>
<span>{bill.created_at}</span>
</div>
<div className="flex justify-between mb-2">
<span>Expiration Date:</span>
<span>{bill.expirationDate}</span>
<span>{bill.expiration_date}</span>
</div>
<div className="mt-4">
<h3 className="font-semibold">Number of Passengers</h3>
{bill.passengers.map((p, index) => (
<div key={index} className="flex justify-between">
<span>#{p.count} ({p.type})</span>
</div>
))}
<div className="flex justify-between">
<span>
# {bill.detail_service.passenger_counts.adults} (Adults)
</span>
</div>
<div className="flex justify-between">
<span>
# {bill.detail_service.passenger_counts.children} (Children)
</span>
</div>
<div className="flex justify-between">
<span>
# {bill.detail_service.passenger_counts.infants} (Infants)
</span>
</div>
</div>
<div className="flex justify-between mt-4 font-semibold">
@ -78,8 +152,10 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
<div className="mt-4">
<h3 className="font-semibold">Account Number</h3>
<div className="flex items-center bg-gray-100 p-2 rounded-md mt-2">
<span className="flex-grow">{bill.accountNumber}</span>
<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>
<Button className="text-blue-500">Copy</Button>
</div>
</div>
@ -87,25 +163,60 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
{bill.uploadedImage ? (
<div className="mt-4">
<h3 className="font-semibold">Uploaded Image</h3>
<img src={bill.uploadedImage.src} alt="Uploaded" className="mt-2 rounded-md" />
<img
src={bill.uploadedImage.src}
alt="Uploaded"
className="mt-2 rounded-md"
/>
<div className="flex justify-between mt-2">
<span>{bill.uploadedImage.name}</span>
<span className="text-gray-500 text-sm">{bill.uploadedImage.size}</span>
<span className="text-gray-500 text-sm">
{bill.uploadedImage.size}
</span>
<Button className="text-red-500">Delete</Button>
</div>
</div>
) : (
<div className="mt-4">
<label className="block font-semibold mb-2">Upload Passport Image</label>
<div className="flex items-center bg-gray-100 p-2 rounded-md">
<Button className="bg-orange-500 text-white">Upload</Button>
<span className="flex-grow text-gray-500 text-sm ml-4">No File Selected</span>
<label className="block font-semibold mb-2">
Upload Passport Image
</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}
/>
{loadingUpload && <p>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="Current Receipt" />
</span>
) : (
<span className="flex-grow text-gray-500 text-sm ml-4">
No File Selected
</span>
)}
</div>
</div>
)}
</div>
<ButtonPrimary className="w-full mt-6">Submit</ButtonPrimary>
<ButtonPrimary loading ={loading} className="w-full mt-6" onClick={handleSubmit}>
Submit
</ButtonPrimary>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
</div>
);
};

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

@ -1,12 +1,18 @@
// BillsPage.tsx
"use client"
import React, { useState } from "react";
"use client";
import React, { useEffect, useState } from "react";
import BillCard from "./BillCard";
import BillDetailCard from "./[slug]/page";
import axiosInstance from "@/components/api/axios";
import { useUserContext } from "@/components/contexts/userContext";
import { headers } from "next/dist/client/components/headers";
// types.ts
export type BillStatus = "Awaiting Payment" | "Approved" | "Rejected" | "Pending";
export type BillStatus =
| "Awaiting Payment"
| "Approved"
| "Rejected"
| "Pending";
export interface Bill {
title: string;
@ -14,68 +20,68 @@ export interface Bill {
expirationDate: string;
amount: number;
status: BillStatus;
passengers : {}
accountNumber: string,
message? : string
passengers: {};
accountNumber: string;
message?: string;
}
const bills: Bill[] = [
{
title: "Karbala Tour Bill",
issuedDate: "12 Jan 2023",
expirationDate: "10 Jan 2023",
amount: 960,
status: "Awaiting Payment",
passengers: [
{ type: "Adult", count: 3 },
{ type: "Child", count: 2 },
{ type: "Infant", count: 1 },
],
accountNumber: "123-456-7890-0123",
},
{
title: "SIM Card Bill",
issuedDate: "12 Jan 2023",
expirationDate: "10 Jan 2023",
amount: 960,
status: "Approved",
passengers: [
{ type: "Adult", count: 2 },
{ type: "Child", count: 1 },
],
accountNumber: "987-654-3210-0123",
},
{
title: "Shop Bill",
issuedDate: "12 Jan 2023",
expirationDate: "10 Jan 2023",
amount: 960,
status: "Rejected",
passengers: [
{ type: "Adult", count: 1 },
],
accountNumber: "456-789-0123-4567",
message:
"The uploaded image does not meet the required quality standards. Please use a higher-resolution photo.",
},
{
title: "Tasrif Bill",
issuedDate: "12 Jan 2023",
expirationDate: "10 Jan 2023",
amount: 960,
status: "Pending",
passengers: [
{ type: "Adult", count: 3 },
{ type: "Child", count: 2 },
],
accountNumber: "321-654-9870-1234",
},
];
// const bills: Bill[] = [
// {
// title: "Karbala Tour Bill",
// issuedDate: "12 Jan 2023",
// expirationDate: "10 Jan 2023",
// amount: 960,
// status: "Awaiting Payment",
// passengers: [
// { type: "Adult", count: 3 },
// { type: "Child", count: 2 },
// { type: "Infant", count: 1 },
// ],
// accountNumber: "123-456-7890-0123",
// },
// {
// title: "SIM Card Bill",
// issuedDate: "12 Jan 2023",
// expirationDate: "10 Jan 2023",
// amount: 960,
// status: "Approved",
// passengers: [
// { type: "Adult", count: 2 },
// { type: "Child", count: 1 },
// ],
// accountNumber: "987-654-3210-0123",
// },
// {
// title: "Shop Bill",
// issuedDate: "12 Jan 2023",
// expirationDate: "10 Jan 2023",
// amount: 960,
// status: "Rejected",
// passengers: [
// { type: "Adult", count: 1 },
// ],
// accountNumber: "456-789-0123-4567",
// message:
// "The uploaded image does not meet the required quality standards. Please use a higher-resolution photo.",
// },
// {
// title: "Tasrif Bill",
// issuedDate: "12 Jan 2023",
// expirationDate: "10 Jan 2023",
// amount: 960,
// status: "Pending",
// passengers: [
// { type: "Adult", count: 3 },
// { type: "Child", count: 2 },
// ],
// accountNumber: "321-654-9870-1234",
// },
// ];
const BillsPage: React.FC = () => {
const [bills, setBills] = useState([]);
const [selectedBill, setSelectedBill] = useState<Bill | null>(null);
const { user } = useUserContext();
const handleViewDetail = (bill: Bill) => {
setSelectedBill(bill);
@ -84,9 +90,23 @@ const BillsPage: React.FC = () => {
const handleBackToList = () => {
setSelectedBill(null);
};
useEffect(() => {
axiosInstance("/api/factors/list/", {
headers: {
Authorization: `token ${user.token}`,
},
})
.then((response) => {
console.log(response);
setBills(response.data.results);
})
.catch((error) => {
console.log(error.message);
});
}, []);
return (
<div className="p-4 bg-gray-100 min-h-screen">
<div className="p-4 bg-gray-100 dark:bg-neutral-900 min-h-screen">
{selectedBill ? (
<div>
<button onClick={handleBackToList} className="text-blue-500 mb-4">

14
src/app/[locale]/(client-components)/(Header)/LangDropdown.tsx

@ -3,6 +3,7 @@ import { Popover, Tab, Transition } from "@headlessui/react";
import { useRouter, usePathname } from "next/navigation";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { MdOutlineLanguage } from "react-icons/md";
import Cookies from "js-cookie"; // Import js-cookie
// Language options
const languageOptions = [
@ -24,6 +25,12 @@ const languageOptions = [
description: "France",
icon: <MdOutlineLanguage />,
},
{
id: "ar",
name: "Arabic",
description: "Arabic",
icon: <MdOutlineLanguage />,
},
];
interface LangDropdownProps {
@ -45,7 +52,7 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
// Update selectedItem based on the current locale in the URL
useEffect(() => {
const locales = ["en", "vi", "fr"];
const locales = ["en", "vi", "fr", "ar"];
const segments = pathname.split("/").filter(Boolean);
const currentLocale = locales.includes(segments[0]) ? segments[0] : "en";
const newSelectedItem =
@ -58,7 +65,7 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
const segments = pathname.split("/").filter(Boolean);
// Remove the current locale if present
const locales = ["en", "vi", "fr"];
const locales = ["en", "vi", "fr", "ar"];
if (locales.includes(segments[0])) {
segments.shift();
}
@ -66,6 +73,9 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
// Prepend the new locale
const newPathname = `/${locale}/${segments.join("/")}`;
// Set the locale cookie
Cookies.set("locale", locale, { expires: 365 });
// Navigate to the new locale path
router.push(newPathname);
};

39
src/app/[locale]/(client-components)/(Header)/MainNav1.tsx

@ -1,28 +1,20 @@
"use client";
import React, { FC, use, useContext, useEffect, useState } from "react";
import React, { FC } from "react";
import Logo from "@/shared/Logo";
import Navigation from "@/shared/Navigation/Navigation";
import SearchDropdown from "./SearchDropdown";
import ButtonPrimary from "@/shared/ButtonPrimary";
import MenuBar from "@/shared/MenuBar";
import SwitchDarkMode from "@/shared/SwitchDarkMode";
import HeroSearchForm2MobileFactory from "../(HeroSearchForm2Mobile)/HeroSearchForm2MobileFactory";
import { MdOutlineCardTravel } from "react-icons/md";
import Avatar from "@/shared/Avatar";
import Link from "next/link";
import { useUserContext } from "@/components/contexts/userContext";
import LangDropdown from "./LangDropdown";
import UserMenu from "@/components/UserMenu";
export interface MainNav1Props {
className?: string;
}
const MainNav1: FC<MainNav1Props> = ({ className = "" }) => {
const { user } = useUserContext();
console.log(Object.keys(user).length);
return (
<div className={`nc-MainNav1 relative z-10 ${className}`}>
<div className="px-4 lg:container h-20 relative flex justify-between">
@ -40,32 +32,21 @@ const MainNav1: FC<MainNav1Props> = ({ className = "" }) => {
<div className="hidden md:flex flex-shrink-0 justify-end flex-1 lg:flex-none text-neutral-700 dark:text-neutral-100">
<div className="hidden xl:flex space-x-0.5 items-center">
<LangDropdown />
<Link
href={`${Object.keys(user).length ? "/my-trips" : "signup"}`}
className="self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center"
>
<MdOutlineCardTravel size={25} />
</Link>
<Link
href="/my-trips"
className="self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center"
>
<MdOutlineCardTravel size={25} />
</Link>
<SwitchDarkMode />
<SearchDropdown className="flex items-center" />
<div className="px-1" />
{Object.keys(user).length ? (
<Link className="self-center" href={"/account"}>
<Avatar imgUrl={user?.avatar} sizeClass="w-10 h-10" />
</Link>
) : (
<div >
<Link className="mr-4 text-md" href={"/login"}>LogIn</Link>
<ButtonPrimary className="self-center" href="/signup">
Sign up
</ButtonPrimary>
</div>
)}
<UserMenu /> {/* Use the new UserMenu component here */}
</div>
<div className="flex xl:hidden items-center">
<div className="cursor-pointer self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center">
<div className="cursor-pointer self-center text-2xl md:text-[28px] w-12 h-12 rounded-full text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center">
<LangDropdown
className="flex"
panelClassName="z-10 w-screen max-w-[280px] px-4 mb-3 right-3 bottom-full sm:px-0"

4
src/app/[locale]/(client-components)/(Header)/SearchDropdown.tsx

@ -92,7 +92,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
>
<Popover.Panel
static
className="absolute right-0 z-10 top-full w-screen max-w-sm"
className="absolute right-0 z-10 top-full w-screen max-w-sm "
>
<Input
value={value}
@ -105,7 +105,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
placeholder="Type and search"
/>
{value.replaceAll(" ", "") && (
<Popover.Panel className="absolute right-0 z-10 top-full w-screen max-w-sm bg-white shadow-md rounded-3xl mt-1">
<Popover.Panel className="absolute right-0 z-10 top-full w-screen max-w-sm bg-white shadow-md rounded-3xl mt-1 dark:bg-neutral-900">
{filteredTours?.length ? (
filteredTours?.map((item) => (
<div

2
src/app/[locale]/(client-components)/(HeroSearchForm)/(stay-search-form)/StaySearchForm.tsx

@ -6,7 +6,7 @@ import StayDatesRangeInput from "./StayDatesRangeInput";
const StaySearchForm: FC<{}> = ({}) => {
const renderForm = () => {
return (
<form className="w-full relative mt-8 flex rounded-full shadow-xl dark:shadow-2xl bg-white dark:bg-neutral-800 ">
<form dir="ltr" className="w-full relative mt-8 flex rounded-full shadow-xl dark:shadow-2xl bg-white dark:bg-neutral-800 ">
<LocationInput className="flex-[1.5]" />
<div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div>
<StayDatesRangeInput className="flex-1" />

2
src/app/[locale]/(client-components)/(HeroSearchForm)/LocationInput.tsx

@ -103,7 +103,7 @@ const LocationInput: FC<LocationInputProps> = ({
className={`block w-full bg-transparent border-none focus:ring-0 p-0 focus:outline-none focus:placeholder-neutral-300 xl:text-lg font-semibold placeholder-neutral-800 dark:placeholder-neutral-200 truncate`}
>{value}</h2>
<span className="block mt-0.5 text-sm text-neutral-400 ">
<span className="text-black text-lg font-semibold">{!!value ? placeHolder : desc}</span>
<span className="text-black text-lg font-semibold dark:text-white">{!!value ? placeHolder : desc}</span>
</span>
{value && showPopover && (
<ClearDataButton

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

@ -45,7 +45,7 @@ const CommonLayout: FC<CommonLayoutProps> = ({ params }) => {
const totalPassengers = guestAdults + guestChildren + guestInfants
console.log(totalPassengers);
console.log(passengers);
const nextHref = () => setIndex((prev) => prev + 1);

75
src/app/[locale]/add-new-passenger/page.tsx

@ -9,6 +9,7 @@ import getImageURL from "@/components/api/getImageURL";
import axiosInstance from "@/components/api/axios";
import { useRouter } from "next/navigation";
import { useUserContext } from "@/components/contexts/userContext";
import { PhoneNumberUtil } from "google-libphonenumber";
export interface CommonLayoutProps {
params: {
@ -17,9 +18,11 @@ export interface CommonLayoutProps {
}
const CommonLayout: FC<CommonLayoutProps> = () => {
const {user} = useUserContext()
const { user } = useUserContext();
const router = useRouter();
const phoneUtil = PhoneNumberUtil.getInstance();
const [passenger, setPassenger] = useState({
name: "",
passport: "",
@ -33,18 +36,31 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
number: "",
date: "",
image: "",
request : ""
});
const [loading , setLoading] = useState(false)
const [loading, setLoading] = useState(false);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoading(true)
setLoading(true);
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
setLoading(false)
setLoading(false);
}
};
const validatePhoneNumber = () => {
try {
const parsedNumber = phoneUtil.parseAndKeepRawInput(passenger.number);
if (!phoneUtil.isValidNumber(parsedNumber)) {
return "Invalid phone number.";
}
return null;
} catch (error) {
return "Invalid phone number format.";
}
};
@ -79,9 +95,12 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
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.";
} else {
const phoneError = validatePhoneNumber();
if (phoneError) {
formIsValid = false;
errors.number = phoneError;
}
}
if (!passenger.image) {
@ -95,9 +114,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
const handleSavePassenger = async (passenger) => {
if (!validateForm()) return; // Validate before saving
try {
const response = await axiosInstance.post(
await axiosInstance.post(
`/api/account/passengers/`,
{
fullname: passenger.name,
@ -112,12 +131,14 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
},
}
);
router.push("/passengers-list");
} catch (error) {
} catch (error: any) {
console.error("Error saving passenger:", error);
setErrors((prevErrors) => ({ ...prevErrors, request: error.message }));
}
};
return (
<div
@ -147,7 +168,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
}}
placeholder="Full Name"
/>
{errors.name && <p className="text-red-500 text-xs">{errors.name}</p>}
{errors.name && (
<p className="text-red-500 text-xs">{errors.name}</p>
)}
</FormItem>
<FormItem label="Passport Number" desc="">
@ -165,7 +188,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
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>}
{errors.passport && (
<p className="text-red-500 text-xs">{errors.passport}</p>
)}
</FormItem>
<FormItem label="Date of Birth" desc="">
@ -182,7 +207,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
type="date"
placeholder="Date of Birth"
/>
{errors.date && <p className="text-red-500 text-xs">{errors.date}</p>}
{errors.date && (
<p className="text-red-500 text-xs">{errors.date}</p>
)}
</FormItem>
<FormItem label="Phone Number" desc="">
@ -196,11 +223,11 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
}));
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>}
{errors.number && (
<p className="text-red-500 text-xs">{errors.number}</p>
)}
</FormItem>
<FormItem label="Upload Passport Image Here" desc="">
@ -210,7 +237,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
type="file"
placeholder="Passport"
/>
{errors.image && <p className="text-red-500 text-xs">{errors.image}</p>}
{errors.image && (
<p className="text-red-500 text-xs">{errors.image}</p>
)}
{loading && <p>Loading ...</p>}
</FormItem>
</div>
@ -218,8 +247,14 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
</div>
{/* --------------------- */}
<div className="flex justify-end space-x-5">
<ButtonPrimary onClick={(e) => { e.preventDefault(); handleSavePassenger(passenger); }}>
{errors.request ?? (<p>{errors.request}</p>)}
<div className="flex justify-end space-x-5 mt-5">
<ButtonPrimary
onClick={(e) => {
e.preventDefault();
handleSavePassenger(passenger);
}}
>
Continue
</ButtonPrimary>
</div>

25
src/app/[locale]/blog/SectionMagazine5.tsx

@ -1,4 +1,4 @@
"use client"
"use client";
import React, { FC, useEffect, useState } from "react";
import Card12 from "./Card12";
@ -6,25 +6,24 @@ import Card13 from "./Card13";
import axiosInstance from "@/components/api/axios";
import { useTranslation } from "next-i18next";
const SectionMagazine5 = () => {
const { t } = useTranslation("common");
const [posts , setPosts] = useState([])
const [posts, setPosts] = useState([]);
useEffect(()=>{
axiosInstance.get("/api/blogs/")
.then((response)=>{
setPosts(response.data.results);
})
} , [])
useEffect(() => {
axiosInstance
.get("/api/blogs/")
.then((response) => {
setPosts(response.data.results);
})
.catch((error) => {
console.error(error.message);
});
}, []);
console.log(t("welcome"));
return (
<div className="nc-SectionMagazine5">
<div className="grid lg:grid-cols-2 gap-6 md:gap-8">
{posts[0] && <Card12 post={posts[0]} />}
<div className="grid gap-6 md:gap-8">
{posts

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

@ -13,7 +13,7 @@ import FooterNav from "@/components/FooterNav";
import { UserProvider } from "@/components/contexts/userContext";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import I18nProvider from "@/components/I18nProvider"; // Import the new component
import I18nProvider from "@/components/I18nProvider";
const poppins = Poppins({
subsets: ["latin"],
@ -21,6 +21,11 @@ const poppins = Poppins({
weight: ["300", "400", "500", "600", "700"],
});
function isRtlLocale(locale: string) {
const rtlLocales = ["ar", "he", "fa", "ur"];
return rtlLocales.includes(locale);
}
export default function LocaleLayout({
children,
params,
@ -29,28 +34,31 @@ export default function LocaleLayout({
params: { locale: string };
}) {
const { locale } = params;
const dir = isRtlLocale(locale) ? "rtl" : "ltr";
return (
<html lang={locale} className={poppins.className}>
<html lang={locale} dir={dir} className={poppins.className}>
<body className="bg-white text-base dark:bg-neutral-900 text-neutral-900 dark:text-neutral-200">
<ContextProvider>
<UserProvider>
<ClientCommons />
<SiteHeader />
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
{/* Wrap children with I18nProvider */}
<I18nProvider locale={locale}>{children}</I18nProvider>
<I18nProvider locale={locale}>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
{children}
</I18nProvider>
<FooterNav />
<Footer />
</UserProvider>

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

@ -13,6 +13,7 @@ import MobileFooterSticky from "@/components/MobileFooterSticky";
import ButtonPrimary from "@/shared/ButtonPrimary";
import StartRating from "@/components/StartRating";
import { useTranslation } from "react-i18next"; // Import useTranslation
import Head from "next/head";
// Define the type of `details`
interface TourDetails {
@ -49,7 +50,8 @@ const ListingStayDetailPage: FC = () => {
const { slug } = useParams(); // Assuming `slug` contains the tour ID
const id = slug?.match(/-?(\d+)$/)?.[1] || null;
const totalGuests = passengers.guestAdults + passengers.guestChildren + passengers.guestInfants;
const totalGuests =
passengers.guestAdults + passengers.guestChildren + passengers.guestInfants;
// Fetch details and itinerary on component mount
useEffect(() => {
@ -79,7 +81,8 @@ const ListingStayDetailPage: FC = () => {
const renderItinerarySection = () => {
return (
<div className="listingSection__wrap">
<h2 className="text-2xl font-semibold">{t("itinerary")}</h2> {/* Translate using the key */}
<h2 className="text-2xl font-semibold">{t("itinerary")}</h2>{" "}
{/* Translate using the key */}
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="flow-root">
<div className="text-sm sm:text-base text-neutral-600 dark:text-neutral-300 mb-4">
@ -92,7 +95,9 @@ const ListingStayDetailPage: FC = () => {
)}
</div>
<div className="p-5 rounded-3xl mb-4 border-2">
<h1 className="text-black font-bold mb-1 dark:text-white">{item.title}</h1>
<h1 className="text-black font-bold mb-1 dark:text-white">
{item.title}
</h1>
<span>{item.started_at.replace(/T/, " | ")}</span>
<div className="mt-3">{item.summary}</div>
</div>
@ -107,7 +112,8 @@ const ListingStayDetailPage: FC = () => {
const renderTourFeatures = () => {
return (
<div className="listingSection__wrap">
<h2 className="text-2xl font-semibold">{t("tourFeatures")}</h2> {/* Translate using the key */}
<h2 className="text-2xl font-semibold">{t("tourFeatures")}</h2>{" "}
{/* Translate using the key */}
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="grid gap-6 text-sm text-neutral-700 dark:text-neutral-300">
{details?.tour_features?.map((feature) => (
@ -122,10 +128,15 @@ const ListingStayDetailPage: FC = () => {
};
const renderSidebar = () => {
const total = details?.final_price && passengers
? (Number(details?.final_price) * totalGuests).toLocaleString("en-US", { style: "currency", currency: "USD" })
: 0;
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);
return (
<div className="listingSectionSidebar__wrap shadow-xl">
{/* Price display */}
@ -134,11 +145,13 @@ const ListingStayDetailPage: FC = () => {
<span className="text-3xl font-semibold">${details?.price}</span>
) : (
<div>
<span className="mr-2 text-3xl font-semibold">${details?.final_price}</span>
<span className="mr-2 text-3xl font-semibold">
${details?.final_price}
</span>
<span className="line-through">${details?.price}</span>
</div>
)}
<StartRating />
{/* <StartRating /> */}
</div>
{/* Booking Form */}
@ -156,7 +169,11 @@ const ListingStayDetailPage: FC = () => {
{/* Reserve Button */}
<ButtonPrimary
className={details?.status === "AVAILABLE" ? "" : "opacity-60 pointer-events-none"}
className={
details?.status === "AVAILABLE"
? ""
: "opacity-60 pointer-events-none"
}
href={`/add-listing/${id}`}
>
{t("reserve")} {/* Translate reserve button text */}
@ -167,6 +184,28 @@ const ListingStayDetailPage: FC = () => {
return (
<div className="nc-ListingStayDetailPage">
<Head>
<title>
{details?.title ? `${details.title} - My Tours` : "Tour Details"}
</title>
<meta
name="description"
content={
details?.description || "Explore our tour options and book today!"
}
/>
<meta property="og:title" content={details?.title || "Tour Details"} />
<meta
property="og:description"
content={
details?.description || "Explore our tour options and book today!"
}
/>
<meta
property="og:image"
content={details?.images[0]?.image_url.lg || "/default-image.jpg"}
/>
</Head>
{/* Header Section with Images */}
<header className="rounded-md sm:rounded-xl">
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2">
@ -185,17 +224,33 @@ const ListingStayDetailPage: FC = () => {
{/* Additional Images */}
{details?.images.slice(1, 5).map((item, index) => (
<div key={index} className={`relative rounded-md sm:rounded-xl overflow-hidden ${index >= 3 ? "hidden sm:block" : ""}`}>
<div
key={index}
className={`relative rounded-md sm:rounded-xl overflow-hidden ${
index >= 3 ? "hidden sm:block" : ""
}`}
>
<ConfirmModal
lable={
<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" />
<Image
fill
className="object-cover rounded-md sm:rounded-xl"
src={item.image_url.lg}
alt=""
sizes="400px"
/>
</div>
}
>
{() => (
<div className="h-[600px] w-[600px]">
<Image fill className="object-cover rounded-md sm:rounded-xl" src={item.image_url.lg} alt="" />
<Image
fill
className="object-cover rounded-md sm:rounded-xl"
src={item.image_url.lg}
alt=""
/>
</div>
)}
</ConfirmModal>

9
src/components/MobileFooterSticky.tsx

@ -5,7 +5,7 @@ import converSelectedDateToString from "@/utils/converSelectedDateToString";
import ModalReserveMobile from "./ModalReserveMobile";
import { useParams } from "next/navigation";
import { useToursContext } from "@/components/contexts/tourDetails";
import GuestsInput from "@/app/[locale]/(client-components)/(HeroSearchForm)/GuestsInput";
import GuestsInput from "@/app/[locale]/tours/[slug]/GuestsInput";
const MobileFooterSticky = () => {
const [startDate, setStartDate] = useState<Date | null>(
@ -30,12 +30,13 @@ const MobileFooterSticky = () => {
// Calculate total based on guestAdults and data.price
const totalGuests = guestAdultsInputValue;
const totalPrice = details?.price
? (Number(details.price) * totalGuests).toLocaleString("en-US", {
const totalPrice =
details?.final_price && passengers
? (Number((passengers.guestAdults) + (passengers.guestChildren) + (passengers.guestInfants * details.price_infant))).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})
: "N/A";
: 0;
return (
<div className="block lg:hidden fixed bottom-0 inset-x-0 py-2 sm:py-3 bg-white dark:bg-neutral-800 border-t border-neutral-200 dark:border-neutral-600 z-40">

2
src/components/SearchCard.tsx

@ -44,7 +44,7 @@ function SearchCard({ data }: { data: any }) {
const tripDuration = calculateDuration(data.started_at, data.ended_at);
return (
<Link href={`/tours/${data?.slug}-${data?.id}`} className="flex hover:bg-neutral-100 p-4 cursor-pointer rounded-3xl">
<Link href={`/tours/${data?.slug}-${data?.id}`} className="flex hover:bg-neutral-100 p-4 cursor-pointer rounded-3xl dark:hover:bg-neutral-800">
<div className="h-30 inset-0 w-30 h-30 overflow-hidden rounded-xl relative">
<Image
className="max-h-16 object-cover"

3
src/components/SectionGridFeaturePlaces.tsx

@ -81,7 +81,8 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
setCurrentIndex(newVal);
}
console.log(tours);
const handlers = useSwipeable({
onSwipedLeft: () => {
if (currentIndex < countryTours?.length - 1) {

42
src/components/UserMenu.tsx

@ -0,0 +1,42 @@
"use client";
import React, { FC, useEffect, useState } from "react";
import { useUserContext } from "@/components/contexts/userContext";
import Avatar from "@/shared/Avatar";
import Link from "next/link";
import ButtonPrimary from "@/shared/ButtonPrimary";
import { useTranslation } from "react-i18next"; // Import useTranslation
export interface UserMenuProps {
className?: string;
}
const UserMenu: FC<UserMenuProps> = ({ className = "" }) => {
const { user } = useUserContext();
const { t } = useTranslation("common"); // Initialize useTranslation with the "common" namespace
const [translation , setTranslation] = useState({logIn : "Log In" , signUp : "Sign Up"})
useEffect(() => {
setTranslation({ logIn :t("login") , signUp : t("signup")})
}, [t]);
return (
<div className={`nc-UserMenu ${className}`}>
{Object.keys(user).length ? (
<Link className="self-center" href="/account">
<Avatar imgUrl={user?.avatar} sizeClass="w-10 h-10" />
</Link>
) : (
<div className="flex items-center space-x-4">
<Link className="text-md" href="/login">
{translation.logIn}
</Link>
<ButtonPrimary className="self-center" href="/signup">
{translation.signUp}
</ButtonPrimary>
</div>
)}
</div>
);
};
export default UserMenu;

58
src/components/api/axios.tsx

@ -1,26 +1,38 @@
import axios from "axios";
import Cookies from "js-cookie"; // Import js-cookie
const axiosInstance= axios.create({
baseURL : "https://aqila.nwhco.ir/"
})
axiosInstance.interceptors.request.use(
(config) => {
return config;
},
(error) => {
console.error('Request Error:', error);
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
console.error('Response Error:', error);
return Promise.reject(error);
}
);
// Create an axios instance
const axiosInstance = axios.create({
baseURL: "https://aqila.nwhco.ir/",
});
export default axiosInstance;
// Request interceptor to dynamically set language_code from the locale cookie
axiosInstance.interceptors.request.use(
(config) => {
// Read the locale cookie
const locale = Cookies.get("locale") || "en"; // Default to "en" if cookie is not set
// Set the language_code param based on the locale
config.params = {
...config.params, // Include any existing params
language_code: locale, // Set the language_code
};
return config;
},
(error) => {
console.error("Request Error:", error);
return Promise.reject(error);
}
);
// Response interceptor to handle response errors
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
console.error("Response Error:", error);
return Promise.reject(error);
}
);
export default axiosInstance;

14
src/components/contexts/tourDetails.tsx

@ -2,7 +2,13 @@
import { usePathname } from "next/navigation";
import axiosInstance from "../api/axios";
import React, { createContext, useContext, useEffect, useState, ReactNode } from "react";
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
interface ImageURL {
sm: string;
@ -43,6 +49,8 @@ interface TourDetails {
final_price: string | number | undefined;
images: Image[];
trip_status: string;
price_infant: string | number;
price_child: string | number;
}
interface Country {
@ -104,8 +112,10 @@ export const ContextProvider = ({ children }: ContextProviderProps) => {
useEffect(() => {
axiosInstance
.get("/api/tours/")
.get("api/tours")
.then((response) => {
console.log(response);
setTours({ results: response.data.results });
})
.catch((error) => {

5
src/components/contexts/userContext.tsx

@ -11,6 +11,8 @@ type UserContextType = {
method: any[];
setMethod: (method: any[]) => void;
clerUser: () => void;
locale : string ;
setLocale : (method:string) => void;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
@ -23,6 +25,7 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
const [form, setForm] = useState<Record<string, any>>({});
const [user, setUser] = useState<Record<string, any>>({});
const [method, setMethod] = useState<any[]>([]);
const [locale , setLocale] = useState("en")
const router = useRouter()
// Load user from localStorage on initial render
useEffect(() => {
@ -59,6 +62,8 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
user,
setUser,
clerUser,
locale,
setLocale
}}
>
{children}

4
src/i18n.ts

@ -6,11 +6,13 @@ import { initReactI18next } from 'react-i18next';
import enCommon from '../public/locales/en/common.json';
import enFAQ from '../public/locales/en/FAQ.json';
import frCommon from '../public/locales/fr/common.json';
import arCommon from '../public/locales/ar/common.json';
// import viCommon from '../public/locales/vi/common.json';
const resources = {
en: { common: enCommon , FAQ: enFAQ },
fr: { common: frCommon },
ar: { common: arCommon },
// vi: { common: viCommon },
};
@ -19,7 +21,7 @@ if (!i18n.isInitialized) {
.use(initReactI18next)
.init({
resources,
lng: 'en', // You can set this dynamically
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,

15
src/middleware.ts

@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/;
const locales = ['en', 'vi', 'fr'];
const locales = ['en', 'vi', 'fr', 'ar'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
@ -13,18 +13,19 @@ export function middleware(request: NextRequest) {
return NextResponse.next();
}
// Get the locale from the cookie or fall back to defaultLocale
const locale = request.cookies.get('locale')?.value || defaultLocale;
// Check if the locale is already set in the URL
const pathnameParts = pathname.split('/');
const hasLocale = locales.includes(pathnameParts[1]);
if (!hasLocale) {
// Handle root path separately to avoid double slashes
const newPathname = pathname === '/' ? `/${defaultLocale}` : `/${defaultLocale}${pathname}`;
const newPathname =
pathname === '/' ? `/${locale}` : `/${locale}${pathname}`;
const url = new URL(`${newPathname}${search}`, request.url);
// Optionally, log for debugging
// console.log('Redirecting to:', url.toString());
return NextResponse.redirect(url);
}
@ -34,7 +35,5 @@ export function middleware(request: NextRequest) {
// Matcher configuration remains the same
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

2
src/shared/Navigation/Navigation.tsx

@ -11,7 +11,7 @@ function Navigation() {
{NAVIGATION_DEMO.map((item) => (
<NavigationItem key={item.id} menuItem={item} />
))}
<Link href="/custom-trip" className="m-5 self-center border border-gray-300 p-1.5 rounded-full px-4 hover:bg-bronze hover:text-white">Custom Tour</Link>
<Link href="custom-trip" className="m-5 self-center border border-gray-300 p-1.5 rounded-full px-4 hover:bg-bronze hover:text-white">Custom Tour</Link>
</ul>
);
}

Loading…
Cancel
Save