Browse Source

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

fix(bills) : bills page connected to API
main
sina_sajjadi 2 weeks 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. 113
      public/locales/fr/common.json
  6. 85
      src/app/[locale]/(account-pages)/bills/BillCard.tsx
  7. 179
      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. 27
      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. 69
      src/app/[locale]/add-new-passenger/page.tsx
  16. 21
      src/app/[locale]/blog/SectionMagazine5.tsx
  17. 16
      src/app/[locale]/layout.tsx
  18. 79
      src/app/[locale]/tours/[slug]/page.tsx
  19. 9
      src/components/MobileFooterSticky.tsx
  20. 2
      src/components/SearchCard.tsx
  21. 1
      src/components/SectionGridFeaturePlaces.tsx
  22. 42
      src/components/UserMenu.tsx
  23. 32
      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 = { module.exports = {
i18n: { i18n: {
locales: ['en', 'vi', 'fr'], // List all supported locales
locales: ['en', 'vi', 'fr' , 'ar'], // List all supported locales
defaultLocale: 'en', // Set the default locale defaultLocale: 'en', // Set the default locale
}, },
}; };

1
next.config.js

@ -5,7 +5,6 @@ const { i18n } = require("./next-i18next.config");
module.exports = { module.exports = {
eslint: { eslint: {
ignoreDuringBuilds: false, 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", "addDestination": "Add Destination",
"continue": "Continue", "continue": "Continue",
"successMessage": "Successfully Registered", "successMessage": "Successfully Registered",
"to": "to"
"to": "to",
"login": "LogIn",
"signup": "Sign Up"
} }

113
public/locales/fr/common.json

@ -3,65 +3,114 @@
"allTours": "Tous les Tours", "allTours": "Tous les Tours",
"blogs": "Blogs", "blogs": "Blogs",
"faq": "FAQ", "faq": "FAQ",
"aboutUs": propos de nous",
"customTour": "Visite sur mesure",
"aboutUs": Propos de Nous",
"customTour": "Tour Personnalisé",
"searchPlaceholder": "Où aller ?", "searchPlaceholder": "Où aller ?",
"searchDescription": "Partout • À toute semaine • Ajouter des invités",
"searchDescription": "Partout • N'importe quelle semaine • Ajouter des invités",
"beginAdventure": "Commencez votre aventure spirituelle", "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",
"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", "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",
"exploreTours": "Explorez les tours et accommodations adaptées pour un voyage spirituel et mémorable",
"tourPeriod": "Période du Tour",
"tourPeriodDescription": "Début - Fin", "tourPeriodDescription": "Début - Fin",
"guests": "Invités", "guests": "Invités",
"addGuests": "Ajouter des invités", "addGuests": "Ajouter des invités",
"available": "Disponible", "available": "Disponible",
"soldOut": "Épuisé", "soldOut": "Épuisé",
"showMore": "Montrez-moi plus", "showMore": "Montrez-moi plus",
"happeningCities": "Villes en vogue",
"costEffectiveAdvertising": "Publicité rentable",
"costEffectiveDescription": "Avec une annonce gratuite, vous pouvez promouvoir votre location sans frais initiaux",
"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", "reachMillions": "Atteignez des millions avec Chisfis",
"reachMillionsDescription": "Des millions de personnes recherchent des lieux uniques où séjourner à travers le monde", "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",
"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", "mobileApps": "Applications Mobiles",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.", "mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
"installation": "Installation", "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",
"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", "prototyping": "Prototypage",
"designSystems": "Systèmes de design",
"designSystems": "Systèmes de Design",
"pricing": "Tarification", "pricing": "Tarification",
"security": "Sécurité", "security": "Sécurité",
"bestPractices": "Meilleures pratiques",
"bestPractices": "Meilleures Pratiques",
"support": "Support", "support": "Support",
"developers": "Développeurs", "developers": "Développeurs",
"learnDesign": "Apprendre la conception",
"learnDesign": "Apprendre le Design",
"releases": "Versions", "releases": "Versions",
"discussionForums": "Forums de discussion",
"codeOfConduct": "Code de conduite",
"communityResources": "Ressources communautaires",
"discussionForums": "Forums de Discussion",
"codeOfConduct": "Code de Conduite",
"communityResources": "Ressources Communautaires",
"contributing": "Contribuer", "contributing": "Contribuer",
"concurrentMode": "Mode concurrent",
"goodNews": "Bonne nouvelle de loin",
"concurrentMode": "Mode Concurrent",
"goodNews": "Bonnes Nouvelles d'Ailleurs",
"whatPeopleThink": "Voyons ce que les gens pensent de Chisfis", "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 !",
"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", "clientName": "Tiana Abie",
"clientLocation": "Malaisie", "clientLocation": "Malaisie",
"myTrips": "Mes Voyages", "myTrips": "Mes Voyages",
"account": "Compte", "account": "Compte",
"menu": "Menu", "menu": "Menu",
"gettingStarted": "Commencer",
"gettingStarted": "Premiers Pas",
"explore": "Explorer", "explore": "Explorer",
"resources": "Ressources", "resources": "Ressources",
"community": "Communauté", "community": "Communauté",
"placeType": "Type de lieu",
"noTours": "Aucun tour disponible"
}
"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 // BillCard.tsx
import React from "react"; import React from "react";
import ButtonPrimary from "@/shared/ButtonPrimary"; 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 { BiSolidPlaneAlt } from "react-icons/bi";
import { FaSimCard } from "react-icons/fa"; import { FaSimCard } from "react-icons/fa";
import { BsCart3 } from "react-icons/bs"; import { BsCart3 } from "react-icons/bs";
import { MdCurrencyExchange } from "react-icons/md"; import { MdCurrencyExchange } from "react-icons/md";
export interface Bill { export interface Bill {
title: string;
issuedDate: string;
expirationDate: string;
service: string;
created_at: string;
expiration_date: string;
amount: number; amount: number;
status: BillStatus; status: BillStatus;
} }
interface BillCardProps { interface BillCardProps {
bill: Bill; bill: Bill;
onViewDetail: (bill: Bill) => void; // Add onViewDetail prop 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 }) => { const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
console.log(bill);
return ( 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"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center space-x-2"> <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>
</h2>
<span className={`px-2 py-1 text-sm rounded-full ${statusStyles[bill.status]}`}>
{bill.status}
{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> </span>
)}
{bill.service.includes("Tasrif") && (
<span>
<MdCurrencyExchange className="text-bronze" size={25} />
</span>
)}
<span>{bill.service}</span>
</h2>
{statusStyles[bill.status]}
</div> </div>
<div className="mt-4 text-sm text-gray-600"> <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>Issued Date:</span>
<span>{bill.issuedDate}</span>
<span>{bill.created_at}</span>
</div> </div>
<div className="flex justify-between mb-2">
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>Expiration Date:</span> <span>Expiration Date:</span>
<span>{bill.expirationDate}</span>
<span>{bill.expiration_date}</span>
</div> </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>Tour Invoice Amount:</span>
<span className="text-orange-600">${bill.amount.toFixed(2)}</span>
<span className="text-orange-600">${bill.amount}</span>
</div> </div>
</div> </div>

179
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 ButtonPrimary from "@/shared/ButtonPrimary";
import Button from "@/shared/Button"; 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 { export interface Bill {
id: number;
title: string; title: string;
issuedDate: string;
expirationDate: string;
created_at: string;
expiration_date: string;
amount: number; amount: number;
status: BillStatus; status: BillStatus;
passengers: { type: string; count: number }[]; passengers: { type: string; count: number }[];
@ -20,29 +29,84 @@ export interface Bill {
size: string; size: string;
src: string; src: string;
}; };
detail_service: {
passenger_counts: { adults: string; children: string; infants: string };
};
transaction_receipt: string;
card_number: string | number;
} }
interface BillDetailCardProps { interface BillDetailCardProps {
bill: Bill; 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 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 ( 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"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{bill.title}</h2> <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> </div>
{bill.message && ( {bill.message && (
@ -52,23 +116,33 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
</div> </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"> <div className="flex justify-between mb-2">
<span>Issued Date:</span> <span>Issued Date:</span>
<span>{bill.issuedDate}</span>
<span>{bill.created_at}</span>
</div> </div>
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span>Expiration Date:</span> <span>Expiration Date:</span>
<span>{bill.expirationDate}</span>
<span>{bill.expiration_date}</span>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<h3 className="font-semibold">Number of Passengers</h3> <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 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> </div>
<div className="flex justify-between mt-4 font-semibold"> <div className="flex justify-between mt-4 font-semibold">
@ -78,8 +152,10 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
<div className="mt-4"> <div className="mt-4">
<h3 className="font-semibold">Account Number</h3> <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> <Button className="text-blue-500">Copy</Button>
</div> </div>
</div> </div>
@ -87,25 +163,60 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
{bill.uploadedImage ? ( {bill.uploadedImage ? (
<div className="mt-4"> <div className="mt-4">
<h3 className="font-semibold">Uploaded Image</h3> <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"> <div className="flex justify-between mt-2">
<span>{bill.uploadedImage.name}</span> <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> <Button className="text-red-500">Delete</Button>
</div> </div>
</div> </div>
) : ( ) : (
<div className="mt-4"> <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> </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> </div>
); );
}; };

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

@ -1,12 +1,18 @@
// BillsPage.tsx // BillsPage.tsx
"use client"
import React, { useState } from "react";
"use client";
import React, { useEffect, useState } from "react";
import BillCard from "./BillCard"; import BillCard from "./BillCard";
import BillDetailCard from "./[slug]/page"; 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 // types.ts
export type BillStatus = "Awaiting Payment" | "Approved" | "Rejected" | "Pending";
export type BillStatus =
| "Awaiting Payment"
| "Approved"
| "Rejected"
| "Pending";
export interface Bill { export interface Bill {
title: string; title: string;
@ -14,68 +20,68 @@ export interface Bill {
expirationDate: string; expirationDate: string;
amount: number; amount: number;
status: BillStatus; 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 BillsPage: React.FC = () => {
const [bills, setBills] = useState([]);
const [selectedBill, setSelectedBill] = useState<Bill | null>(null); const [selectedBill, setSelectedBill] = useState<Bill | null>(null);
const { user } = useUserContext();
const handleViewDetail = (bill: Bill) => { const handleViewDetail = (bill: Bill) => {
setSelectedBill(bill); setSelectedBill(bill);
@ -84,9 +90,23 @@ const BillsPage: React.FC = () => {
const handleBackToList = () => { const handleBackToList = () => {
setSelectedBill(null); 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 ( 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 ? ( {selectedBill ? (
<div> <div>
<button onClick={handleBackToList} className="text-blue-500 mb-4"> <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 { useRouter, usePathname } from "next/navigation";
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { MdOutlineLanguage } from "react-icons/md"; import { MdOutlineLanguage } from "react-icons/md";
import Cookies from "js-cookie"; // Import js-cookie
// Language options // Language options
const languageOptions = [ const languageOptions = [
@ -24,6 +25,12 @@ const languageOptions = [
description: "France", description: "France",
icon: <MdOutlineLanguage />, icon: <MdOutlineLanguage />,
}, },
{
id: "ar",
name: "Arabic",
description: "Arabic",
icon: <MdOutlineLanguage />,
},
]; ];
interface LangDropdownProps { interface LangDropdownProps {
@ -45,7 +52,7 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
// Update selectedItem based on the current locale in the URL // Update selectedItem based on the current locale in the URL
useEffect(() => { useEffect(() => {
const locales = ["en", "vi", "fr"];
const locales = ["en", "vi", "fr", "ar"];
const segments = pathname.split("/").filter(Boolean); const segments = pathname.split("/").filter(Boolean);
const currentLocale = locales.includes(segments[0]) ? segments[0] : "en"; const currentLocale = locales.includes(segments[0]) ? segments[0] : "en";
const newSelectedItem = const newSelectedItem =
@ -58,7 +65,7 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
const segments = pathname.split("/").filter(Boolean); const segments = pathname.split("/").filter(Boolean);
// Remove the current locale if present // Remove the current locale if present
const locales = ["en", "vi", "fr"];
const locales = ["en", "vi", "fr", "ar"];
if (locales.includes(segments[0])) { if (locales.includes(segments[0])) {
segments.shift(); segments.shift();
} }
@ -66,6 +73,9 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
// Prepend the new locale // Prepend the new locale
const newPathname = `/${locale}/${segments.join("/")}`; const newPathname = `/${locale}/${segments.join("/")}`;
// Set the locale cookie
Cookies.set("locale", locale, { expires: 365 });
// Navigate to the new locale path // Navigate to the new locale path
router.push(newPathname); router.push(newPathname);
}; };

27
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 Logo from "@/shared/Logo";
import Navigation from "@/shared/Navigation/Navigation"; import Navigation from "@/shared/Navigation/Navigation";
import SearchDropdown from "./SearchDropdown"; import SearchDropdown from "./SearchDropdown";
import ButtonPrimary from "@/shared/ButtonPrimary";
import MenuBar from "@/shared/MenuBar"; import MenuBar from "@/shared/MenuBar";
import SwitchDarkMode from "@/shared/SwitchDarkMode"; import SwitchDarkMode from "@/shared/SwitchDarkMode";
import HeroSearchForm2MobileFactory from "../(HeroSearchForm2Mobile)/HeroSearchForm2MobileFactory"; import HeroSearchForm2MobileFactory from "../(HeroSearchForm2Mobile)/HeroSearchForm2MobileFactory";
import { MdOutlineCardTravel } from "react-icons/md"; import { MdOutlineCardTravel } from "react-icons/md";
import Avatar from "@/shared/Avatar";
import Link from "next/link"; import Link from "next/link";
import { useUserContext } from "@/components/contexts/userContext";
import LangDropdown from "./LangDropdown"; import LangDropdown from "./LangDropdown";
import UserMenu from "@/components/UserMenu";
export interface MainNav1Props { export interface MainNav1Props {
className?: string; className?: string;
} }
const MainNav1: FC<MainNav1Props> = ({ className = "" }) => { const MainNav1: FC<MainNav1Props> = ({ className = "" }) => {
const { user } = useUserContext();
console.log(Object.keys(user).length);
return ( return (
<div className={`nc-MainNav1 relative z-10 ${className}`}> <div className={`nc-MainNav1 relative z-10 ${className}`}>
<div className="px-4 lg:container h-20 relative flex justify-between"> <div className="px-4 lg:container h-20 relative flex justify-between">
@ -41,7 +33,7 @@ const MainNav1: FC<MainNav1Props> = ({ className = "" }) => {
<div className="hidden xl:flex space-x-0.5 items-center"> <div className="hidden xl:flex space-x-0.5 items-center">
<LangDropdown /> <LangDropdown />
<Link <Link
href={`${Object.keys(user).length ? "/my-trips" : "signup"}`}
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" 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} /> <MdOutlineCardTravel size={25} />
@ -50,18 +42,7 @@ const MainNav1: FC<MainNav1Props> = ({ className = "" }) => {
<SwitchDarkMode /> <SwitchDarkMode />
<SearchDropdown className="flex items-center" /> <SearchDropdown className="flex items-center" />
<div className="px-1" /> <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>
<div className="flex xl:hidden items-center"> <div className="flex xl:hidden items-center">

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

@ -92,7 +92,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
> >
<Popover.Panel <Popover.Panel
static 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 <Input
value={value} value={value}
@ -105,7 +105,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
placeholder="Type and search" placeholder="Type and search"
/> />
{value.replaceAll(" ", "") && ( {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?.length ? (
filteredTours?.map((item) => ( filteredTours?.map((item) => (
<div <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 StaySearchForm: FC<{}> = ({}) => {
const renderForm = () => { const renderForm = () => {
return ( 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]" /> <LocationInput className="flex-[1.5]" />
<div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div> <div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div>
<StayDatesRangeInput className="flex-1" /> <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`} 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> >{value}</h2>
<span className="block mt-0.5 text-sm text-neutral-400 "> <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> </span>
{value && showPopover && ( {value && showPopover && (
<ClearDataButton <ClearDataButton

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

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

69
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 axiosInstance from "@/components/api/axios";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useUserContext } from "@/components/contexts/userContext"; import { useUserContext } from "@/components/contexts/userContext";
import { PhoneNumberUtil } from "google-libphonenumber";
export interface CommonLayoutProps { export interface CommonLayoutProps {
params: { params: {
@ -17,9 +18,11 @@ export interface CommonLayoutProps {
} }
const CommonLayout: FC<CommonLayoutProps> = () => { const CommonLayout: FC<CommonLayoutProps> = () => {
const {user} = useUserContext()
const { user } = useUserContext();
const router = useRouter(); const router = useRouter();
const phoneUtil = PhoneNumberUtil.getInstance();
const [passenger, setPassenger] = useState({ const [passenger, setPassenger] = useState({
name: "", name: "",
passport: "", passport: "",
@ -33,18 +36,31 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
number: "", number: "",
date: "", date: "",
image: "", image: "",
request : ""
}); });
const [loading , setLoading] = useState(false)
const [loading, setLoading] = useState(false);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoading(true)
setLoading(true);
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
const image = await getImageURL(file); const image = await getImageURL(file);
setPassenger((prev) => ({ ...prev, image: image.url })); setPassenger((prev) => ({ ...prev, image: image.url }));
setErrors((prev) => ({ ...prev, image: "" })); // Clear image error 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) { if (!passenger.number) {
formIsValid = false; formIsValid = false;
errors.number = "Phone Number is required."; errors.number = "Phone Number is required.";
} else if (!/^\d+$/.test(passenger.number)) {
} else {
const phoneError = validatePhoneNumber();
if (phoneError) {
formIsValid = false; formIsValid = false;
errors.number = "Phone Number must be numeric.";
errors.number = phoneError;
}
} }
if (!passenger.image) { if (!passenger.image) {
@ -97,7 +116,7 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
if (!validateForm()) return; // Validate before saving if (!validateForm()) return; // Validate before saving
try { try {
const response = await axiosInstance.post(
await axiosInstance.post(
`/api/account/passengers/`, `/api/account/passengers/`,
{ {
fullname: passenger.name, fullname: passenger.name,
@ -114,11 +133,13 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
); );
router.push("/passengers-list"); router.push("/passengers-list");
} catch (error) {
} catch (error: any) {
console.error("Error saving passenger:", error); console.error("Error saving passenger:", error);
setErrors((prevErrors) => ({ ...prevErrors, request: error.message }));
} }
}; };
return ( return (
<div <div
className={`nc-PageAddListing1 px-4 max-w-3xl mx-auto pb-24 pt-14 sm:py-24 lg:pb-32`} className={`nc-PageAddListing1 px-4 max-w-3xl mx-auto pb-24 pt-14 sm:py-24 lg:pb-32`}
@ -147,7 +168,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
}} }}
placeholder="Full Name" 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>
<FormItem label="Passport Number" desc=""> <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" className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
placeholder="Passport Number" 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>
<FormItem label="Date of Birth" desc=""> <FormItem label="Date of Birth" desc="">
@ -182,7 +207,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
type="date" type="date"
placeholder="Date of Birth" 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>
<FormItem label="Phone Number" desc=""> <FormItem label="Phone Number" desc="">
@ -196,11 +223,11 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
})); }));
setErrors((prev) => ({ ...prev, number: "" })); // Clear error on input change 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" 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>
<FormItem label="Upload Passport Image Here" desc=""> <FormItem label="Upload Passport Image Here" desc="">
@ -210,7 +237,9 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
type="file" type="file"
placeholder="Passport" 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>} {loading && <p>Loading ...</p>}
</FormItem> </FormItem>
</div> </div>
@ -218,8 +247,14 @@ const CommonLayout: FC<CommonLayoutProps> = () => {
</div> </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 Continue
</ButtonPrimary> </ButtonPrimary>
</div> </div>

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

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

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

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

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

@ -13,6 +13,7 @@ import MobileFooterSticky from "@/components/MobileFooterSticky";
import ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonPrimary from "@/shared/ButtonPrimary";
import StartRating from "@/components/StartRating"; import StartRating from "@/components/StartRating";
import { useTranslation } from "react-i18next"; // Import useTranslation import { useTranslation } from "react-i18next"; // Import useTranslation
import Head from "next/head";
// Define the type of `details` // Define the type of `details`
interface TourDetails { interface TourDetails {
@ -49,7 +50,8 @@ const ListingStayDetailPage: FC = () => {
const { slug } = useParams(); // Assuming `slug` contains the tour ID const { slug } = useParams(); // Assuming `slug` contains the tour ID
const id = slug?.match(/-?(\d+)$/)?.[1] || null; 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 // Fetch details and itinerary on component mount
useEffect(() => { useEffect(() => {
@ -79,7 +81,8 @@ const ListingStayDetailPage: FC = () => {
const renderItinerarySection = () => { const renderItinerarySection = () => {
return ( return (
<div className="listingSection__wrap"> <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="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="flow-root"> <div className="flow-root">
<div className="text-sm sm:text-base text-neutral-600 dark:text-neutral-300 mb-4"> <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>
<div className="p-5 rounded-3xl mb-4 border-2"> <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> <span>{item.started_at.replace(/T/, " | ")}</span>
<div className="mt-3">{item.summary}</div> <div className="mt-3">{item.summary}</div>
</div> </div>
@ -107,7 +112,8 @@ const ListingStayDetailPage: FC = () => {
const renderTourFeatures = () => { const renderTourFeatures = () => {
return ( return (
<div className="listingSection__wrap"> <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="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"> <div className="grid gap-6 text-sm text-neutral-700 dark:text-neutral-300">
{details?.tour_features?.map((feature) => ( {details?.tour_features?.map((feature) => (
@ -122,9 +128,14 @@ const ListingStayDetailPage: FC = () => {
}; };
const renderSidebar = () => { const renderSidebar = () => {
const total = details?.final_price && passengers
? (Number(details?.final_price) * totalGuests).toLocaleString("en-US", { style: "currency", currency: "USD" })
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; : 0;
console.log(details);
return ( return (
<div className="listingSectionSidebar__wrap shadow-xl"> <div className="listingSectionSidebar__wrap shadow-xl">
@ -134,11 +145,13 @@ const ListingStayDetailPage: FC = () => {
<span className="text-3xl font-semibold">${details?.price}</span> <span className="text-3xl font-semibold">${details?.price}</span>
) : ( ) : (
<div> <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> <span className="line-through">${details?.price}</span>
</div> </div>
)} )}
<StartRating />
{/* <StartRating /> */}
</div> </div>
{/* Booking Form */} {/* Booking Form */}
@ -156,7 +169,11 @@ const ListingStayDetailPage: FC = () => {
{/* Reserve Button */} {/* Reserve Button */}
<ButtonPrimary <ButtonPrimary
className={details?.status === "AVAILABLE" ? "" : "opacity-60 pointer-events-none"}
className={
details?.status === "AVAILABLE"
? ""
: "opacity-60 pointer-events-none"
}
href={`/add-listing/${id}`} href={`/add-listing/${id}`}
> >
{t("reserve")} {/* Translate reserve button text */} {t("reserve")} {/* Translate reserve button text */}
@ -167,6 +184,28 @@ const ListingStayDetailPage: FC = () => {
return ( return (
<div className="nc-ListingStayDetailPage"> <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 Section with Images */}
<header className="rounded-md sm:rounded-xl"> <header className="rounded-md sm:rounded-xl">
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2"> <div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2">
@ -185,17 +224,33 @@ const ListingStayDetailPage: FC = () => {
{/* Additional Images */} {/* Additional Images */}
{details?.images.slice(1, 5).map((item, index) => ( {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 <ConfirmModal
lable={ lable={
<div className="aspect-w-4 aspect-h-3 sm:aspect-w-6 sm:aspect-h-5"> <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>
} }
> >
{() => ( {() => (
<div className="h-[600px] w-[600px]"> <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> </div>
)} )}
</ConfirmModal> </ConfirmModal>

9
src/components/MobileFooterSticky.tsx

@ -5,7 +5,7 @@ import converSelectedDateToString from "@/utils/converSelectedDateToString";
import ModalReserveMobile from "./ModalReserveMobile"; import ModalReserveMobile from "./ModalReserveMobile";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useToursContext } from "@/components/contexts/tourDetails"; 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 MobileFooterSticky = () => {
const [startDate, setStartDate] = useState<Date | null>( const [startDate, setStartDate] = useState<Date | null>(
@ -30,12 +30,13 @@ const MobileFooterSticky = () => {
// Calculate total based on guestAdults and data.price // Calculate total based on guestAdults and data.price
const totalGuests = guestAdultsInputValue; 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", style: "currency",
currency: "USD", currency: "USD",
}) })
: "N/A";
: 0;
return ( 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"> <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); const tripDuration = calculateDuration(data.started_at, data.ended_at);
return ( 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"> <div className="h-30 inset-0 w-30 h-30 overflow-hidden rounded-xl relative">
<Image <Image
className="max-h-16 object-cover" className="max-h-16 object-cover"

1
src/components/SectionGridFeaturePlaces.tsx

@ -81,6 +81,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
setCurrentIndex(newVal); setCurrentIndex(newVal);
} }
console.log(tours);
const handlers = useSwipeable({ const handlers = useSwipeable({
onSwipedLeft: () => { onSwipedLeft: () => {

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;

32
src/components/api/axios.tsx

@ -1,26 +1,38 @@
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie"; // Import js-cookie
const axiosInstance= axios.create({
baseURL : "https://aqila.nwhco.ir/"
})
// Create an axios instance
const axiosInstance = axios.create({
baseURL: "https://aqila.nwhco.ir/",
});
axiosInstance.interceptors.request.use(
// Request interceptor to dynamically set language_code from the locale cookie
axiosInstance.interceptors.request.use(
(config) => { (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; return config;
}, },
(error) => { (error) => {
console.error('Request Error:', error);
console.error("Request Error:", error);
return Promise.reject(error); return Promise.reject(error);
} }
);
);
axiosInstance.interceptors.response.use(
// Response interceptor to handle response errors
axiosInstance.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
console.error('Response Error:', error);
console.error("Response Error:", error);
return Promise.reject(error); return Promise.reject(error);
} }
);
);
export default axiosInstance; export default axiosInstance;

14
src/components/contexts/tourDetails.tsx

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

5
src/components/contexts/userContext.tsx

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

4
src/i18n.ts

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

15
src/middleware.ts

@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/; const PUBLIC_FILE = /\.(.*)$/;
const locales = ['en', 'vi', 'fr'];
const locales = ['en', 'vi', 'fr', 'ar'];
const defaultLocale = 'en'; const defaultLocale = 'en';
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
@ -13,18 +13,19 @@ export function middleware(request: NextRequest) {
return NextResponse.next(); 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 // Check if the locale is already set in the URL
const pathnameParts = pathname.split('/'); const pathnameParts = pathname.split('/');
const hasLocale = locales.includes(pathnameParts[1]); const hasLocale = locales.includes(pathnameParts[1]);
if (!hasLocale) { if (!hasLocale) {
// Handle root path separately to avoid double slashes // 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); const url = new URL(`${newPathname}${search}`, request.url);
// Optionally, log for debugging
// console.log('Redirecting to:', url.toString());
return NextResponse.redirect(url); return NextResponse.redirect(url);
} }
@ -34,7 +35,5 @@ export function middleware(request: NextRequest) {
// Matcher configuration remains the same // Matcher configuration remains the same
export const config = { 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) => ( {NAVIGATION_DEMO.map((item) => (
<NavigationItem key={item.id} menuItem={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> </ul>
); );
} }

Loading…
Cancel
Save