Browse Source

feat(localization): add new navigation and footer translations for multiple languages

main
sina_sajjadi 2 weeks ago
parent
commit
4e3b7c55b6
  1. 2
      next-i18next.config.js
  2. 24
      public/locales/ar/FAQ.json
  3. 332
      public/locales/ar/common.json
  4. 23
      public/locales/ar/footer.json
  5. 91
      public/locales/ar/form.json
  6. 15
      public/locales/ar/navigation.json
  7. 47
      public/locales/en/FAQ.json
  8. 155
      public/locales/en/common.json
  9. 23
      public/locales/en/footer.json
  10. 91
      public/locales/en/form.json
  11. 15
      public/locales/en/navigation.json
  12. 116
      public/locales/fr/common.json
  13. 24
      public/locales/id/FAQ.json
  14. 222
      public/locales/id/common.json
  15. 23
      public/locales/id/footer.json
  16. 91
      public/locales/id/form.json
  17. 15
      public/locales/id/navigation.json
  18. 24
      public/locales/ru/FAQ.json
  19. 222
      public/locales/ru/common.json
  20. 23
      public/locales/ru/footer.json
  21. 91
      public/locales/ru/form.json
  22. 15
      public/locales/ru/navigation.json
  23. 24
      src/app/[locale]/(account-pages)/(components)/Nav.tsx
  24. 122
      src/app/[locale]/(account-pages)/account/page.tsx
  25. 96
      src/app/[locale]/(account-pages)/bills/BillCard.tsx
  26. 177
      src/app/[locale]/(account-pages)/bills/[slug]/page.tsx
  27. 16
      src/app/[locale]/(account-pages)/bills/page.tsx
  28. 48
      src/app/[locale]/(account-pages)/my-trips/page.tsx
  29. 15
      src/app/[locale]/(account-pages)/passengers-list/PassengerTable.tsx
  30. 301
      src/app/[locale]/(account-pages)/passengers-list/page.tsx
  31. 31
      src/app/[locale]/(client-components)/(Header)/LangDropdown.tsx
  32. 2
      src/app/[locale]/(client-components)/(Header)/LangDropdownSingle.tsx
  33. 4
      src/app/[locale]/(client-components)/(Header)/MainNav1.tsx
  34. 15
      src/app/[locale]/(client-components)/(Header)/SearchDropdown.tsx
  35. 7
      src/app/[locale]/(client-components)/(HeroSearchForm)/(stay-search-form)/StayDatesRangeInput.tsx
  36. 4
      src/app/[locale]/(client-components)/(HeroSearchForm)/(stay-search-form)/StaySearchForm.tsx
  37. 35
      src/app/[locale]/(client-components)/(HeroSearchForm)/GuestsInput.tsx
  38. 38
      src/app/[locale]/(home)/SectionDowloadApp.tsx
  39. 2
      src/app/[locale]/about/SectionHero.tsx
  40. 2
      src/app/[locale]/about/page.tsx
  41. 4
      src/app/[locale]/add-listing/[[...stepIndex]]/page.tsx
  42. 109
      src/app/[locale]/forgot-password/page.tsx
  43. 78
      src/app/[locale]/login/page.tsx
  44. 49
      src/app/[locale]/signup/methodes/page.tsx
  45. 112
      src/app/[locale]/signup/otp-code/page.tsx
  46. 106
      src/app/[locale]/signup/page.tsx
  47. 74
      src/app/[locale]/tours/SectionGridFilterCard.tsx
  48. 29
      src/app/[locale]/tours/[slug]/page.tsx
  49. 13
      src/app/globals.css
  50. 2
      src/components/BackgroundSection.tsx
  51. 21
      src/components/CardCategory3.tsx
  52. 96
      src/components/Footer.tsx
  53. 10
      src/components/HeaderFilter.tsx
  54. 93
      src/components/SectionClientSay.tsx
  55. 31
      src/components/SectionCustomTour.tsx
  56. 35
      src/components/SectionGridFeaturePlaces.tsx
  57. 76
      src/components/SectionHowItWork.tsx
  58. 34
      src/components/SectionOurFeatures.tsx
  59. 149
      src/components/TourSuggestion.tsx
  60. 10
      src/data/navigation.ts
  61. 24
      src/hooks/FormValidation.ts
  62. 25
      src/i18n.ts
  63. BIN
      src/images/HIW1.webp
  64. BIN
      src/images/HIW2.webp
  65. BIN
      src/images/HIW3.webp
  66. 27
      src/middleware.ts
  67. 20
      src/routers/types.ts
  68. 2
      src/shared/Button.tsx
  69. 2
      src/shared/Logo.tsx
  70. 5
      src/shared/Navigation/Navigation.tsx
  71. 6
      src/shared/Navigation/NavigationItem.tsx

2
next-i18next.config.js

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

24
public/locales/ar/FAQ.json

@ -0,0 +1,24 @@
{
"faqTitle": "الأسئلة الشائعة",
"faqSubtitle": "هل لديك أسئلة؟ نحن هنا للمساعدة!",
"faqQuestion1": "كيف يمكنني حجز جولة على موقعكم؟",
"faqAnswer1": "لحجز جولة، ببساطة اختر الوجهة التي ترغب فيها من 'قائمة الجولات'، اختر التواريخ الخاصة بك، واتبع الخطوات لإكمال عملية الحجز.",
"faqQuestion2": "هل يمكنني تخصيص جولتي؟",
"faqAnswer2": "نعم، تتيح لك ميزة 'الجولة المخصصة' تخصيص رحلتك بناءً على تفضيلاتك. انقر على 'الجولة المخصصة' في أعلى الصفحة لبدء تخصيص تجربتك.",
"faqQuestion3": "ما طرق الدفع التي تقبلها؟",
"faqAnswer3": "نقبل جميع بطاقات الائتمان الرئيسية، باي بال، والتحويلات البنكية. يمكنك اختيار الطريقة المفضلة لديك أثناء عملية الدفع.",
"faqQuestion4": "كيف يمكنني معرفة ما إذا تم تأكيد حجزي؟",
"faqAnswer4": "بمجرد إتمام عملية الدفع، ستتلقى رسالة تأكيد عبر البريد الإلكتروني تحتوي على جميع تفاصيل حجزك. يمكنك أيضًا عرض تفاصيل الحجز في لوحة التحكم الخاصة بحسابك.",
"faqQuestion5": "هل يمكنني إلغاء أو تعديل حجزتي؟",
"faqAnswer5": "نعم، يمكنك إلغاء أو تعديل حجزك من حسابك. يرجى ملاحظة أنه يجب إجراء الإلغاء قبل 24 ساعة على الأقل من بداية الجولة لكي تكون مؤهلاً لاسترداد المبلغ.",
"faqQuestion6": "هل هناك أي رسوم مخفية؟",
"faqAnswer6": "لا، جميع الرسوم شفافة وتُعرض مسبقًا قبل إتمام الحجز. نحن نضمن أنه لا توجد أي رسوم مخفية.",
"faqQuestion7": "هل تقدمون خصومات للمجموعات؟",
"faqAnswer7": "نعم، نقدم خصومات للمجموعات التي تحجز أعدادًا كبيرة. يرجى الاتصال بفريق الدعم الخاص بنا للحصول على مزيد من المعلومات حول أسعار المجموعات.",
"faqQuestion8": "هل يشمل الحجز تأمين السفر؟",
"faqAnswer8": "نعم، جميع باقات الجولات لدينا تشمل تأمين السفر الأساسي. يمكنك اختيار ترقية باقة التأمين الخاصة بك أثناء عملية الدفع.",
"faqQuestion9": "ماذا يحدث إذا تم إلغاء الجولة من قبل الموفر؟",
"faqAnswer9": "إذا تم إلغاء الجولة من قبل الموفر بسبب ظروف غير متوقعة، ستتلقى استردادًا كاملًا أو خيار إعادة جدولة الجولة.",
"faqQuestion10": "كيف يمكنني الاتصال بدعم العملاء؟",
"faqAnswer10": "يمكنك الاتصال بدعم العملاء عن طريق النقر على زر 'اتصل بنا' في أسفل الصفحة أو بزيارة قسم الدعم."
}

332
public/locales/ar/common.json

@ -1,116 +1,222 @@
{
"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)",
"home": "الرئيسية",
"allTours": "جميع الجولات",
"blogs": "المدونات",
"faq": "الأسئلة الشائعة",
"aboutUs": "من نحن",
"customTour": "جولة مخصصة",
"searchPlaceholder": "إلى أين؟",
"searchDescription": "أي مكان • أي أسبوع • إضافة ضيوف",
"beginAdventure": "ابدأ رحلتك",
"beginAdventure1": "الروحية",
"beginAdventure2": "المغامرة",
"planPilgrimage": "خطط لرحلة الحج بسهولة. اعثر على أفضل أماكن الإقامة، وسائل النقل، والتجارب الموجهة إلى الأضرحة الشيعية في جميع أنحاء العالم.",
"startJourney": "ابدأ رحلتك",
"listOfTours": "قائمة الجولات",
"exploreTours": "استكشف الجولات والإقامات التي تم تصميمها لرحلة روحية لا تُنسى",
"tourPeriod": "مدة الجولة",
"tourPeriodDescription": "البداية - النهاية",
"guests": "الضيوف",
"addGuests": "إضافة ضيوف",
"available": "متاح",
"soldOut": "نفذ من المخزون",
"showMore": "عرض المزيد",
"happeningCities": "المدن النشطة",
"costEffectiveAdvertising": "إعلانات فعالة من حيث التكلفة",
"costEffectiveDescription": "من خلال إدراج مجاني، يمكنك الإعلان عن إيجارك دون تكاليف مسبقة",
"reachMillions": "وصل إلى الملايين مع Chisfis",
"reachMillionsDescription": "ملايين الأشخاص يبحثون عن أماكن فريدة للإقامة حول العالم",
"secureAndSimple": "آمن وبسيط",
"secureDescription": "يوفر إدراج Holiday Lettings طريقة آمنة وسهلة لاستلام الحجوزات والمدفوعات عبر الإنترنت",
"mobileApps": "تطبيقات الهاتف المحمول",
"mobileAppsDescription": "لوريم إيبسوم دولار سيت أميت، كونسيكتيتور أديبيسكينغ أليت. سيد دابيبوس بورتتيتور نيسل، سيت أميت فينيبوس ليبرو.",
"installation": "التثبيت",
"releaseNotes": "ملاحظات الإصدار",
"upgradeGuide": "دليل الترقية",
"browserSupport": "دعم المتصفح",
"editorSupport": "دعم المحرر",
"designFeatures": "ميزات التصميم",
"prototyping": "النمذجة الأولية",
"designSystems": "أنظمة التصميم",
"pricing": "التسعير",
"security": "الأمان",
"bestPractices": "أفضل الممارسات",
"support": "الدعم",
"developers": "المطورون",
"learnDesign": "تعلم التصميم",
"releases": "الإصدارات",
"discussionForums": "منتديات المناقشة",
"codeOfConduct": "مدونة السلوك",
"communityResources": "موارد المجتمع",
"contributing": "المساهمة",
"concurrentMode": "وضع التوازي",
"goodNews": "أخبار جيدة من بعيد",
"whatPeopleThink": "لنرَ ماذا يفكر الناس في Chisfis",
"testimonial": "هذا المكان هو تمامًا كما في الصورة المنشورة على Chisfis. خدمة رائعة، قضينا إقامة رائعة!",
"clientName": "تيانا أبي",
"clientLocation": "ماليزيا",
"myTrips": "رحلاتي",
"account": "الحساب",
"menu": "القائمة",
"gettingStarted": "البدء",
"explore": "استكشاف",
"resources": "الموارد",
"community": "المجتمع",
"placeType": "نوع المكان",
"noTours": "لا توجد جولات متاحة",
"itinerary": "خطة الرحلة",
"itineraryTitle": "خطة الرحلة",
"total": "الإجمالي",
"reserve": "احجز",
"tourFeatures": "ميزات الجولة",
"tourFeaturesTitle": "ميزات الجولة",
"startRating": "تقييم البدء",
"listingDetails": "التفاصيل",
"imageAlt": "صورة للجولة",
"loading": "جاري التحميل...",
"adults": "البالغين",
"adultsDesc": "الأعمار 13 فما فوق",
"children": "الأطفال",
"childrenDesc": "الأعمار من 2 إلى 12",
"infants": "الرضع",
"infantsDesc": "الأعمار من 0 إلى 2",
"traveler": "المسافر",
"responses": "الردود ({{count}})",
"submit": "إرسال",
"cancel": "إلغاء",
"relatedPosts": "المنشورات ذات الصلة",
"aboutUsHeading": "👋 عنّا",
"aboutUsSubheading": "نحن فريق متحمس مكرس لصناعة تجارب سفر لا تُنسى للمستكشفين والحالمين على حد سواء. من الهروب الهادئ إلى الشواطئ الاستوائية إلى المغامرات المليئة بالأدرينالين في الأماكن الغريبة، نصمم رحلات فريدة كما أنت. انضم إلينا ولنكشف العالم، مغامرة واحدة في كل مرة!",
"statisticTitle": "🚀 حقائق سريعة",
"statisticDescription": "نحن محايدون ومستقلون، وكل يوم نصنع برامج ومحتوى متميز على مستوى عالمي.",
"statisticHeading1": "10 مليون",
"statisticSubHeading1": "تم نشر المقالات حول العالم (حتى 30 سبتمبر 2021)",
"statisticHeading2": "100,000",
"statisticSubHeading2": "حسابات المستخدمين المسجلة (حتى 30 سبتمبر 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"
"statisticSubHeading3": "الدول والمناطق التي تتواجد فيها خدماتنا (حتى 30 سبتمبر 2021)",
"customTrip": "رحلة مخصصة",
"guide": "الدليل",
"guideDescription": "أولاً، اكتب مصدر مغادرتك، ثم اختر أول وجهة في رحلتك، عدد ليالي الإقامة، ووسائل السفر، ثم اختر وجهاتك إذا رغبت.",
"beginYourTrip": "ابدأ رحلتك",
"startDate": "تاريخ البدء",
"numberOfPassengers": "عدد الركاب",
"destination": "الوجهة",
"selectCity": "اختر المدينة",
"transportation": "النقل",
"selectTransport": "اختر وسيلة النقل",
"hotel": "الفندق",
"selectHotel": "اختر الفندق",
"duration": "المدة",
"finishDate": "تاريخ الانتهاء",
"addDestination": "أضف وجهة",
"continue": "استمرار",
"successMessage": "تم التسجيل بنجاح",
"to": "إلى",
"login": "تسجيل الدخول",
"signup": "إنشاء حساب",
"All": "الكل",
"create": "إنشاء",
"createPersonalizedTourLine1": "أنشئ جولتك المخصصة وصمم",
"createPersonalizedTourLine2": "تجربة السفر المثالية المخصصة لتفضيلاتك.",
"imageAltCustomTourBackground": "خلفية الجولة المخصصة",
"howItWorks": {
"title": "كيف يعمل",
"desc": "ابق هادئًا وسافر بثقة",
"vectorAlt": "صورة توضيحية توضح كيفية العمل",
"bookAndRelax": {
"title": "احجز واسترخي",
"desc": "دَعْ كل رحلة تكون تجربة ملهمة، وكل غرفة مساحة هادئة"
},
"smartChecklist": {
"title": "قائمة التحقق الذكية",
"desc": "دَعْ كل رحلة تكون تجربة ملهمة، وكل غرفة مساحة هادئة"
},
"saveMore": {
"title": "وفر أكثر",
"desc": "دَعْ كل رحلة تكون تجربة ملهمة، وكل غرفة مساحة هادئة"
},
"item1": {
"imageAlt": "توضيح ميزة احجز واسترخي",
"imageAltDark": "توضيح ميزة احجز واسترخي في الوضع المظلم"
},
"item2": {
"imageAlt": "توضيح ميزة قائمة التحقق الذكية",
"imageAltDark": "توضيح ميزة قائمة التحقق الذكية في الوضع المظلم"
},
"item3": {
"imageAlt": "توضيح ميزة وفر أكثر",
"imageAltDark": "توضيح ميزة وفر أكثر في الوضع المظلم"
}
},
"imageAltMapBackground": "خلفية الخريطة",
"imageAltAppRightImg": "صورة التطبيق اليمنى",
"buttonDownloadOnAppStore": "تنزيل من متجر التطبيقات",
"buttonGetItOnGooglePlay": "احصل عليها من Google Play",
"tourSuggestion": {
"heading": "الدول",
"subHeading": "أماكن شهيرة نوصي بها لك",
"vectorAlt": "صورة توضيحية توضح اقتراحات الجولات",
"categories": {
"Nature House": "منزل طبيعي",
"Wooden house": "منزل خشبي",
"Houseboat": "قارب منزل",
"Farm House": "منزل مزرعة",
"Dome House": "منزل قبة",
"Wooden Dome": "قبة خشبية"
}
},
"tourSuggestion.heading": "الدول",
"tourSuggestion.subHeading": "أماكن شهيرة نوصي بها لك",
"cardCategory3.imageAltPlaces": "أماكن",
"cardCategory3.tours": "الجولات",
"sectionClientSay": {
"heading": "ماذا يقول عملاؤنا عنا؟",
"imageAltMain": "صورة رئيسية لآراء العملاء",
"imageAltClient1": "العميل 1",
"imageAltClient2": "العميل 2",
"imageAltClient3": "العميل 3",
"imageAltClient4": "العميل 4",
"imageAltClient5": "العميل 5",
"imageAltClient6": "العميل 6",
"imageAltQuotation1": "علامة اقتباس يسار",
"imageAltQuotation2": "علامة اقتباس يمين",
"testimonials": {
"0": {
"content": "كانت الرحلة مع عقيلة تجربة روحية غنية. المرشدين المطلعين والطاقم الدافئ جعلوا كل لحظة لا تُنسى.",
"clientName": "تيانا أبي",
"clientAddress": "ماليزيا"
},
"1": {
"content": "تم ترتيب كل شيء بشكل مثالي، من الإقامة إلى الزيارات إلى الأماكن المقدسة. شعرت بالاهتمام طوال الجولة.",
"clientName": "ليني سويفان",
"clientAddress": "لندن"
},
"2": {
"content": "الانضمام إلى هذه الجولة سمح لي بالتواصل بشكل عميق مع المسافرين الآخرين وتراثي. أوصي بشدة بـ عقيلة لرحلة ذات مغزى!",
"clientName": "بيرتا إميلي",
"clientAddress": "طوكيو"
}
}
},
"sectionGridFilterCard": {
"allTours": "جميع الجولات",
"noToursAvailable": "لا توجد جولات متاحة",
"toursInfo": "{{count}} إقامة · {{dateRange}} · {{guests}} ضيوف"
},
"accountHeading": "معلومات الحساب",
"nameLabel": "الاسم",
"emailLabel": "البريد الإلكتروني",
"phoneLabel": "رقم الهاتف",
"changeImage": "تغيير الصورة",
"updateButton": "تحديث المعلومات",
"signOutButton": "تسجيل الخروج",
"deleteButton": "حذف الحساب",
"deleteSuccess": "تم حذف حسابك بنجاح.",
"signOutSuccess": "تم تسجيل الخروج بنجاح.",
"updateSuccess": "تم تحديث معلوماتك بنجاح.",
"noChanges": "لم يتم اكتشاف أي تغييرات.",
"errorGeneric": "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
"errorUnknown": "حدث خطأ غير معروف.",
"benefits": "الفوائد"
}

23
public/locales/ar/footer.json

@ -0,0 +1,23 @@
{
"widgetMenus": {
"Quick Links": {
"title": "روابط سريعة",
"menus": [
{ "label": "جميع الجولات" },
{ "label": "المدونات" },
{ "label": "الأسئلة الشائعة" },
{ "label": "من نحن" }
]
}
},
"aboutUs": {
"title": "من نحن",
"description": "نحن فريق متحمس مكرس لصناعة تجارب سفر لا تُنسى للمستكشفين والحالمين على حد سواء. من الهروب الهادئ إلى الشواطئ الاستوائية إلى المغامرات المليئة بالأدرينالين في الأماكن الغريبة، نصمم رحلات فريدة كما أنت. انضم إلينا ولنكشف العالم، مغامرة واحدة في كل مرة!"
},
"footerNav": {
"description": "نحن فريق متحمس مكرس لصناعة تجارب سفر لا تُنسى للمستكشفين والحالمين على حد سواء."
},
"socials": {
"title": "تابعنا"
}
}

91
public/locales/ar/form.json

@ -0,0 +1,91 @@
{
"selectTour": "اختر جولتك",
"tourPeriod": "مدة الجولة",
"startEndDate": "البداية - النهاية",
"guests": "الضيوف",
"addGuests": "إضافة ضيوف",
"adults": "البالغين",
"adultsDesc": "الأعمار 13 فما فوق",
"children": "الأطفال",
"childrenDesc": "الأعمار من 2 إلى 12",
"infants": "الرضع",
"infantsDesc": "الأعمار من 0 إلى 2",
"clear": "مسح",
"submit": "إرسال",
"login": "تسجيل الدخول",
"phoneNumber": "رقم الهاتف",
"enterPhoneNumber": "أدخل رقم هاتفك",
"password": "كلمة المرور",
"forgotPassword": "نسيت كلمة المرور؟",
"enterPassword": "أدخل كلمة المرور",
"continue": "استمرار",
"createAccount": "إنشاء حساب",
"invalidPhoneNumber": "رقم الهاتف غير صالح.",
"invalidPhoneNumberFormat": "تنسيق رقم الهاتف غير صالح.",
"passwordRequired": "كلمة المرور مطلوبة.",
"loginSuccessful": "تم تسجيل الدخول بنجاح!",
"loginFailed": "فشل تسجيل الدخول، يرجى التحقق من بياناتك.",
"unknownError": "حدث خطأ غير معروف.",
"hidePassword": "إخفاء كلمة المرور",
"showPassword": "عرض كلمة المرور",
"signup": "إنشاء حساب",
"changePassword": "تغيير كلمة المرور",
"fullName": "الاسم الكامل",
"enterFullName": "أدخل اسمك الكامل",
"confirmPassword": "تأكيد كلمة المرور",
"enterConfirmPassword": "أعد إدخال كلمة المرور",
"alreadyHaveAccount": "هل لديك حساب بالفعل؟",
"signIn": "تسجيل الدخول",
"errorOccurred": "حدث خطأ.",
"hideConfirmPassword": "إخفاء تأكيد كلمة المرور",
"showConfirmPassword": "عرض تأكيد كلمة المرور",
"verificationMethod": "طريقة التحقق",
"sendViaWhatsApp": "إرسال عبر واتساب",
"sendViaSMS": "إرسال عبر الرسائل النصية",
"verificationCode": "رمز التحقق",
"enterOtpDescription": "أدخل الرمز المكون من 5 أرقام الذي أرسلناه لإكمال تسجيل حسابك",
"haventGotCode": "لم تحصل على رمز التأكيد بعد؟",
"resend": "إعادة إرسال",
"seconds": "ثواني",
"confirm": "تأكيد",
"signInSuccessful": "تم تسجيل دخولك بنجاح",
"somethingWentWrong": "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
"otpInput": "إدخال OTP {{index}}",
"otpNotComplete": "رمز التحقق غير صالح",
"addNewPassenger": "إضافة راكب جديد",
"passengerInfo": "معلومات الراكب",
"passportNo": "رقم الجواز",
"fullNameRequired": "الاسم الكامل مطلوب.",
"passportRequired": "رقم الجواز مطلوب.",
"passportNumeric": "يجب أن يكون رقم الجواز رقميًا.",
"dobRequired": "تاريخ الميلاد مطلوب.",
"phoneRequired": "رقم الهاتف مطلوب.",
"phoneNumeric": "يجب أن يكون رقم الهاتف رقميًا.",
"passportImageRequired": "صورة الجواز مطلوبة.",
"editPassengerInfo": "تعديل معلومات الراكب",
"backToBills": "العودة إلى الفواتير",
"noBillsAvailable": "لا توجد فواتير متاحة",
"awaitingPayment": "بانتظار الدفع",
"approved": "معتمد",
"rejected": "مرفوض",
"pending": "قيد الانتظار",
"issuedDate": "تاريخ الإصدار",
"expirationDate": "تاريخ الانتهاء",
"tourInvoiceAmount": "قيمة فاتورة الجولة",
"viewBill": "عرض الفاتورة",
"transactionReceiptUpdated": "تم تحديث إيصال العملية بنجاح",
"updateFailed": "فشل في تحديث الإيصال",
"billDetails": "تفاصيل الفاتورة",
"whyRejected": "لماذا تم رفضها؟",
"numberOfPassengers": "عدد الركاب",
"adult": "بالغ",
"infant": "رضيع",
"tourPrice": "سعر الجولة",
"accountNumber": "رقم الحساب",
"uploadPassportImage": "تحميل صورة الجواز",
"noFileSelected": "لم يتم اختيار ملف",
"uploadedImage": "الصورة المرفوعة",
"delete": "حذف",
"currentReceipt": "الإيصال الحالي",
"copy": "نسخ"
}

15
public/locales/ar/navigation.json

@ -0,0 +1,15 @@
{
"text-home": "الرئيسية",
"text-all-tours": "جميع الجولات",
"text-blog": "المدونة",
"text-faq": "الأسئلة الشائعة",
"text-about": "عن الموقع",
"text-language": "اللغة",
"text-no-match": "لا توجد نتائج",
"search-placeholder": "اكتب وابحث...",
"customTrip": "رحلة مخصصة",
"navAccount": "الحساب",
"navMyTrips": "رحلاتي",
"navPassengersList": "قائمة الركاب",
"navBills": "الفواتير"
}

47
public/locales/en/FAQ.json

@ -1,25 +1,24 @@
{
"faqTitle": "Frequently Asked Questions",
"faqSubtitle": "Have Questions? We are here to help you!",
"faqQuestion1": "How can I book a tour on your site?",
"faqAnswer1": "To book a tour, simply select your desired destination from the 'List of Tours', choose your dates, and follow the steps to complete the booking process.",
"faqQuestion2": "Can I customize my tour?",
"faqAnswer2": "Yes, our 'Customize Tour' feature allows you to tailor your journey based on your preferences. Click on 'Customize Tour' at the top of the page to start personalizing your experience.",
"faqQuestion3": "What payment methods do you accept?",
"faqAnswer3": "We accept all major credit cards, PayPal, and bank transfers. You can choose your preferred method during the checkout process.",
"faqQuestion4": "How do I know my booking is confirmed?",
"faqAnswer4": "Once you complete the payment process, you will receive a confirmation email with all the details of your booking. You can also view your booking details in your account dashboard.",
"faqQuestion5": "Can I cancel or modify my booking?",
"faqAnswer5": "Yes, you can cancel or modify your booking from your account. Please note that cancellations must be made at least 24 hours before the tour starts to be eligible for a refund.",
"faqQuestion6": "Are there any hidden fees?",
"faqAnswer6": "No, all fees are transparent and shown upfront before you complete your booking. We ensure there are no hidden fees.",
"faqQuestion7": "Do you offer group discounts?",
"faqAnswer7": "Yes, we offer group discounts for larger bookings. Please contact our support team for more information on group rates.",
"faqQuestion8": "Is travel insurance included in the booking?",
"faqAnswer8": "Yes, all our tour packages include basic travel insurance. You can choose to upgrade your insurance package during the checkout process.",
"faqQuestion9": "What happens if the tour is canceled by the provider?",
"faqAnswer9": "If the tour is canceled by the provider due to unforeseen circumstances, you will receive a full refund or the option to reschedule your tour.",
"faqQuestion10": "How can I contact customer support?",
"faqAnswer10": "You can contact customer support by clicking the 'Contact Us' button at the bottom of the page or by visiting our support section."
}
"faqTitle": "Frequently Asked Questions",
"faqSubtitle": "Have questions? We are here to help!",
"faqQuestion1": "How can I book a tour on your site?",
"faqAnswer1": "To book a tour, simply select your desired destination from the 'List of Tours', choose your dates, and follow the steps to complete the booking process.",
"faqQuestion2": "Can I customize my tour?",
"faqAnswer2": "Yes, our 'Customize Tour' feature allows you to tailor your journey based on your preferences. Click on 'Customize Tour' at the top of the page to start personalizing your experience.",
"faqQuestion3": "What payment methods do you accept?",
"faqAnswer3": "We accept all major credit cards, PayPal, and bank transfers. You can choose your preferred method during the checkout process.",
"faqQuestion4": "How do I know my booking is confirmed?",
"faqAnswer4": "Once you complete the payment process, you will receive a confirmation email with all the details of your booking. You can also view your booking details in your account dashboard.",
"faqQuestion5": "Can I cancel or modify my booking?",
"faqAnswer5": "Yes, you can cancel or modify your booking from your account. Please note that cancellations must be made at least 24 hours before the tour starts to be eligible for a refund.",
"faqQuestion6": "Are there any hidden fees?",
"faqAnswer6": "No, all fees are transparent and shown upfront before you complete your booking. We ensure there are no hidden fees.",
"faqQuestion7": "Do you offer group discounts?",
"faqAnswer7": "Yes, we offer group discounts for larger bookings. Please contact our support team for more information on group rates.",
"faqQuestion8": "Is travel insurance included in the booking?",
"faqAnswer8": "Yes, all our tour packages include basic travel insurance. You can choose to upgrade your insurance package during the checkout process.",
"faqQuestion9": "What happens if the tour is canceled by the provider?",
"faqAnswer9": "If the tour is canceled by the provider due to unforeseen circumstances, you will receive a full refund or the option to reschedule your tour.",
"faqQuestion10": "How can I contact customer support?",
"faqAnswer10": "You can contact customer support by clicking the 'Contact Us' button at the bottom of the page or by visiting our support section."
}

155
public/locales/en/common.json

@ -10,23 +10,23 @@
"beginAdventure": "Begin your",
"beginAdventure1": "spiritual",
"beginAdventure2": "adventure",
"planPilgrimage": "Plan your pilgrimage with ease. Find the best accommodations, transportation, and guided experiences to Shia shrines around the world",
"planPilgrimage": "Plan your pilgrimage with ease. Find the best accommodations, transportation, and guided experiences to Shia shrines around the world.",
"startJourney": "Start your journey",
"listOfTours": "List of Tours",
"exploreTours": "Explore tours and accommodations tailored for a spiritual and memorable journey",
"tourPeriod": "Tour period",
"tourPeriod": "Tour Period",
"tourPeriodDescription": "Start - End",
"guests": "Guests",
"addGuests": "Add guests",
"available": "Available",
"soldOut": "Sold Out",
"soldOut": "Sold out",
"showMore": "Show me more",
"happeningCities": "Happening cities",
"costEffectiveAdvertising": "Cost-effective advertising",
"happeningCities": "Happening Cities",
"costEffectiveAdvertising": "Cost-effective Advertising",
"costEffectiveDescription": "With a free listing, you can advertise your rental with no upfront costs",
"reachMillions": "Reach millions with Chisfis",
"reachMillionsDescription": "Millions of people are searching for unique places to stay around the world",
"secureAndSimple": "Secure and simple",
"secureAndSimple": "Secure and Simple",
"secureDescription": "A Holiday Lettings listing gives you a secure and easy way to take bookings and payments online",
"mobileApps": "Mobile Apps",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
@ -35,22 +35,22 @@
"upgradeGuide": "Upgrade Guide",
"browserSupport": "Browser Support",
"editorSupport": "Editor Support",
"designFeatures": "Design features",
"designFeatures": "Design Features",
"prototyping": "Prototyping",
"designSystems": "Design systems",
"designSystems": "Design Systems",
"pricing": "Pricing",
"security": "Security",
"bestPractices": "Best practices",
"bestPractices": "Best Practices",
"support": "Support",
"developers": "Developers",
"learnDesign": "Learn design",
"learnDesign": "Learn Design",
"releases": "Releases",
"discussionForums": "Discussion Forums",
"codeOfConduct": "Code of Conduct",
"communityResources": "Community Resources",
"contributing": "Contributing",
"concurrentMode": "Concurrent Mode",
"goodNews": "Good news from far away",
"goodNews": "Good News from Far Away",
"whatPeopleThink": "Let's see what people think of Chisfis",
"testimonial": "This place is exactly like the picture posted on Chisfis. Great service, we had a great stay!",
"clientName": "Tiana Abie",
@ -58,11 +58,11 @@
"myTrips": "My Trips",
"account": "Account",
"menu": "Menu",
"gettingStarted": "Getting started",
"gettingStarted": "Getting Started",
"explore": "Explore",
"resources": "Resources",
"community": "Community",
"placeType": "Type of place",
"placeType": "Type of Place",
"noTours": "No tours available",
"itinerary": "Itinerary",
"itineraryTitle": "Itinerary",
@ -85,34 +85,139 @@
"submit": "Submit",
"cancel": "Cancel",
"relatedPosts": "Related Posts",
"aboutUsHeading": "👋 About Us.",
"aboutUsHeading": "👋 About Us",
"aboutUsSubheading": "We are a passionate team dedicated to crafting unforgettable travel experiences for explorers and dreamers alike. From serene escapes on tropical beaches to adrenaline-fueled adventures in exotic locales, we curate journeys that are as unique as you are. Join us and let’s explore the world, one adventure at a time!",
"statisticTitle": "🚀 Fast Facts",
"statisticDescription": "We’re impartial and independent, and every day we create distinctive, world-class programmes and content.",
"statisticHeading1": "10 million",
"statisticSubHeading1": "Articles have been public around the world (as of Sept. 30, 2021)",
"statisticSubHeading1": "Articles have been published around the world (as of Sept. 30, 2021)",
"statisticHeading2": "100,000",
"statisticSubHeading2": "Registered users account (as of Sept. 30, 2021)",
"statisticSubHeading2": "Registered user accounts (as of Sept. 30, 2021)",
"statisticHeading3": "220+",
"statisticSubHeading3": "Countries and regions have our presence (as of Sept. 30, 2021)",
"customTrip": "Custom Trip",
"guide": "Guide",
"guideDescription": "First, write the origin of your departure, then choose the first destination of your trip, the number of nights of stay, and the means of travel, then choose your travel destinations if you wish.",
"beginYourTrip": "Begin your trip",
"startDate": "Start Date",
"numberOfPassengers": "Number Of Passengers",
"startDate": "Start date",
"numberOfPassengers": "Number of passengers",
"destination": "Destination",
"selectCity": "Select City",
"selectCity": "Select city",
"transportation": "Transportation",
"selectTransport": "Select Transport",
"selectTransport": "Select transport",
"hotel": "Hotel",
"selectHotel": "Select Hotel",
"selectHotel": "Select hotel",
"duration": "Duration",
"finishDate": "Finish date",
"addDestination": "Add Destination",
"addDestination": "Add destination",
"continue": "Continue",
"successMessage": "Successfully Registered",
"successMessage": "Successfully registered",
"to": "to",
"login": "LogIn",
"signup": "Sign Up"
"login": "Login",
"signup": "Sign up",
"All": "All",
"create": "Create",
"createPersonalizedTourLine1": "Create your personalized tour and design the",
"createPersonalizedTourLine2": "perfect travel experience tailored to your preferences.",
"imageAltCustomTourBackground": "Custom tour background",
"howItWorks": {
"title": "How it works",
"desc": "Keep calm & travel on",
"vectorAlt": "Decorative vector image illustrating how it works",
"bookAndRelax": {
"title": "Book & Relax",
"desc": "Let each trip be an inspirational journey, each room a peaceful space"
},
"smartChecklist": {
"title": "Smart Checklist",
"desc": "Let each trip be an inspirational journey, each room a peaceful space"
},
"saveMore": {
"title": "Save More",
"desc": "Let each trip be an inspirational journey, each room a peaceful space"
},
"item1": {
"imageAlt": "Book & Relax feature illustration",
"imageAltDark": "Book & Relax feature illustration in dark mode"
},
"item2": {
"imageAlt": "Smart Checklist feature illustration",
"imageAltDark": "Smart Checklist feature illustration in dark mode"
},
"item3": {
"imageAlt": "Save More feature illustration",
"imageAltDark": "Save More feature illustration in dark mode"
}
},
"imageAltMapBackground": "Map background",
"imageAltAppRightImg": "App right image",
"buttonDownloadOnAppStore": "Download on the App Store",
"buttonGetItOnGooglePlay": "Get it on Google Play",
"tourSuggestion": {
"heading": "Countries",
"subHeading": "Popular places to recommend for you",
"vectorAlt": "Decorative vector image illustrating tour suggestions",
"categories": {
"Nature House": "Nature House",
"Wooden house": "Wooden House",
"Houseboat": "Houseboat",
"Farm House": "Farm House",
"Dome House": "Dome House",
"Wooden Dome": "Wooden Dome"
}
},
"tourSuggestion.heading": "Countries",
"tourSuggestion.subHeading": "Popular places to recommend for you",
"cardCategory3.imageAltPlaces": "Places",
"cardCategory3.tours": "Tours",
"sectionClientSay": {
"heading": "What do our customers say about us?",
"imageAltMain": "Client Say Main Image",
"imageAltClient1": "Client 1",
"imageAltClient2": "Client 2",
"imageAltClient3": "Client 3",
"imageAltClient4": "Client 4",
"imageAltClient5": "Client 5",
"imageAltClient6": "Client 6",
"imageAltQuotation1": "Quotation Mark Left",
"imageAltQuotation2": "Quotation Mark Right",
"testimonials": {
"0": {
"content": "Traveling with Aqila was a spiritually enriching experience. The knowledgeable guides and warm staff made every moment memorable.",
"clientName": "Tiana Abie",
"clientAddress": "Malaysia"
},
"1": {
"content": "Everything was perfectly arranged, from accommodations to visits to sacred sites. I felt well taken care of throughout the entire tour.",
"clientName": "Lennie Swiffan",
"clientAddress": "London"
},
"2": {
"content": "Joining this tour allowed me to connect deeply with fellow travelers and my heritage. Highly recommend Aqila for a meaningful trip!",
"clientName": "Berta Emili",
"clientAddress": "Tokyo"
}
}
},
"sectionGridFilterCard": {
"allTours": "All Tours",
"noToursAvailable": "No tours available",
"toursInfo": "{{count}} stays · {{dateRange}} · {{guests}} Guests"
},
"accountHeading": "Account Information",
"nameLabel": "Name",
"emailLabel": "Email",
"phoneLabel": "Phone",
"changeImage": "Change Image",
"updateButton": "Update Info",
"signOutButton": "Sign Out",
"deleteButton": "Delete Account",
"deleteSuccess": "Your account has been deleted successfully.",
"signOutSuccess": "You have signed out successfully.",
"updateSuccess": "Your information has been updated successfully.",
"noChanges": "No changes detected.",
"errorGeneric": "Something went wrong. Please try again.",
"errorUnknown": "An unknown error occurred.",
"benefits" : "BENEFITS"
}

23
public/locales/en/footer.json

@ -0,0 +1,23 @@
{
"widgetMenus": {
"Quick Links": {
"title": "Quick Links",
"menus": [
{ "label": "All Tours" },
{ "label": "Blogs" },
{ "label": "FAQ" },
{ "label": "About Us" }
]
}
},
"aboutUs": {
"title": "About Us",
"description": "We are a passionate team dedicated to crafting unforgettable travel experiences for explorers and dreamers alike. From serene escapes on tropical beaches to adrenaline-fueled adventures in exotic locales, we curate journeys that are as unique as you are. Join us and let’s explore the world, one adventure at a time!"
},
"footerNav": {
"description": "We are a passionate team dedicated to crafting unforgettable travel experiences for explorers and dreamers alike."
},
"socials": {
"title": "Follow us"
}
}

91
public/locales/en/form.json

@ -0,0 +1,91 @@
{
"selectTour": "Select your tour",
"tourPeriod": "Tour period",
"startEndDate": "Start - End",
"guests": "Guests",
"addGuests": "Add Guests",
"adults": "Adults",
"adultsDesc": "Ages 13 or above",
"children": "Children",
"childrenDesc": "Ages 2–12",
"infants": "Infants",
"infantsDesc": "Ages 0–2",
"clear": "Clear",
"submit": "Submit",
"login": "Login",
"phoneNumber": "Phone Number",
"enterPhoneNumber": "Enter your phone number",
"password": "Password",
"forgotPassword": "Forgot password?",
"enterPassword": "Enter your password",
"continue": "Continue",
"createAccount": "Create an account",
"invalidPhoneNumber": "Invalid phone number.",
"invalidPhoneNumberFormat": "Invalid phone number format.",
"passwordRequired": "Password is required.",
"loginSuccessful": "Login successful!",
"loginFailed": "Login failed, please check your credentials.",
"unknownError": "An unknown error occurred.",
"hidePassword": "Hide password",
"showPassword": "Show password",
"signup": "Signup",
"changePassword": "Change password",
"fullName": "Full Name",
"enterFullName": "Enter your full name",
"confirmPassword": "Confirm Password",
"enterConfirmPassword": "Re-enter your password",
"alreadyHaveAccount": "Already have an account?",
"signIn": "Sign in",
"errorOccurred": "An error occurred.",
"hideConfirmPassword": "Hide confirm password",
"showConfirmPassword": "Show confirm password",
"verificationMethod": "Verification method",
"sendViaWhatsApp": "Send via WhatsApp",
"sendViaSMS": "Send via SMS",
"verificationCode": "Verification Code",
"enterOtpDescription": "Enter the 5-digit code that we sent to complete your account registration",
"haventGotCode": "Haven't got the confirmation code yet?",
"resend": "Resend",
"seconds": "Seconds",
"confirm": "Confirm",
"signInSuccessful": "Your sign in was successful",
"somethingWentWrong": "Something went wrong. Please try again.",
"otpInput": "OTP input {{index}}",
"otpNotComplete": "Verification code is not valid",
"addNewPassenger": "Add new passenger",
"passengerInfo": "Passenger information",
"passportNo": "Passport No",
"fullNameRequired": "Full Name is required.",
"passportRequired": "Passport Number is required.",
"passportNumeric": "Passport Number must be numeric.",
"dobRequired": "Date of Birth is required.",
"phoneRequired": "Phone Number is required.",
"phoneNumeric": "Phone Number must be numeric.",
"passportImageRequired": "Passport image is required.",
"editPassengerInfo": "Edit Passenger Information",
"backToBills": "Back to Bills",
"noBillsAvailable": "No bills available",
"awaitingPayment": "Awaiting Payment",
"approved": "Approved",
"rejected": "Rejected",
"pending": "Pending",
"issuedDate": "Issued Date",
"expirationDate": "Expiration Date",
"tourInvoiceAmount": "Tour Invoice Amount",
"viewBill": "View Bill",
"transactionReceiptUpdated": "Transaction receipt updated successfully",
"updateFailed": "Failed to update receipt",
"billDetails": "Bill Details",
"whyRejected": "Why was it rejected?",
"numberOfPassengers": "Number of Passengers",
"adult": "Adult",
"infant": "Infant",
"tourPrice": "Tour Price",
"accountNumber": "Account Number",
"uploadPassportImage": "Upload Passport Image",
"noFileSelected": "No File Selected",
"uploadedImage": "Uploaded Image",
"delete": "Delete",
"currentReceipt": "Current Receipt",
"copy": "Copy"
}

15
public/locales/en/navigation.json

@ -0,0 +1,15 @@
{
"text-home": "Home",
"text-all-tours": "All Tours",
"text-blog": "Blog",
"text-faq": "FAQ",
"text-about": "About",
"text-language": "Language",
"text-no-match": "No Matches Found",
"search-placeholder": "Type and search...",
"customTrip": "Custom Trip",
"navAccount": "Account",
"navMyTrips": "My Trips",
"navPassengersList": "Passengers List",
"navBills": "Bills"
}

116
public/locales/fr/common.json

@ -1,116 +0,0 @@
{
"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"
}

24
public/locales/id/FAQ.json

@ -0,0 +1,24 @@
{
"faqTitle": "Pertanyaan yang Sering Diajukan",
"faqSubtitle": "Punya pertanyaan? Kami di sini untuk membantu!",
"faqQuestion1": "Bagaimana cara saya memesan tur di situs Anda?",
"faqAnswer1": "Untuk memesan tur, cukup pilih tujuan yang Anda inginkan dari 'Daftar Tur', pilih tanggal Anda, dan ikuti langkah-langkah untuk menyelesaikan proses pemesanan.",
"faqQuestion2": "Apakah saya bisa menyesuaikan tur saya?",
"faqAnswer2": "Ya, fitur 'Tur Kustom' kami memungkinkan Anda untuk menyesuaikan perjalanan berdasarkan preferensi Anda. Klik 'Tur Kustom' di bagian atas halaman untuk memulai personalisasi pengalaman Anda.",
"faqQuestion3": "Metode pembayaran apa yang Anda terima?",
"faqAnswer3": "Kami menerima semua kartu kredit utama, PayPal, dan transfer bank. Anda dapat memilih metode pembayaran yang diinginkan selama proses checkout.",
"faqQuestion4": "Bagaimana cara saya tahu jika pemesanan saya sudah dikonfirmasi?",
"faqAnswer4": "Setelah Anda menyelesaikan proses pembayaran, Anda akan menerima email konfirmasi dengan semua detail pemesanan Anda. Anda juga dapat melihat detail pemesanan Anda di dasbor akun Anda.",
"faqQuestion5": "Bisakah saya membatalkan atau mengubah pemesanan saya?",
"faqAnswer5": "Ya, Anda dapat membatalkan atau mengubah pemesanan Anda dari akun Anda. Harap dicatat bahwa pembatalan harus dilakukan setidaknya 24 jam sebelum tur dimulai agar memenuhi syarat untuk mendapatkan pengembalian dana.",
"faqQuestion6": "Apakah ada biaya tersembunyi?",
"faqAnswer6": "Tidak, semua biaya transparan dan ditampilkan sebelumnya sebelum Anda menyelesaikan pemesanan. Kami memastikan tidak ada biaya tersembunyi.",
"faqQuestion7": "Apakah Anda menawarkan diskon untuk grup?",
"faqAnswer7": "Ya, kami menawarkan diskon grup untuk pemesanan besar. Silakan hubungi tim dukungan kami untuk informasi lebih lanjut mengenai tarif grup.",
"faqQuestion8": "Apakah asuransi perjalanan termasuk dalam pemesanan?",
"faqAnswer8": "Ya, semua paket tur kami termasuk asuransi perjalanan dasar. Anda dapat memilih untuk meningkatkan paket asuransi Anda selama proses checkout.",
"faqQuestion9": "Apa yang terjadi jika tur dibatalkan oleh penyedia?",
"faqAnswer9": "Jika tur dibatalkan oleh penyedia karena keadaan yang tidak terduga, Anda akan menerima pengembalian dana penuh atau pilihan untuk menjadwal ulang tur Anda.",
"faqQuestion10": "Bagaimana cara saya menghubungi dukungan pelanggan?",
"faqAnswer10": "Anda dapat menghubungi dukungan pelanggan dengan mengklik tombol 'Hubungi Kami' di bagian bawah halaman atau dengan mengunjungi bagian dukungan kami."
}

222
public/locales/id/common.json

@ -0,0 +1,222 @@
{
"home": "Beranda",
"allTours": "Semua Tur",
"blogs": "Blog",
"faq": "FAQ",
"aboutUs": "Tentang Kami",
"customTour": "Tur Kustom",
"searchPlaceholder": "Ke mana?",
"searchDescription": "Ke mana saja • Minggu mana saja • Tambahkan tamu",
"beginAdventure": "Mulai perjalanan",
"beginAdventure1": "spiritual",
"beginAdventure2": "petualangan",
"planPilgrimage": "Rencanakan perjalanan ziarah Anda dengan mudah. Temukan akomodasi terbaik, transportasi, dan pengalaman terpanduan ke makam-makam Syiah di seluruh dunia.",
"startJourney": "Mulai perjalanan Anda",
"listOfTours": "Daftar Tur",
"exploreTours": "Jelajahi tur dan akomodasi yang disesuaikan untuk perjalanan spiritual yang tak terlupakan",
"tourPeriod": "Periode Tur",
"tourPeriodDescription": "Mulai - Selesai",
"guests": "Tamu",
"addGuests": "Tambahkan tamu",
"available": "Tersedia",
"soldOut": "Habis terjual",
"showMore": "Tampilkan lebih banyak",
"happeningCities": "Kota yang sedang berlangsung",
"costEffectiveAdvertising": "Iklan Hemat Biaya",
"costEffectiveDescription": "Dengan daftar gratis, Anda dapat mengiklankan penyewaan Anda tanpa biaya di muka",
"reachMillions": "Jangkau jutaan dengan Chisfis",
"reachMillionsDescription": "Jutaan orang mencari tempat unik untuk menginap di seluruh dunia",
"secureAndSimple": "Aman dan Sederhana",
"secureDescription": "Daftar Holiday Lettings memberi Anda cara aman dan mudah untuk menerima pemesanan dan pembayaran secara online",
"mobileApps": "Aplikasi Seluler",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
"installation": "Instalasi",
"releaseNotes": "Catatan Rilis",
"upgradeGuide": "Panduan Pembaruan",
"browserSupport": "Dukungan Browser",
"editorSupport": "Dukungan Editor",
"designFeatures": "Fitur Desain",
"prototyping": "Pembuatan Prototipe",
"designSystems": "Sistem Desain",
"pricing": "Harga",
"security": "Keamanan",
"bestPractices": "Praktik Terbaik",
"support": "Dukungan",
"developers": "Pengembang",
"learnDesign": "Belajar Desain",
"releases": "Rilis",
"discussionForums": "Forum Diskusi",
"codeOfConduct": "Kode Etik",
"communityResources": "Sumber Daya Komunitas",
"contributing": "Berkontribusi",
"concurrentMode": "Mode Bersamaan",
"goodNews": "Berita Baik dari Jauh",
"whatPeopleThink": "Mari kita lihat apa yang dipikirkan orang tentang Chisfis",
"testimonial": "Tempat ini persis seperti gambar yang diposting di Chisfis. Layanan yang luar biasa, kami memiliki waktu yang luar biasa!",
"clientName": "Tiana Abie",
"clientLocation": "Malaysia",
"myTrips": "Perjalanan Saya",
"account": "Akun",
"menu": "Menu",
"gettingStarted": "Memulai",
"explore": "Jelajahi",
"resources": "Sumber Daya",
"community": "Komunitas",
"placeType": "Jenis Tempat",
"noTours": "Tidak ada tur yang tersedia",
"itinerary": "Rencana Perjalanan",
"itineraryTitle": "Rencana Perjalanan",
"total": "Total",
"reserve": "Pesan",
"tourFeatures": "Fitur Tur",
"tourFeaturesTitle": "Fitur Tur",
"startRating": "Mulai Peringkat",
"listingDetails": "Detail",
"imageAlt": "Gambar tur",
"loading": "Memuat...",
"adults": "Dewasa",
"adultsDesc": "Usia 13 atau lebih",
"children": "Anak-anak",
"childrenDesc": "Usia 2–12",
"infants": "Bayi",
"infantsDesc": "Usia 0–2",
"traveler": "Pelancong",
"responses": "Tanggapan ({{count}})",
"submit": "Kirim",
"cancel": "Batal",
"relatedPosts": "Postingan Terkait",
"aboutUsHeading": "👋 Tentang Kami",
"aboutUsSubheading": "Kami adalah tim yang penuh semangat yang berdedikasi untuk menciptakan pengalaman perjalanan yang tak terlupakan bagi para penjelajah dan pemimpi. Dari pelarian damai di pantai tropis hingga petualangan yang memacu adrenalin di tempat-tempat eksotis, kami merancang perjalanan yang sama uniknya dengan Anda. Bergabunglah dengan kami dan mari jelajahi dunia, satu petualangan pada satu waktu!",
"statisticTitle": "🚀 Fakta Cepat",
"statisticDescription": "Kami tidak memihak dan independen, dan setiap hari kami menciptakan program dan konten yang unik dan berkualitas dunia.",
"statisticHeading1": "10 juta",
"statisticSubHeading1": "Artikel telah diterbitkan di seluruh dunia (hingga 30 September 2021)",
"statisticHeading2": "100.000",
"statisticSubHeading2": "Akun pengguna terdaftar (hingga 30 September 2021)",
"statisticHeading3": "220+",
"statisticSubHeading3": "Negara dan wilayah yang memiliki kehadiran kami (hingga 30 September 2021)",
"customTrip": "Tur Kustom",
"guide": "Pemandu",
"guideDescription": "Pertama, tulis asal keberangkatan Anda, kemudian pilih tujuan pertama perjalanan Anda, jumlah malam menginap, dan sarana transportasi, lalu pilih tujuan perjalanan Anda jika Anda mau.",
"beginYourTrip": "Mulai perjalanan Anda",
"startDate": "Tanggal mulai",
"numberOfPassengers": "Jumlah penumpang",
"destination": "Tujuan",
"selectCity": "Pilih kota",
"transportation": "Transportasi",
"selectTransport": "Pilih transportasi",
"hotel": "Hotel",
"selectHotel": "Pilih hotel",
"duration": "Durasi",
"finishDate": "Tanggal selesai",
"addDestination": "Tambahkan tujuan",
"continue": "Lanjutkan",
"successMessage": "Berhasil terdaftar",
"to": "ke",
"login": "Masuk",
"signup": "Daftar",
"All": "Semua",
"create": "Buat",
"createPersonalizedTourLine1": "Buat tur kustom Anda dan desain",
"createPersonalizedTourLine2": "pengalaman perjalanan yang sempurna sesuai dengan preferensi Anda.",
"imageAltCustomTourBackground": "Latar belakang tur kustom",
"howItWorks": {
"title": "Cara kerja",
"desc": "Tetap tenang & teruskan perjalanan",
"vectorAlt": "Gambar vektor dekoratif yang menggambarkan cara kerjanya",
"bookAndRelax": {
"title": "Pesan & Santai",
"desc": "Biarkan setiap perjalanan menjadi perjalanan inspiratif, setiap kamar menjadi ruang yang damai"
},
"smartChecklist": {
"title": "Daftar Periksa Pintar",
"desc": "Biarkan setiap perjalanan menjadi perjalanan inspiratif, setiap kamar menjadi ruang yang damai"
},
"saveMore": {
"title": "Hemat Lebih Banyak",
"desc": "Biarkan setiap perjalanan menjadi perjalanan inspiratif, setiap kamar menjadi ruang yang damai"
},
"item1": {
"imageAlt": "Ilustrasi fitur Pesan & Santai",
"imageAltDark": "Ilustrasi fitur Pesan & Santai dalam mode gelap"
},
"item2": {
"imageAlt": "Ilustrasi fitur Daftar Periksa Pintar",
"imageAltDark": "Ilustrasi fitur Daftar Periksa Pintar dalam mode gelap"
},
"item3": {
"imageAlt": "Ilustrasi fitur Hemat Lebih Banyak",
"imageAltDark": "Ilustrasi fitur Hemat Lebih Banyak dalam mode gelap"
}
},
"imageAltMapBackground": "Latar belakang peta",
"imageAltAppRightImg": "Gambar kanan aplikasi",
"buttonDownloadOnAppStore": "Unduh di App Store",
"buttonGetItOnGooglePlay": "Dapatkan di Google Play",
"tourSuggestion": {
"heading": "Negara",
"subHeading": "Tempat populer yang kami rekomendasikan untuk Anda",
"vectorAlt": "Gambar vektor dekoratif yang menggambarkan saran tur",
"categories": {
"Nature House": "Rumah Alam",
"Wooden house": "Rumah Kayu",
"Houseboat": "Rumah Perahu",
"Farm House": "Rumah Pertanian",
"Dome House": "Rumah Kubah",
"Wooden Dome": "Kubah Kayu"
}
},
"tourSuggestion.heading": "Negara",
"tourSuggestion.subHeading": "Tempat populer yang kami rekomendasikan untuk Anda",
"cardCategory3.imageAltPlaces": "Tempat",
"cardCategory3.tours": "Tur",
"sectionClientSay": {
"heading": "Apa kata pelanggan kami tentang kami?",
"imageAltMain": "Gambar utama Ulasan Klien",
"imageAltClient1": "Klien 1",
"imageAltClient2": "Klien 2",
"imageAltClient3": "Klien 3",
"imageAltClient4": "Klien 4",
"imageAltClient5": "Klien 5",
"imageAltClient6": "Klien 6",
"imageAltQuotation1": "Tanda kutip kiri",
"imageAltQuotation2": "Tanda kutip kanan",
"testimonials": {
"0": {
"content": "Perjalanan dengan Aqila adalah pengalaman yang memperkaya secara spiritual. Pemandu yang berpengetahuan dan staf yang ramah membuat setiap momen tak terlupakan.",
"clientName": "Tiana Abie",
"clientAddress": "Malaysia"
},
"1": {
"content": "Semua diatur dengan sempurna, dari akomodasi hingga kunjungan ke situs suci. Saya merasa sangat diperhatikan sepanjang perjalanan.",
"clientName": "Lennie Swiffan",
"clientAddress": "London"
},
"2": {
"content": "Bergabung dengan tur ini memungkinkan saya untuk terhubung lebih dalam dengan sesama pelancong dan warisan saya. Sangat merekomendasikan Aqila untuk perjalanan yang bermakna!",
"clientName": "Berta Emili",
"clientAddress": "Tokyo"
}
}
},
"sectionGridFilterCard": {
"allTours": "Semua Tur",
"noToursAvailable": "Tidak ada tur yang tersedia",
"toursInfo": "{{count}} malam · {{dateRange}} · {{guests}} Tamu"
},
"accountHeading": "Informasi Akun",
"nameLabel": "Nama",
"emailLabel": "Email",
"phoneLabel": "Telepon",
"changeImage": "Ganti Gambar",
"updateButton": "Perbarui Info",
"signOutButton": "Keluar",
"deleteButton": "Hapus Akun",
"deleteSuccess": "Akun Anda telah berhasil dihapus.",
"signOutSuccess": "Anda telah berhasil keluar.",
"updateSuccess": "Informasi Anda telah berhasil diperbarui.",
"noChanges": "Tidak ada perubahan yang terdeteksi.",
"errorGeneric": "Terjadi kesalahan. Silakan coba lagi.",
"errorUnknown": "Terjadi kesalahan yang tidak diketahui.",
"benefits": "MANFAAT"
}

23
public/locales/id/footer.json

@ -0,0 +1,23 @@
{
"widgetMenus": {
"Quick Links": {
"title": "Tautan Cepat",
"menus": [
{ "label": "Semua Tur" },
{ "label": "Blog" },
{ "label": "FAQ" },
{ "label": "Tentang Kami" }
]
}
},
"aboutUs": {
"title": "Tentang Kami",
"description": "Kami adalah tim yang berdedikasi untuk menciptakan pengalaman perjalanan yang tak terlupakan bagi para penjelajah dan pemimpi. Dari pelarian tenang ke pantai tropis hingga petualangan penuh adrenalin di tempat-tempat eksotis, kami merancang perjalanan yang unik seperti Anda. Bergabunglah dengan kami dan mari jelajahi dunia, satu petualangan pada satu waktu!"
},
"footerNav": {
"description": "Kami adalah tim yang berdedikasi untuk menciptakan pengalaman perjalanan yang tak terlupakan bagi para penjelajah dan pemimpi."
},
"socials": {
"title": "Ikuti Kami"
}
}

91
public/locales/id/form.json

@ -0,0 +1,91 @@
{
"selectTour": "Pilih tur Anda",
"tourPeriod": "Periode tur",
"startEndDate": "Mulai - Selesai",
"guests": "Tamu",
"addGuests": "Tambahkan tamu",
"adults": "Dewasa",
"adultsDesc": "Usia 13 tahun atau lebih",
"children": "Anak-anak",
"childrenDesc": "Usia 2–12 tahun",
"infants": "Bayi",
"infantsDesc": "Usia 0–2 tahun",
"clear": "Hapus",
"submit": "Kirim",
"login": "Masuk",
"phoneNumber": "Nomor Telepon",
"enterPhoneNumber": "Masukkan nomor telepon Anda",
"password": "Kata sandi",
"forgotPassword": "Lupa kata sandi?",
"enterPassword": "Masukkan kata sandi Anda",
"continue": "Lanjutkan",
"createAccount": "Buat akun",
"invalidPhoneNumber": "Nomor telepon tidak valid.",
"invalidPhoneNumberFormat": "Format nomor telepon tidak valid.",
"passwordRequired": "Kata sandi diperlukan.",
"loginSuccessful": "Masuk berhasil!",
"loginFailed": "Masuk gagal, periksa kredensial Anda.",
"unknownError": "Terjadi kesalahan yang tidak diketahui.",
"hidePassword": "Sembunyikan kata sandi",
"showPassword": "Tampilkan kata sandi",
"signup": "Daftar",
"changePassword": "Ubah kata sandi",
"fullName": "Nama lengkap",
"enterFullName": "Masukkan nama lengkap Anda",
"confirmPassword": "Konfirmasi kata sandi",
"enterConfirmPassword": "Masukkan kata sandi Anda kembali",
"alreadyHaveAccount": "Sudah punya akun?",
"signIn": "Masuk",
"errorOccurred": "Terjadi kesalahan.",
"hideConfirmPassword": "Sembunyikan konfirmasi kata sandi",
"showConfirmPassword": "Tampilkan konfirmasi kata sandi",
"verificationMethod": "Metode verifikasi",
"sendViaWhatsApp": "Kirim lewat WhatsApp",
"sendViaSMS": "Kirim lewat SMS",
"verificationCode": "Kode verifikasi",
"enterOtpDescription": "Masukkan kode 5 digit yang kami kirim untuk menyelesaikan pendaftaran akun Anda",
"haventGotCode": "Belum menerima kode konfirmasi?",
"resend": "Kirim ulang",
"seconds": "Detik",
"confirm": "Konfirmasi",
"signInSuccessful": "Masuk Anda berhasil",
"somethingWentWrong": "Ada yang salah. Silakan coba lagi.",
"otpInput": "Masukkan OTP {{index}}",
"otpNotComplete": "Kode verifikasi tidak valid",
"addNewPassenger": "Tambah penumpang baru",
"passengerInfo": "Informasi penumpang",
"passportNo": "Nomor Paspor",
"fullNameRequired": "Nama lengkap diperlukan.",
"passportRequired": "Nomor paspor diperlukan.",
"passportNumeric": "Nomor paspor harus berupa angka.",
"dobRequired": "Tanggal lahir diperlukan.",
"phoneRequired": "Nomor telepon diperlukan.",
"phoneNumeric": "Nomor telepon harus berupa angka.",
"passportImageRequired": "Gambar paspor diperlukan.",
"editPassengerInfo": "Ubah Informasi Penumpang",
"backToBills": "Kembali ke Tagihan",
"noBillsAvailable": "Tidak ada tagihan yang tersedia",
"awaitingPayment": "Menunggu Pembayaran",
"approved": "Disetujui",
"rejected": "Ditolak",
"pending": "Tertunda",
"issuedDate": "Tanggal Dikeluarkan",
"expirationDate": "Tanggal Kadaluarsa",
"tourInvoiceAmount": "Jumlah Faktur Tur",
"viewBill": "Lihat Tagihan",
"transactionReceiptUpdated": "Tanda terima transaksi berhasil diperbarui",
"updateFailed": "Gagal memperbarui tanda terima",
"billDetails": "Detail Tagihan",
"whyRejected": "Mengapa ditolak?",
"numberOfPassengers": "Jumlah Penumpang",
"adult": "Dewasa",
"infant": "Bayi",
"tourPrice": "Harga Tur",
"accountNumber": "Nomor Akun",
"uploadPassportImage": "Unggah Gambar Paspor",
"noFileSelected": "Tidak ada file yang dipilih",
"uploadedImage": "Gambar yang diunggah",
"delete": "Hapus",
"currentReceipt": "Tanda Terima Saat Ini",
"copy": "Salin"
}

15
public/locales/id/navigation.json

@ -0,0 +1,15 @@
{
"textHome": "Beranda",
"textAllTours": "Semua Tur",
"textBlog": "Blog",
"textFaq": "FAQ",
"textAbout": "Tentang",
"textLanguage": "Bahasa",
"textNoMatch": "Tidak Ditemukan Hasil",
"searchPlaceholder": "Ketik dan cari...",
"customTrip": "Tur Kustom",
"navAccount": "Akun",
"navMyTrips": "Perjalanan Saya",
"navPassengersList": "Daftar Penumpang",
"navBills": "Tagihan"
}

24
public/locales/ru/FAQ.json

@ -0,0 +1,24 @@
{
"faqTitle": "Часто задаваемые вопросы",
"faqSubtitle": "Есть вопросы? Мы здесь, чтобы помочь!",
"faqQuestion1": "Как я могу забронировать тур на вашем сайте?",
"faqAnswer1": "Чтобы забронировать тур, просто выберите желаемое направление из 'Списка туров', выберите даты и следуйте шагам, чтобы завершить процесс бронирования.",
"faqQuestion2": "Могу ли я настроить свой тур?",
"faqAnswer2": "Да, наша функция 'Настроить тур' позволяет вам адаптировать путешествие в соответствии с вашими предпочтениями. Нажмите на 'Настроить тур' в верхней части страницы, чтобы начать персонализировать ваш опыт.",
"faqQuestion3": "Какие способы оплаты вы принимаете?",
"faqAnswer3": "Мы принимаем все основные кредитные карты, PayPal и банковские переводы. Вы можете выбрать предпочтительный способ оплаты во время оформления заказа.",
"faqQuestion4": "Как я могу узнать, что моя бронь подтверждена?",
"faqAnswer4": "После завершения процесса оплаты вы получите подтверждающее письмо с деталями бронирования. Вы также можете просматривать детали бронирования в своей панели управления аккаунтом.",
"faqQuestion5": "Могу ли я отменить или изменить свою бронь?",
"faqAnswer5": "Да, вы можете отменить или изменить свою бронь через ваш аккаунт. Обратите внимание, что отмена должна быть сделана не позднее чем за 24 часа до начала тура, чтобы иметь право на возврат средств.",
"faqQuestion6": "Есть ли скрытые сборы?",
"faqAnswer6": "Нет, все сборы прозрачны и показываются заранее перед завершением бронирования. Мы гарантируем, что скрытых сборов нет.",
"faqQuestion7": "Предлагаете ли вы скидки для групп?",
"faqAnswer7": "Да, мы предлагаем скидки для групповых бронирований. Пожалуйста, свяжитесь с нашей службой поддержки для получения дополнительной информации о групповых тарифах.",
"faqQuestion8": "Включает ли страховка путешествия в бронирование?",
"faqAnswer8": "Да, все наши турпакеты включают базовую страховку путешествий. Вы можете выбрать улучшенную страховку во время оформления заказа.",
"faqQuestion9": "Что происходит, если тур отменен поставщиком?",
"faqAnswer9": "Если тур отменяется поставщиком по независящим от нас причинам, вы получите полный возврат средств или возможность переноса тура на другую дату.",
"faqQuestion10": "Как я могу связаться со службой поддержки клиентов?",
"faqAnswer10": "Вы можете связаться со службой поддержки клиентов, нажав на кнопку 'Связаться с нами' внизу страницы или посетив наш раздел поддержки."
}

222
public/locales/ru/common.json

@ -0,0 +1,222 @@
{
"home": "Главная",
"allTours": "Все туры",
"blogs": "Блоги",
"faq": "Часто задаваемые вопросы",
"aboutUs": "О нас",
"customTour": "Индивидуальный тур",
"searchPlaceholder": "Куда?",
"searchDescription": "Любое место • Любая неделя • Добавить гостей",
"beginAdventure": "Начните ваше",
"beginAdventure1": "духовное",
"beginAdventure2": "приключение",
"planPilgrimage": "Планируйте своё паломничество с легкостью. Найдите лучшие варианты проживания, транспорт и экскурсии по шиитским святыням по всему миру.",
"startJourney": "Начните своё путешествие",
"listOfTours": "Список туров",
"exploreTours": "Исследуйте туры и места проживания, адаптированные для духовного и незабываемого путешествия",
"tourPeriod": "Период тура",
"tourPeriodDescription": "Начало - Конец",
"guests": "Гости",
"addGuests": "Добавить гостей",
"available": "Доступно",
"soldOut": "Распродано",
"showMore": "Показать больше",
"happeningCities": "Города, где проходят события",
"costEffectiveAdvertising": "Рентабельная реклама",
"costEffectiveDescription": "С бесплатным размещением вы можете рекламировать свою аренду без предоплаты",
"reachMillions": "Достигайте миллионов с Chisfis",
"reachMillionsDescription": "Миллионы людей ищут уникальные места для проживания по всему миру",
"secureAndSimple": "Безопасно и просто",
"secureDescription": "Размещение на Holiday Lettings предоставляет вам безопасный и простой способ принимать бронирования и платежи онлайн",
"mobileApps": "Мобильные приложения",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
"installation": "Установка",
"releaseNotes": "Примечания к версии",
"upgradeGuide": "Руководство по обновлению",
"browserSupport": "Поддержка браузеров",
"editorSupport": "Поддержка редактора",
"designFeatures": "Особенности дизайна",
"prototyping": "Прототипирование",
"designSystems": "Системы дизайна",
"pricing": "Ценообразование",
"security": "Безопасность",
"bestPractices": "Лучшие практики",
"support": "Поддержка",
"developers": "Разработчики",
"learnDesign": "Изучение дизайна",
"releases": "Выпуски",
"discussionForums": "Форумы для обсуждений",
"codeOfConduct": "Кодекс поведения",
"communityResources": "Ресурсы сообщества",
"contributing": "Вклад",
"concurrentMode": "Режим параллельной работы",
"goodNews": "Хорошие новости издалека",
"whatPeopleThink": "Посмотрим, что люди думают о Chisfis",
"testimonial": "Это место точно такое же, как на фотографии, опубликованной на Chisfis. Отличное обслуживание, мы отлично провели время!",
"clientName": "Тиана Аби",
"clientLocation": "Малайзия",
"myTrips": "Мои поездки",
"account": "Аккаунт",
"menu": "Меню",
"gettingStarted": "Начало работы",
"explore": "Изучить",
"resources": "Ресурсы",
"community": "Сообщество",
"placeType": "Тип места",
"noTours": "Нет доступных туров",
"itinerary": "Маршрут",
"itineraryTitle": "Маршрут",
"total": "Итого",
"reserve": "Забронировать",
"tourFeatures": "Особенности тура",
"tourFeaturesTitle": "Особенности тура",
"startRating": "Оценка начала",
"listingDetails": "Детали",
"imageAlt": "Изображение тура",
"loading": "Загрузка...",
"adults": "Взрослые",
"adultsDesc": "Возраст 13 лет и старше",
"children": "Дети",
"childrenDesc": "Возраст 2–12 лет",
"infants": "Младенцы",
"infantsDesc": "Возраст 0–2 года",
"traveler": "Путешественник",
"responses": "Ответы ({{count}})",
"submit": "Отправить",
"cancel": "Отменить",
"relatedPosts": "Похожие записи",
"aboutUsHeading": "👋 О нас",
"aboutUsSubheading": "Мы страстная команда, стремящаяся создавать незабываемые путешествия для исследователей и мечтателей. От спокойных побегов на тропических пляжах до захватывающих приключений в экзотических местах — мы составляем путешествия, которые так же уникальны, как и вы. Присоединяйтесь к нам и давайте исследовать мир, одно приключение за раз!",
"statisticTitle": "🚀 Быстрые факты",
"statisticDescription": "Мы беспристрастны и независимы, и каждый день создаем уникальные, мировые программы и контент.",
"statisticHeading1": "10 миллионов",
"statisticSubHeading1": "Статьи были опубликованы по всему миру (на 30 сентября 2021 года)",
"statisticHeading2": "100,000",
"statisticSubHeading2": "Зарегистрированных пользователей (на 30 сентября 2021 года)",
"statisticHeading3": "220+",
"statisticSubHeading3": "Страны и регионы, где мы представлены (на 30 сентября 2021 года)",
"customTrip": "Индивидуальный тур",
"guide": "Гид",
"guideDescription": "Сначала напишите место вашего отправления, затем выберите первую точку назначения вашей поездки, количество ночей проживания и средство передвижения, затем выберите ваши дальнейшие точки назначения, если хотите.",
"beginYourTrip": "Начните ваше путешествие",
"startDate": "Дата начала",
"numberOfPassengers": "Количество пассажиров",
"destination": "Пункт назначения",
"selectCity": "Выберите город",
"transportation": "Транспорт",
"selectTransport": "Выберите транспорт",
"hotel": "Отель",
"selectHotel": "Выберите отель",
"duration": "Продолжительность",
"finishDate": "Дата окончания",
"addDestination": "Добавить пункт назначения",
"continue": "Продолжить",
"successMessage": "Успешно зарегистрировано",
"to": "к",
"login": "Войти",
"signup": "Зарегистрироваться",
"All": "Все",
"create": "Создать",
"createPersonalizedTourLine1": "Создайте свой персонализированный тур и разработайте",
"createPersonalizedTourLine2": "идеальный опыт путешествия, соответствующий вашим предпочтениям.",
"imageAltCustomTourBackground": "Фон индивидуального тура",
"howItWorks": {
"title": "Как это работает",
"desc": "Оставайтесь спокойными и продолжайте путешествовать",
"vectorAlt": "Декоративная векторная картинка, иллюстрирующая, как это работает",
"bookAndRelax": {
"title": "Забронируйте и расслабьтесь",
"desc": "Позвольте каждому путешествию быть вдохновляющим, каждой комнате — мирным пространством"
},
"smartChecklist": {
"title": "Умный список",
"desc": "Позвольте каждому путешествию быть вдохновляющим, каждой комнате — мирным пространством"
},
"saveMore": {
"title": "Сэкономьте больше",
"desc": "Позвольте каждому путешествию быть вдохновляющим, каждой комнате — мирным пространством"
},
"item1": {
"imageAlt": "Иллюстрация функции Забронируйте и расслабьтесь",
"imageAltDark": "Иллюстрация функции Забронируйте и расслабьтесь в темном режиме"
},
"item2": {
"imageAlt": "Иллюстрация функции Умный список",
"imageAltDark": "Иллюстрация функции Умный список в темном режиме"
},
"item3": {
"imageAlt": "Иллюстрация функции Сэкономьте больше",
"imageAltDark": "Иллюстрация функции Сэкономьте больше в темном режиме"
}
},
"imageAltMapBackground": "Фон карты",
"imageAltAppRightImg": "Правая картинка приложения",
"buttonDownloadOnAppStore": "Скачать в App Store",
"buttonGetItOnGooglePlay": "Получить в Google Play",
"tourSuggestion": {
"heading": "Страны",
"subHeading": "Популярные места, которые мы рекомендуем для вас",
"vectorAlt": "Декоративная векторная картинка, иллюстрирующая предложения по турам",
"categories": {
"Nature House": "Дом в природе",
"Wooden house": "Деревянный дом",
"Houseboat": "Дом на воде",
"Farm House": "Фермерский дом",
"Dome House": "Купольный дом",
"Wooden Dome": "Деревянный купол"
}
},
"tourSuggestion.heading": "Страны",
"tourSuggestion.subHeading": "Популярные места, которые мы рекомендуем для вас",
"cardCategory3.imageAltPlaces": "Места",
"cardCategory3.tours": "Туры",
"sectionClientSay": {
"heading": "Что говорят наши клиенты о нас?",
"imageAltMain": "Основное изображение отзывов клиентов",
"imageAltClient1": "Клиент 1",
"imageAltClient2": "Клиент 2",
"imageAltClient3": "Клиент 3",
"imageAltClient4": "Клиент 4",
"imageAltClient5": "Клиент 5",
"imageAltClient6": "Клиент 6",
"imageAltQuotation1": "Левая кавычка",
"imageAltQuotation2": "Правая кавычка",
"testimonials": {
"0": {
"content": "Путешествие с Aqila было духовно обогащающе. Знание гидов и дружелюбный персонал сделали каждое мгновение незабываемым.",
"clientName": "Тиана Аби",
"clientAddress": "Малайзия"
},
"1": {
"content": "Все было идеально организовано, от проживания до посещений святых мест. Я чувствовал заботу на протяжении всей поездки.",
"clientName": "Ленни Суффан",
"clientAddress": "Лондон"
},
"2": {
"content": "Присоединившись к этому туру, я смог глубже понять себя и моё наследие. Рекомендую Aqila для значимого путешествия!",
"clientName": "Берта Эмили",
"clientAddress": "Токио"
}
}
},
"sectionGridFilterCard": {
"allTours": "Все туры",
"noToursAvailable": "Нет доступных туров",
"toursInfo": "{{count}} ночей · {{dateRange}} · {{guests}} Гостей"
},
"accountHeading": "Информация об аккаунте",
"nameLabel": "Имя",
"emailLabel": "Электронная почта",
"phoneLabel": "Телефон",
"changeImage": "Изменить изображение",
"updateButton": "Обновить информацию",
"signOutButton": "Выйти",
"deleteButton": "Удалить аккаунт",
"deleteSuccess": "Ваш аккаунт был успешно удален.",
"signOutSuccess": "Вы успешно вышли из аккаунта.",
"updateSuccess": "Ваша информация была успешно обновлена.",
"noChanges": "Изменения не обнаружены.",
"errorGeneric": "Произошла ошибка. Пожалуйста, попробуйте снова.",
"errorUnknown": "Произошла неизвестная ошибка.",
"benefits": "ПРЕИМУЩЕСТВА"
}

23
public/locales/ru/footer.json

@ -0,0 +1,23 @@
{
"widgetMenus": {
"Quick Links": {
"title": "Быстрые ссылки",
"menus": [
{ "label": "Все туры" },
{ "label": "Блоги" },
{ "label": "FAQ" },
{ "label": "О нас" }
]
}
},
"aboutUs": {
"title": "О нас",
"description": "Мы — команда, увлеченная созданием незабываемых путешествий для исследователей и мечтателей. От спокойного отдыха на тропических пляжах до приключений, полных адреналина, в экзотических местах — мы разрабатываем уникальные путешествия, такие же уникальные, как и вы. Присоединяйтесь к нам, и давайте открывать мир, одно приключение за раз!"
},
"footerNav": {
"description": "Мы — команда, увлеченная созданием незабываемых путешествий для исследователей и мечтателей."
},
"socials": {
"title": "Следите за нами"
}
}

91
public/locales/ru/form.json

@ -0,0 +1,91 @@
{
"selectTour": "Выберите ваш тур",
"tourPeriod": "Период тура",
"startEndDate": "Начало - Конец",
"guests": "Гости",
"addGuests": "Добавить гостей",
"adults": "Взрослые",
"adultsDesc": "Возраст 13 лет и старше",
"children": "Дети",
"childrenDesc": "Возраст 2–12 лет",
"infants": "Младенцы",
"infantsDesc": "Возраст 0–2 года",
"clear": "Очистить",
"submit": "Отправить",
"login": "Войти",
"phoneNumber": "Номер телефона",
"enterPhoneNumber": "Введите ваш номер телефона",
"password": "Пароль",
"forgotPassword": "Забыли пароль?",
"enterPassword": "Введите ваш пароль",
"continue": "Продолжить",
"createAccount": "Создать аккаунт",
"invalidPhoneNumber": "Неверный номер телефона.",
"invalidPhoneNumberFormat": "Неверный формат номера телефона.",
"passwordRequired": "Пароль обязателен.",
"loginSuccessful": "Вход выполнен успешно!",
"loginFailed": "Не удалось войти, проверьте свои данные.",
"unknownError": "Произошла неизвестная ошибка.",
"hidePassword": "Скрыть пароль",
"showPassword": "Показать пароль",
"signup": "Зарегистрироваться",
"changePassword": "Изменить пароль",
"fullName": "Полное имя",
"enterFullName": "Введите ваше полное имя",
"confirmPassword": "Подтвердите пароль",
"enterConfirmPassword": "Введите пароль повторно",
"alreadyHaveAccount": "Уже есть аккаунт?",
"signIn": "Войти",
"errorOccurred": "Произошла ошибка.",
"hideConfirmPassword": "Скрыть подтверждение пароля",
"showConfirmPassword": "Показать подтверждение пароля",
"verificationMethod": "Метод подтверждения",
"sendViaWhatsApp": "Отправить через WhatsApp",
"sendViaSMS": "Отправить через SMS",
"verificationCode": "Код подтверждения",
"enterOtpDescription": "Введите 5-значный код, который мы отправили для завершения регистрации",
"haventGotCode": "Еще не получили код подтверждения?",
"resend": "Отправить снова",
"seconds": "Секунды",
"confirm": "Подтвердить",
"signInSuccessful": "Вы успешно вошли в систему",
"somethingWentWrong": "Что-то пошло не так. Пожалуйста, попробуйте снова.",
"otpInput": "Ввод OTP {{index}}",
"otpNotComplete": "Код подтверждения недействителен",
"addNewPassenger": "Добавить нового пассажира",
"passengerInfo": "Информация о пассажире",
"passportNo": "Номер паспорта",
"fullNameRequired": "Полное имя обязательно.",
"passportRequired": "Номер паспорта обязателен.",
"passportNumeric": "Номер паспорта должен быть числовым.",
"dobRequired": "Дата рождения обязательна.",
"phoneRequired": "Номер телефона обязателен.",
"phoneNumeric": "Номер телефона должен быть числовым.",
"passportImageRequired": "Изображение паспорта обязательно.",
"editPassengerInfo": "Редактировать информацию о пассажире",
"backToBills": "Вернуться к счетам",
"noBillsAvailable": "Нет доступных счетов",
"awaitingPayment": "Ожидание оплаты",
"approved": "Одобрено",
"rejected": "Отклонено",
"pending": "В ожидании",
"issuedDate": "Дата выдачи",
"expirationDate": "Дата истечения",
"tourInvoiceAmount": "Сумма счета за тур",
"viewBill": "Просмотреть счет",
"transactionReceiptUpdated": "Квитанция о транзакции успешно обновлена",
"updateFailed": "Не удалось обновить квитанцию",
"billDetails": "Детали счета",
"whyRejected": "Почему отклонено?",
"numberOfPassengers": "Количество пассажиров",
"adult": "Взрослый",
"infant": "Младенец",
"tourPrice": "Стоимость тура",
"accountNumber": "Номер счета",
"uploadPassportImage": "Загрузить изображение паспорта",
"noFileSelected": "Файл не выбран",
"uploadedImage": "Загруженное изображение",
"delete": "Удалить",
"currentReceipt": "Текущая квитанция",
"copy": "Копировать"
}

15
public/locales/ru/navigation.json

@ -0,0 +1,15 @@
{
"text-home": "Главная",
"text-all-tours": "Все туры",
"text-blog": "Блог",
"text-faq": "Часто задаваемые вопросы",
"text-about": "О нас",
"text-language": "Язык",
"text-no-match": "Результатов не найдено",
"search-placeholder": "Введите для поиска...",
"customTrip": "Индивидуальный тур",
"navAccount": "Аккаунт",
"navMyTrips": "Мои поездки",
"navPassengersList": "Список пассажиров",
"navBills": "Счета"
}

24
src/app/[locale]/(account-pages)/(components)/Nav.tsx

@ -4,33 +4,35 @@ import { Route } from "@/routers/types";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
import { useTranslation } from "react-i18next";
export const Nav = () => {
const { t } = useTranslation("navigation");
const pathname = usePathname();
const listNav: Route[] = [
"/account",
"/my-trips",
"/passengers-list",
"/bills",
{ path: "/account", label: t("navAccount") },
{ path: "/my-trips", label: t("navMyTrips") },
{ path: "/passengers-list", label: t("navPassengersList") },
{ path: "/bills", label: t("navBills") },
];
return (
<div className="container">
<div className="flex space-x-8 md:space-x-14 overflow-x-auto hiddenScrollbar">
{listNav.map((item) => {
const isActive = pathname === item;
<div className="flex space-x-8 md:space-x-14 overflow-x-auto hiddenScrollbar rtl:space-x-0">
{listNav.map(({ path, label }) => {
const isActive = pathname === path;
return (
<Link
key={item}
href={item}
className={`block py-5 md:py-8 border-b-2 flex-shrink-0 capitalize ${
key={path}
href={path}
className={`block py-5 md:py-8 border-b-2 flex-shrink-0 capitalize rtl:!ml-14 ${
isActive
? "border-primary-500 font-medium"
: "border-transparent"
}`}
>
{item.replace("-", " ").replace("/", " ")}
{label}
</Link>
);
})}

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

@ -1,6 +1,7 @@
"use client";
import React, { useState, ChangeEvent, FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import Label from "@/components/Label";
import Avatar from "@/shared/Avatar";
import ButtonPrimary from "@/shared/ButtonPrimary";
@ -14,55 +15,28 @@ import { toast } from "react-toastify";
export interface AccountPageProps {}
interface User {
fullname: string;
email: string;
phone_number: string;
avatar: string;
token: string;
}
interface LoadingState {
delete?: boolean;
change?: boolean;
}
interface APIResponse {
avatar: string;
email: string;
fullname: string;
phone_number: string;
}
const AccountPage: FC<AccountPageProps> = () => {
const { t } = useTranslation("common");
const router = useRouter();
const { user, setUser, clerUser } = useUserContext();
// Redirect to home if user object is empty
useEffect(() => {
if (!Object.keys(user).length) {
router.replace("/");
}
}, [user, router]);
// Return null to avoid rendering when redirecting
// if (!Object.keys(user).length) {
// return null;
// }
const [name, setName] = useState<string>(user.fullname || "");
const [email, setEmail] = useState<string>(user.email || "");
const [number, setNumber] = useState<string>(user.phone_number || "");
const [image, setImage] = useState<File | null>(null);
const [imageURL, setImageURL] = useState<string | null>(null);
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<LoadingState>({});
const [loading, setLoading] = useState({ delete: false, change: false });
const deleteHandler = async (): Promise<void> => {
setError("");
setLoading({ delete: true });
setLoading({ ...loading, delete: true });
try {
const response = await axiosInstance.delete(`/api/account/profile/delete/`, {
headers: {
@ -71,47 +45,38 @@ const AccountPage: FC<AccountPageProps> = () => {
});
if (response.status === 204) {
clerUser();
toast.success("Your Delete account was successful")
toast.success(t("deleteSuccess"));
} else {
setError("Something went wrong");
setError(t("errorGeneric"));
}
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
} else {
setError("An unknown error occurred");
setError(t("errorUnknown"));
}
} finally {
setLoading({ delete: false });
setLoading({ ...loading, delete: false });
}
};
const signOutHandler = (): void => {
clerUser();
toast.success("Your sign out was successful")
toast.success(t("signOutSuccess"));
};
const changeHandler = async (): Promise<void> => {
setError("");
setLoading({ change: true });
setLoading({ ...loading, change: true });
const formData = new FormData();
if (name !== user.fullname) {
formData.append("fullname", name);
}
if (email !== user.email) {
formData.append("email", email);
}
if (imageURL) {
formData.append("avatar", imageURL);
}
if (name !== user.fullname) formData.append("fullname", name);
if (email !== user.email) formData.append("email", email);
if (imageURL) formData.append("avatar", imageURL);
if (formData.has("fullname") || formData.has("email") || formData.has("avatar")) {
try {
const response = await axiosInstance.put<APIResponse>(
const response = await axiosInstance.put(
`/api/account/profile/update/`,
formData,
{
@ -122,37 +87,33 @@ const AccountPage: FC<AccountPageProps> = () => {
}
);
if (response.status === 200) {
console.log(response);
toast.success("Updated successfully");
toast.success(t("updateSuccess"));
setUser({
...user,
avatar: response.data.avatar,
email: response.data.email,
fullname: response.data.fullname,
phone_number: response.data.phone_number,
email: response.data.email,
avatar: response.data.avatar,
});
} else {
setError("Something went wrong");
setError(t("errorGeneric"));
}
} catch (error: unknown) {
if (error instanceof Error) {
setError(error.message);
} else {
setError("An unknown error occurred");
setError(t("errorUnknown"));
}
} finally {
setLoading({ change: false });
setLoading({ ...loading, change: false });
}
} else {
toast.info("No changes detected");
setLoading({ change: false });
toast.info(t("noChanges"));
setLoading({ ...loading, change: false });
}
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>): Promise<void> => {
const file : File | undefined = e.target.files?.[0];
const file = e.target.files?.[0];
if (file) {
const uploadedFile = await getImageURL(file);
setImageURL(uploadedFile.url);
@ -162,7 +123,7 @@ const AccountPage: FC<AccountPageProps> = () => {
return (
<div className="space-y-6 sm:space-y-8">
<h2 className="text-3xl font-semibold">Account information</h2>
<h2 className="text-3xl font-semibold">{t("accountHeading")}</h2>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="flex flex-col md:flex-row">
<div className="flex-shrink-0 flex items-start">
@ -172,22 +133,7 @@ const AccountPage: FC<AccountPageProps> = () => {
sizeClass="w-32 h-32"
/>
<div className="absolute inset-0 bg-black bg-opacity-60 flex flex-col items-center justify-center text-neutral-50 cursor-pointer">
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.5 5H7.5C6.83696 5 6.20107 5.26339 5.73223 5.73223C5.26339 6.20107 5 6.83696 5 7.5V20M5 20V22.5C5 23.163 5.26339 23.7989 5.73223 24.2678C6.20107 24.7366 6.83696 25 7.5 25H22.5C23.163 25 23.7989 24.7366 24.2678 24.2678C24.7366 23.7989 25 23.163 25 22.5V17.5M5 20L10.7325 14.2675C11.2013 13.7988 11.8371 13.5355 12.5 13.5355C13.1629 13.5355 13.7987 13.7988 14.2675 14.2675L17.5 17.5M25 12.5V17.5M25 17.5L23.0175 15.5175C22.5487 15.0488 21.9129 14.7855 21.25 14.7855C20.5871 14.7855 19.9513 15.0488 19.4825 15.5175L17.5 17.5M17.5 17.5L20 20M22.5 5H27.5M25 2.5V7.5M17.5 10H17.5125"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className="mt-1 text-xs">Change Image</span>
<span className="mt-1 text-xs">{t("changeImage")}</span>
</div>
<input
type="file"
@ -197,9 +143,9 @@ const AccountPage: FC<AccountPageProps> = () => {
/>
</div>
</div>
<div className="flex-grow mt-10 md:mt-0 md:pl-16 max-w-3xl space-y-6">
<div className="flex-grow mt-10 md:mt-0 md:pl-16 max-w-3xl space-y-6 rtl:md:pr-16">
<div>
<Label>Name</Label>
<Label>{t("nameLabel")}</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
@ -207,7 +153,7 @@ const AccountPage: FC<AccountPageProps> = () => {
/>
</div>
<div>
<Label>Email</Label>
<Label>{t("emailLabel")}</Label>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
@ -215,7 +161,7 @@ const AccountPage: FC<AccountPageProps> = () => {
/>
</div>
<div className="max-w-lg">
<Label>Phone</Label>
<Label>{t("phoneLabel")}</Label>
<Input
value={number}
onChange={(e) => setNumber(e.target.value)}
@ -225,17 +171,17 @@ const AccountPage: FC<AccountPageProps> = () => {
</div>
<div className="flex flex-col pt-2 sm:flex-row">
<ButtonPrimary className="mt-8" loading={loading.change} onClick={changeHandler}>
Update info
{t("updateButton")}
</ButtonPrimary>
<ButtonPrimary className="mt-8 sm:ml-8" onClick={signOutHandler}>
Sign out
{t("signOutButton")}
</ButtonPrimary>
<ButtonSecondary
loading={loading.delete}
onClick={deleteHandler}
className="opacity-60 mt-8 hover:bg-red-500 hover:text-white hover:opacity-100 sm:ml-8"
>
Delete account
{t("deleteButton")}
</ButtonSecondary>
</div>
</div>

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

@ -1,17 +1,16 @@
// BillCard.tsx
"use client";
import React from "react";
import ButtonPrimary from "@/shared/ButtonPrimary";
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";
import { useTranslation } from "react-i18next";
export type BillStatus = "awaiting_payment" | "approved" | "rejected" | "pending";
export interface Bill {
service: string;
id: number;
title: string;
created_at: string;
@ -31,63 +30,48 @@ export interface Bill {
};
transaction_receipt: string;
card_number: string | number;
service: string;
}
interface BillCardProps {
bill: Bill;
onViewDetail: (bill: Bill) => void; // Add onViewDetail prop
onViewDetail: (bill: Bill) => void;
}
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 { t } = useTranslation("form");
const statusStyles: { [key in BillStatus]: JSX.Element } = {
awaiting_payment: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-yellow-100 text-yellow-700">
{t("awaitingPayment")}
</span>
),
approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
{t("approved")}
</span>
),
rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
{t("rejected")}
</span>
),
pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
{t("pending")}
</span>
),
};
const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
console.log(bill);
return (
<div className="bg-white shadow-md rounded-lg p-4 mb-4 dark:bg-neutral-800">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center space-x-2">
{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>
)}
{bill.service.includes("tour") && <BiSolidPlaneAlt size={25} />}
{bill.service.includes("SIM Card") && <FaSimCard size={25} />}
{bill.service.includes("shop") && <BsCart3 size={25} />}
{bill.service.includes("tasrif") && <MdCurrencyExchange size={25} />}
<span>{bill.service}</span>
</h2>
{statusStyles[bill.status]}
@ -95,21 +79,21 @@ const BillCard: React.FC<BillCardProps> = ({ bill, onViewDetail }) => {
<div className="mt-4 text-sm text-gray-600">
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>Issued Date:</span>
<span>{t("issuedDate")}:</span>
<span>{bill.created_at}</span>
</div>
<div className="flex justify-between mb-2 dark:text-neutral-400">
<span>Expiration Date:</span>
<span>{t("expirationDate")}:</span>
<span>{bill.expiration_date}</span>
</div>
<div className="flex justify-between mt-2 font-semibold dark:text-neutral-400">
<span>Tour Invoice Amount:</span>
<span>{t("tourInvoiceAmount")}:</span>
<span className="text-orange-600">${bill.amount}</span>
</div>
</div>
<ButtonPrimary onClick={() => onViewDetail(bill)} className="mt-4">
View Bill
{t("viewBill")}
</ButtonPrimary>
</div>
);

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

@ -1,4 +1,4 @@
"use client"
"use client";
import React, { useState } from "react";
import axiosInstance from "@/components/api/axios";
@ -9,6 +9,7 @@ import { useUserContext } from "@/components/contexts/userContext";
import getImageURL from "@/components/api/getImageURL";
import { toast, ToastContainer } from "react-toastify";
import Image from "next/image";
import { useTranslation } from "react-i18next";
export type BillStatus =
| "awaiting_payment"
@ -16,50 +17,50 @@ export type BillStatus =
| "rejected"
| "pending";
export interface Bill {
id: number;
title: string;
created_at: string;
expiration_date: string;
amount: number;
status: BillStatus;
passengers: { type: string; count: number }[];
accountNumber: string;
message?: string;
uploadedImage?: {
name: string;
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;
}
export interface Bill {
id: number;
title: string;
created_at: string;
expiration_date: string;
amount: number;
status: BillStatus;
passengers: { type: string; count: number }[];
accountNumber: string;
message?: string;
uploadedImage?: {
name: string;
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]: JSX.Element } = {
awaiting_payment: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-yellow-100 text-yellow-700">
Awaiting Payment
{"t('awaitingPayment')"}
</span>
),
approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
Approved
{"t('approved')"}
</span>
),
rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
Rejected
{"t('rejected')"}
</span>
),
pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
Pending
{"t('pending')"}
</span>
),
};
@ -69,6 +70,7 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
const [loadingUpload, setLoadingUpload] = useState(false);
const [loading, setLoading] = useState(false);
const { user } = useUserContext();
const { t } = useTranslation("form");
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoadingUpload(true);
const file = e.target.files?.[0];
@ -82,24 +84,30 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
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}`,
.patch(
`/api/factors/update/${bill.id}/`,
{
transaction_receipt: uploadedFile,
},
})
{
headers: {
Authorization: `token ${user.token}`,
},
}
)
.then(() => {
toast.success("Transaction receipt updated successfully");
toast.success(t("transactionReceiptUpdated"));
setLoading(false);
})
.catch(() => {
toast.error("Error updating transaction receipt");
.catch((error) => {
console.log(error);
toast.error(t("updateFailed"));
setLoading(false);
});
} else {
console.log("No new file to upload");
console.log(t("noFileSelected"));
}
};
@ -112,58 +120,56 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
{bill.message && (
<div className="bg-red-100 text-red-700 p-3 mt-4 rounded-md">
<h3 className="font-semibold">Why was it rejected?</h3>
<h3 className="font-semibold">{t("whyRejected")}</h3>
<p className="text-sm mt-1">{bill.message}</p>
</div>
)}
<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>{t("issuedDate")}:</span>
<span>{bill.created_at}</span>
</div>
<div className="flex justify-between mb-2">
<span>Expiration Date:</span>
<span>{t("expirationDate")}:</span>
<span>{bill.expiration_date}</span>
</div>
<div className="mt-4">
<h3 className="font-semibold">Number of Passengers</h3>
<h3 className="font-semibold">{t("numberOfPassengers")}</h3>
<div className="flex justify-between">
<span>
# {bill.detail_service.passenger_counts.adults} (Adults)
</span>
{bill.detail_service.passenger_counts.adults && (
<span># {t("adult")}: {bill.detail_service.passenger_counts.adults}</span>
)}
</div>
<div className="flex justify-between">
<span>
# {bill.detail_service.passenger_counts.children} (Children)
</span>
{bill.detail_service.passenger_counts.children && (
<span># {t("children")}: {bill.detail_service.passenger_counts.children}</span>
)}
</div>
<div className="flex justify-between">
<span>
# {bill.detail_service.passenger_counts.infants} (Infants)
</span>
{bill.detail_service.passenger_counts.infants && (
<span># {t("infant")}: {bill.detail_service.passenger_counts.infants}</span>
)}
</div>
</div>
<div className="flex justify-between mt-4 font-semibold">
<span>Tour Price:</span>
<span>{t("tourPrice")}:</span>
<span className="text-orange-600">${bill.amount}</span>
</div>
<div className="mt-4">
<h3 className="font-semibold">Account Number</h3>
<h3 className="font-semibold">{t("accountNumber")}</h3>
<div className="flex items-center bg-gray-100 p-2 rounded-md mt-2 dark:bg-neutral-900">
<span className="flex-grow text-gray text-lg">
{bill.card_number}
</span>
<Button className="text-blue-500">Copy</Button>
<span className="flex-grow text-gray text-lg">{bill.card_number}</span>
<Button className="text-blue-500">{t("copy")}</Button>
</div>
</div>
{bill.uploadedImage ? (
<div className="mt-4">
<h3 className="font-semibold">Uploaded Image</h3>
<h3 className="font-semibold">{t("uploadedImage")}</h3>
<img
src={bill.uploadedImage.src}
alt="Uploaded"
@ -171,16 +177,14 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
/>
<div className="flex justify-between mt-2">
<span>{bill.uploadedImage.name}</span>
<span className="text-gray-500 text-sm">
{bill.uploadedImage.size}
</span>
<Button className="text-red-500">Delete</Button>
<span className="text-gray-500 text-sm">{bill.uploadedImage.size}</span>
<Button className="text-red-500">{t("delete")}</Button>
</div>
</div>
) : (
<div className="mt-4">
<label className="block font-semibold mb-2">
Upload Passport Image
{t("uploadPassportImage")}
</label>
<div className="flex items-center bg-gray-100 p-2 rounded-md dark:bg-neutral-900">
<Input
@ -188,14 +192,19 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
type="file"
onChange={handleFileChange}
/>
{loadingUpload && <p>Loading ...</p>}
{loadingUpload && <p>{t("loading")}</p>}
{bill.transaction_receipt ? (
<span className="flex-grow text-gray-500 text-sm ml-4">
<Image width={65} height={65} src={bill.transaction_receipt} alt="Current Receipt" />
<Image
width={65}
height={65}
src={bill.transaction_receipt}
alt={t("currentReceipt")}
/>
</span>
) : (
<span className="flex-grow text-gray-500 text-sm ml-4">
No File Selected
{t("noFileSelected")}
</span>
)}
</div>
@ -203,23 +212,27 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
)}
</div>
<ButtonPrimary loading ={loading} className="w-full mt-6" onClick={handleSubmit}>
Submit
<ButtonPrimary
loading={loading}
className="w-full mt-6"
onClick={handleSubmit}
>
{t("submit")}
</ButtonPrimary>
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
</div>
);
};
export default BillDetailCard;
export default BillDetailCard;

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

@ -1,15 +1,15 @@
"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 { useTranslation } from "react-i18next";
// Define the BillStatus and Bill interfaces
export type BillStatus = "awaiting_payment" | "approved" | "rejected" | "pending";
export interface Bill {
service: string;
id: number;
title: string;
created_at: string;
@ -29,12 +29,14 @@ export interface Bill {
};
transaction_receipt: string;
card_number: string | number;
service: string;
}
const BillsPage: React.FC = () => {
const [bills, setBills] = useState<Bill[]>([]);
const [selectedBill, setSelectedBill] = useState<Bill | null>(null);
const { user } = useUserContext();
const { t } = useTranslation("form");
const handleViewDetail = (bill: Bill) => {
setSelectedBill(bill);
@ -51,7 +53,6 @@ const BillsPage: React.FC = () => {
},
})
.then((response) => {
// Transform API response to match the Bill interface
const billsFromApi = response.data.results.map((bill: any) => ({
id: bill.id,
title: bill.title,
@ -66,6 +67,7 @@ const BillsPage: React.FC = () => {
detail_service: bill.detail_service,
transaction_receipt: bill.transaction_receipt,
card_number: bill.card_number,
service: bill.service,
}));
setBills(billsFromApi);
})
@ -79,14 +81,18 @@ const BillsPage: React.FC = () => {
{selectedBill ? (
<div>
<button onClick={handleBackToList} className="text-blue-500 mb-4">
Back to Bills
{t("backToBills")}
</button>
<BillDetailCard bill={selectedBill} />
</div>
) : (
) : bills.length > 0 ? (
bills.map((bill, index) => (
<BillCard key={index} bill={bill} onViewDetail={handleViewDetail} />
))
) : (
<div className="text-center mt-10 text-gray-500 dark:text-gray-400">
<p>{t("noBillsAvailable")}</p>
</div>
)}
</div>
);

48
src/app/[locale]/(account-pages)/my-trips/page.tsx

@ -1,37 +1,26 @@
"use client";
import { Tab } from "@headlessui/react";
import CarCard from "@/components/CarCard";
import ExperiencesCard from "@/components/ExperiencesCard";
import StayCard from "@/components/StayCard";
import {
DEMO_CAR_LISTINGS,
DEMO_EXPERIENCES_LISTINGS,
DEMO_STAY_LISTINGS,
} from "@/data/listings";
import React, { Fragment, use, useEffect, useState } from "react";
import ButtonSecondary from "@/shared/ButtonSecondary";
import axiosInstance from "@/components/api/axios";
import StayCard2 from "@/components/StayCard2";
import React, { useEffect, useState } from "react";
import axiosInstance from "@/components/api/axios";
import { useRouter } from "next/navigation";
import { useUserContext } from "@/components/contexts/userContext";
import { useTranslation } from "react-i18next";
import { StayDataType } from "@/data/types";
// interface stay {
// id : string
// }
const MyTrips = () => {
const router = useRouter()
let [tours , setTours] = useState([]);
const { user } =useUserContext()
const router = useRouter();
const { t } = useTranslation("common");
const [tours, setTours] = useState<StayDataType[]>([]);
const { user } = useUserContext();
useEffect(() => {
if (!Object.keys(user).length) {
router.replace("/signup");
}
}, [user, router]);
useEffect(() => {
axiosInstance
.get("/api/tours/orders/", {
@ -41,20 +30,17 @@ const MyTrips = () => {
})
.then((response) => {
setTours(response.data.results);
console.log(response);
})
.catch((error) => {
console.error(error);
});
}, []);
}, [user.token]);
const renderSection1 = () => {
return (
<div className="space-y-6 sm:space-y-8">
<div>
<h2 className="text-3xl font-semibold">Save lists</h2>
<h2 className="text-3xl font-semibold">{t("myTrips")}</h2>
</div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
@ -63,11 +49,15 @@ const MyTrips = () => {
<Tab.Panels>
<Tab.Panel className="mt-8">
<div className="grid grid-cols-1 gap-6 md:gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{tours.length ? (tours?.filter((_, i) => i < 8).map((stay : StayDataType) => (
<StayCard2 key={stay.id} data={stay} />
))) : (<h1 className="text-2xl"> You have no trips </h1>)}
</div>
<div className="flex mt-11 justify-center items-center">
{tours.length ? (
tours
.filter((_, i) => i < 8)
.map((stay: StayDataType) => (
<StayCard2 key={stay.id} data={stay} />
))
) : (
<h1 className="text-2xl">{t("noTrips")}</h1>
)}
</div>
</Tab.Panel>
</Tab.Panels>

15
src/app/[locale]/(account-pages)/passengers-list/PassengerTable.tsx

@ -1,17 +1,19 @@
"use client";
import React, { FC, useState } from "react";
import axiosInstance from "@/components/api/axios";
import { useUserContext } from "@/components/contexts/userContext";
import { useRouter } from "next/navigation";
import React, { FC, useState } from "react";
import { IoMdTrash } from "react-icons/io";
import { MdEdit } from "react-icons/md";
import { useTranslation } from "react-i18next";
interface Passenger {
birthdate: string;
fullname: string;
id: number;
passport_image: string;
passport_number: string;
passport_number: string;
phone_number: string;
}
@ -23,6 +25,7 @@ const PassengerTable: FC<TableProps> = ({ data }) => {
const { user } = useUserContext();
const router = useRouter();
const [show, setShow] = useState(true);
const { t } = useTranslation("form");
const deleteHandler = async () => {
try {
@ -33,7 +36,7 @@ const PassengerTable: FC<TableProps> = ({ data }) => {
});
setShow(false);
} catch (error) {
console.log(error);
console.error(error);
}
};
@ -43,22 +46,20 @@ const PassengerTable: FC<TableProps> = ({ data }) => {
!show && "hidden"
} sm:w-[500px] flex items-center justify-between p-4 bg-white rounded-xl shadow-sm border border-neutral-200 dark:bg-neutral-800`}
>
{/* Passenger Information */}
<div className="flex flex-col">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Passenger information
{t("passengerInfo")}
</p>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
{data.fullname}
</h3>
{data.passport_number && (
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Passport No: {data.passport_number}
{t("passportNo")}: {data.passport_number}
</p>
)}
</div>
{/* Action Icons */}
<div className="flex space-x-6 text-neutral-500">
<button className="hover:text-red-500 transition-colors" type="submit">
<IoMdTrash onClick={deleteHandler} className="text-2xl" />

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

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

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

@ -5,6 +5,7 @@ import { ChevronDownIcon } from "@heroicons/react/24/outline";
import MdOutlineLanguage from "@/images/material-symbols-light_language.svg";
import Cookies from "js-cookie"; // Import js-cookie
import Image from "next/image";
import { useTranslation } from "react-i18next";
// Language options
const languageOptions = [
@ -15,21 +16,21 @@ const languageOptions = [
icon: MdOutlineLanguage,
},
{
id: "vi",
name: "Vietnamese",
description: "Vietnam",
id: "ar",
name: "Arabic",
description: "Arabic",
icon: MdOutlineLanguage,
},
{
id: "fr",
name: "French",
description: "France",
id: "ru",
name: "Russian",
description: "Russian",
icon: MdOutlineLanguage,
},
{
id: "ar",
name: "Arabic",
description: "Arabic",
id: "id",
name: "Indonesian",
description: "Indonesian",
icon: MdOutlineLanguage,
},
];
@ -44,16 +45,16 @@ function classNames(...classes: string[]) {
}
const LangDropdown: React.FC<LangDropdownProps> = ({
panelClassName = "top-full right-0 max-w-sm w-96",
panelClassName = "top-full right-0 max-w-sm w-96 rtl:left-0 rtl:right-auto",
className = "hidden md:flex",
}) => {
const router = useRouter();
const pathname = usePathname();
const [selectedItem, setSelectedItem] = useState(languageOptions[0]);
const {t} = useTranslation("navigation");
// Update selectedItem based on the current locale in the URL
useEffect(() => {
const locales = ["en", "vi", "fr", "ar"];
const locales = ["en", "ru", "id", "ar"];
const segments = pathname.split("/").filter(Boolean);
const currentLocale = locales.includes(segments[0]) ? segments[0] : "en";
const newSelectedItem =
@ -66,13 +67,13 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
const segments = pathname.split("/").filter(Boolean);
// Remove the current locale if present
const locales = ["en", "vi", "fr", "ar"];
const locales = ["en", "ru", "id", "ar"];
if (locales.includes(segments[0])) {
segments.shift();
}
// Prepend the new locale
const newPathname = `/${locale}/${segments.join("/")}`;
const newPathname = `/${locale}/`;
// Set the locale cookie
Cookies.set("locale", locale, { expires: 365 });
@ -124,7 +125,7 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
)
}
>
Language
{t("text-language")}
</Tab>
</Tab.List>

2
src/app/[locale]/(client-components)/(Header)/LangDropdownSingle.tsx

@ -50,7 +50,7 @@ interface LangDropdownProps {
}
const LangDropdown: FC<LangDropdownProps> = ({
panelClassName = "z-10 w-screen max-w-[280px] px-4 mt-4 right-0 sm:px-0",
panelClassName = "z-10 w-screen max-w-[280px] px-4 mt-4 right-0 sm:px-0 "
}) => {
return (
<div className="LangDropdown">

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

@ -16,7 +16,7 @@ export interface MainNav1Props {
const MainNav1: FC<MainNav1Props> = ({ className = "" }) => {
return (
<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">
<div className="flex">
<Logo className="w-24 self-center" />
<Navigation />
@ -30,7 +30,7 @@ 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 className="font-thin" />
<LangDropdown className="font-thin " />
<MyTripButton />
<SwitchDarkMode />
<SearchDropdown className="flex items-center" />

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

@ -6,7 +6,8 @@ import SearchCard from "@/components/SearchCard";
import axiosInstance from "@/components/api/axios";
import SearchImage from "@/images/mynaui_search.svg"
import Image from "next/image";
import { useTranslation } from "react-i18next";
import { LuSearch } from "react-icons/lu";
interface Props {
className?: string;
}
@ -37,7 +38,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
const [value, setValue] = useState("");
const [toursDetails, setToursDetail] = useState<Tour[]>([]);
const { tours } = useToursContext();
const {t} = useTranslation("navigation");
useEffect(() => {
tours?.results?.forEach((item) => {
axiosInstance
@ -78,8 +79,8 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
return (
<>
<Popover.Button className="text-2xl md:text-[28px] w-10 h-10 rounded-full p-1 bg-bronze-secondary text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center">
<Image alt={SearchImage} src = {SearchImage} width={24} height={24} />
<Popover.Button className="text-2xl md:text-[28px] w-10 h-10 rounded-full p-1 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:outline-none flex items-center justify-center dark:bg-neutral-800">
<LuSearch className="text-neutral-700 dark:text-white" size={24} />
</Popover.Button>
<Transition
@ -94,7 +95,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 rtl:left-0 rtl:right-auto"
>
<Input
value={value}
@ -104,7 +105,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
ref={inputRef}
rounded="rounded-full"
type="text"
placeholder="Type and search"
placeholder={t("search-placeholder")}
/>
{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 dark:bg-neutral-900">
@ -121,7 +122,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
</div>
))
) : (
<h3 className="p-4 text-center">No Matches Found</h3>
<h3 className="p-4 text-center">{t("text-no-match")}</h3>
)}
</Popover.Panel>
)}

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

@ -4,6 +4,7 @@ import React, { FC, useContext } from "react";
import { Popover } from "@headlessui/react";
import { CalendarIcon } from "@heroicons/react/24/outline";
import { Context } from "@/components/contexts/tourDetails";
import { useTranslation } from "react-i18next";
export interface StayDatesRangeInputProps {
className?: string;
@ -15,7 +16,7 @@ const StayDatesRangeInput: FC<StayDatesRangeInputProps> = ({
fieldClassName = "[ nc-hero-field-padding ]",
}) => {
const { details } = useContext(Context);
const { t } = useTranslation("form");
return (
<Popover className={`StayDatesRangeInput z-10 relative flex ${className}`}>
{({ open }) => (
@ -29,11 +30,11 @@ const StayDatesRangeInput: FC<StayDatesRangeInputProps> = ({
<span
className="block xl:text-lg font-semibold whitespace-nowrap "
>
{details?.started_at.replaceAll("-", "/") || "Tour period"}
{details?.started_at.replaceAll("-", "/") || t("tourPeriod")}
{details?.ended_at && " - " + details?.ended_at.replaceAll("-", "/")}
</span>
<span className="block mt-1 text-sm text-neutral-400 leading-none font-light">
{"Start - End"}
{t("startEndDate")}
</span>
</div>
</div>

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

@ -2,12 +2,14 @@ import React, { FC } from "react";
import LocationInput from "../LocationInput";
import GuestsInput from "../GuestsInput";
import StayDatesRangeInput from "./StayDatesRangeInput";
import { useTranslation } from "react-i18next";
const StaySearchForm: FC<{}> = ({}) => {
const {t} = useTranslation("form");
const renderForm = () => {
return (
<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 desc={t("selectTour")} className="flex-[1.5]" />
<div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div>
<StayDatesRangeInput className="flex-1" />
<div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div>

35
src/app/[locale]/(client-components)/(HeroSearchForm)/GuestsInput.tsx

@ -10,6 +10,7 @@ import { PathName } from "@/routers/types";
import { UserPlusIcon } from "@heroicons/react/24/outline";
import { GuestsObject } from "../type";
import { Context, useToursContext } from "@/components/contexts/tourDetails";
import { useTranslation } from "react-i18next";
export interface GuestsInputProps {
fieldClassName?: string;
@ -27,24 +28,18 @@ const GuestsInput: FC<GuestsInputProps> = ({
const [guestAdultsInputValue, setGuestAdultsInputValue] = useState(0);
const [guestChildrenInputValue, setGuestChildrenInputValue] = useState(0);
const [guestInfantsInputValue, setGuestInfantsInputValue] = useState(0);
const { details, setPassengers } = useToursContext()
const { t } = useTranslation("form");
const { details, setPassengers } = useToursContext();
const handleChangeData = (value: number, type: keyof GuestsObject) => {
const newValue = {
guestAdults: guestAdultsInputValue,
guestChildren: guestChildrenInputValue,
guestInfants: guestInfantsInputValue,
};
if (type === "guestAdults") setGuestAdultsInputValue(value);
else if (type === "guestChildren") setGuestChildrenInputValue(value);
else if (type === "guestInfants") setGuestInfantsInputValue(value);
setPassengers({
guestAdults: newValue.guestAdults,
guestChildren: newValue.guestChildren,
guestInfants: newValue.guestInfants,
guestAdults: type === "guestAdults" ? value : guestAdultsInputValue,
guestChildren: type === "guestChildren" ? value : guestChildrenInputValue,
guestInfants: type === "guestInfants" ? value : guestInfantsInputValue,
});
};
@ -54,7 +49,7 @@ const GuestsInput: FC<GuestsInputProps> = ({
guestChildren: guestChildrenInputValue,
guestInfants: guestInfantsInputValue,
});
}, [guestAdultsInputValue, guestChildrenInputValue, guestInfantsInputValue]);
}, [guestAdultsInputValue, guestChildrenInputValue, guestInfantsInputValue, setPassengers]);
const totalGuests = guestAdultsInputValue + guestChildrenInputValue + guestInfantsInputValue;
@ -75,10 +70,10 @@ const GuestsInput: FC<GuestsInputProps> = ({
</div>
<div className="flex-grow">
<span className="block xl:text-lg font-semibold">
{totalGuests || ""} Guests
{totalGuests || ""} {t("guests")}
</span>
<span className="block mt-1 text-sm text-neutral-400 leading-none font-light">
{totalGuests ? "Guests" : "Add guests"}
{totalGuests ? t("guests") : t("addGuests")}
</span>
</div>
@ -123,8 +118,8 @@ const GuestsInput: FC<GuestsInputProps> = ({
onChange={(value) => handleChangeData(value, "guestAdults")}
max={10}
min={1}
label="Adults"
desc="Ages 13 or above"
label={t("adults")}
desc={t("adultsDesc")}
/>
<NcInputNumber
className="w-full mt-6"
@ -132,8 +127,8 @@ const GuestsInput: FC<GuestsInputProps> = ({
onChange={(value) => handleChangeData(value, "guestChildren")}
max={4}
min={0}
label="Children"
desc="Ages 2–12"
label={t("children")}
desc={t("childrenDesc")}
/>
<NcInputNumber
className="w-full mt-6"
@ -141,8 +136,8 @@ const GuestsInput: FC<GuestsInputProps> = ({
onChange={(value) => handleChangeData(value, "guestInfants")}
max={4}
min={0}
label="Infants"
desc="Ages 0–2"
label={t("infants")}
desc={t("infantsDesc")}
/>
</Popover.Panel>
</Transition>

38
src/app/[locale]/(home)/SectionDowloadApp.tsx

@ -1,5 +1,7 @@
import BackgroundSection from "@/components/BackgroundSection";
"use client";
import React from "react";
import BackgroundSection from "@/components/BackgroundSection";
import Map from "@/images/map.png";
import appSvg2 from "@/images/appSvg2.png";
import appRightImgTree from "@/images/appRightImgTree.png";
@ -8,53 +10,55 @@ import appRightImg from "@/images/Free Mockup - iPhone 16 Pro Max copy 1.png";
import btnIosPng from "@/images/btn-ios.png";
import btnAndroidPng from "@/images/btn-android.png";
import Image from "next/image";
import { useTranslation } from "react-i18next";
const SectionDowloadApp = () => {
const { t } = useTranslation("common");
return (
<div className="relative h-[455px] pb-0 pt-24 lg:py-32 xl:py-40 2xl:py-48 flex items-center">
<BackgroundSection className="bg-[#FFF8F1] dark:bg-opacity-100 ">
<BackgroundSection className="bg-[#FFF8F1] dark:bg-opacity-100">
<Image
className="object-cover object-left xl:rounded-[40px]"
src={Map}
alt="Map background"
alt={t("imageAltMapBackground")}
/>
<div className="hidden lg:block absolute right-0 bottom-0 max-w-xl xl:max-w-2xl rounded-3xl">
<Image src={appRightImg} alt="" />
<div className="hidden lg:block absolute right-0 bottom-0 max-w-xl xl:max-w-2xl rounded-3xl rtl:left-0 rtl:right-auto">
<Image src={appRightImg} alt={t("imageAltAppRightImg")} />
</div>
{/* <div className="absolute right-0 top-0 max-w-2xl">
<Image src={appRightImgTree} alt="" />
<Image src={appRightImgTree} alt={t("imageAltAppRightImgTree")} />
</div>
<div className="absolute left-0 bottom-10 max-w-2xl">
<Image src={appSvg1} alt="" />
<Image src={appSvg1} alt={t("imageAltAppSvg1")} />
</div> */}
</BackgroundSection>
<div className="relative flex flex-col ">
<h2 className="text-5xl md:text-6xl xl:text-7xl font-medium text-bronze">
Mobile Apps
<div className="relative flex flex-col">
<h2 className="text-5xl md:text-6xl xl:text-7xl font-medium text-bronze">
{t("mobileApps")}
</h2>
<span className="block mt-7 max-w-2xl text-neutral-6000">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus
porttitor nisl, sit amet finibus libero.
{t("mobileAppsDescription")}
</span>
<div className="flex space-x-3 mt-10 sm:mt-14">
<a href="##" target="_blank" rel="noopener noreferrer">
<Image src={btnIosPng} alt="" />
<Image src={btnIosPng} alt={t("buttonDownloadOnAppStore")} />
</a>
<a href="##" target="_blank" rel="noopener noreferrer">
<Image src={btnAndroidPng} alt="" />
<Image src={btnAndroidPng} alt={t("buttonGetItOnGooglePlay")} />
</a>
</div>
{/* <Image
className="hidden lg:block absolute lg:left-full lg:top-0 xl:top-1/2 z-10 lg:max-w-sm 2xl:max-w-none"
className="hidden lg:block absolute lg:left-full lg:top-0 xl:top-1/2 z-10 lg:max-w-sm 2xl:max-w-none"
src={appSvg2}
alt=""
alt={t("imageAltAppSvg2")}
/> */}
<div className="block lg:hidden mt-10 max-w-2xl rounded-3xl overflow-hidden">
{/* <Image src={appRightImg} alt="" /> */}
{/* <Image src={appRightImg} alt={t("imageAltAppRightImgMobile")} /> */}
</div>
</div>
</div>

2
src/app/[locale]/about/SectionHero.tsx

@ -30,7 +30,7 @@ const SectionHero: FC<SectionHeroProps> = ({
{!!btnText && <ButtonPrimary href="/login">{btnText}</ButtonPrimary>}
</div>
<div className="flex-grow">
<Image className="w-full" src={rightImg} alt="" />
<Image className="w-full rtl:mr-10" src={rightImg} alt="" />
</div>
</div>
</div>

2
src/app/[locale]/about/page.tsx

@ -36,7 +36,7 @@ const PageAbout: FC<PageAboutProps> = () => {
<SectionStatistic />
<SectionSubscribe2 />
{/* <SectionSubscribe2 /> */}
</div>
</div>
);

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

@ -89,7 +89,7 @@ const CommonLayout: FC<CommonLayoutProps> = ({ params }) => {
setRedirecting(true);
} catch (error) {
backHref();
console.error("Error submitting passengers:", error);
toast.error(`Error submitting passengers`);
}
};
@ -122,7 +122,7 @@ const CommonLayout: FC<CommonLayoutProps> = ({ params }) => {
return;
}
if (index <= passengers.length) {
if (index <= totalPassengers) {
// Update existing passenger
setPassengers((prevPassengers) => {
const updatedPassengers = [...prevPassengers];

109
src/app/[locale]/forgot-password/page.tsx

@ -9,12 +9,15 @@ import { useRouter } from "next/navigation";
import { useUserContext } from "@/components/contexts/userContext";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { PhoneNumberUtil, PhoneNumberFormat } from "google-libphonenumber"; // Import libphonenumber
import { PhoneNumberUtil } from "google-libphonenumber"; // Import libphonenumber
import { BiShow, BiHide } from "react-icons/bi";
import { useTranslation } from "react-i18next";
export interface PageSignUpProps {}
const PageSignUp: FC<PageSignUpProps> = () => {
// Initialize translation with the specific namespace
const { t } = useTranslation("form");
const router = useRouter();
const { setForm, setMethod } = useUserContext();
const phoneUtil = PhoneNumberUtil.getInstance();
@ -34,12 +37,14 @@ const PageSignUp: FC<PageSignUpProps> = () => {
countryCode?: string;
}>({});
// Handle country code input with maximum 3 digits
const countryCodeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= 3) {
setCountryCode(e.target.value);
}
};
// Validate phone number using libphonenumber
const validatePhoneNumber = () => {
try {
const number = phoneUtil.parseAndKeepRawInput(
@ -47,14 +52,15 @@ const PageSignUp: FC<PageSignUpProps> = () => {
countryCode
);
if (!phoneUtil.isValidNumber(number)) {
return "Invalid phone number.";
return t("invalidPhoneNumber");
}
return null;
} catch (error) {
return "Invalid phone number.";
return t("invalidPhoneNumber");
}
};
// Validate form fields
const validateForm = () => {
const newErrors: {
phoneNumber?: string;
@ -64,14 +70,15 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const phoneError = validatePhoneNumber();
if (phoneError) newErrors.phoneNumber = phoneError;
if (!password) newErrors.password = "Password is required.";
if (!password) newErrors.password = t("passwordRequired");
if (password !== confirmPassword)
newErrors.confirmPassword = "Passwords do not match.";
newErrors.confirmPassword = t("confirmPasswordMismatch");
setErrors(newErrors);
return Object.keys(newErrors).length === 0; // Return true if no errors
};
// Display error toasts whenever errors change
useEffect(() => {
Object.values(errors).forEach((error) => {
toast.error(error, {
@ -85,13 +92,15 @@ const PageSignUp: FC<PageSignUpProps> = () => {
theme: "light",
});
});
}, [errors]);
}, [errors, t]);
// Handle form submission
const submitHandler = async () => {
setLoading(true);
setErrors({}); // Clear previous errors
if (!validateForm()) {
setLoading(false);
return; // Prevent submission if there are validation errors
}
@ -100,7 +109,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
`/api/account/verfication/?range_phone=${countryCode}&phone_number=${phoneNumber}`
);
const form = {
const formData = {
verification_methodes: response.data.verification_method,
method: "reset",
countryCode,
@ -109,20 +118,21 @@ const PageSignUp: FC<PageSignUpProps> = () => {
confirmPassword,
};
setForm(form);
setForm(formData);
router.push("/signup/methodes");
} catch (error) {
} catch (error: any) {
console.error("Error fetching data:", error);
toast.error("An error occurred during verification!", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
if (error.response?.data?.errors?.length) {
error.response?.data?.errors?.map((err) => {
if (err.field) {
toast.error(`${err.field} : ${err.message}`);
} else {
toast.error(err.message);
}
});
} else {
toast.error(error.message || t("errorOccurred"));
}
} finally {
setLoading(false);
}
@ -132,20 +142,21 @@ const PageSignUp: FC<PageSignUpProps> = () => {
<div className={`nc-PageSignUp`}>
<div className="container mb-24 lg:mb-32">
<h2 className="my-20 flex items-center text-3xl leading-[115%] md:text-5xl md:leading-[115%] font-semibold text-neutral-900 dark:text-neutral-100 justify-center">
Change Password
{t("changePassword")}
</h2>
<div className="max-w-md mx-auto space-y-6">
<form
className="grid grid-cols-1 gap-6"
onSubmit={(e) => e.preventDefault()}
>
{/* Phone Number Field */}
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">
Phone Number
{t("phoneNumber")}
</span>
<div
className={`flex items-center mt-1 rounded-2xl ${
errors.countryCode || errors.phoneNumber
errors.phoneNumber || errors.countryCode
? "border border-red-600"
: "border border-neutral-200"
} bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}
@ -157,9 +168,10 @@ const PageSignUp: FC<PageSignUpProps> = () => {
value={countryCode}
onChange={countryCodeHandler}
type="number"
placeholder="000"
placeholder={t("enterPhoneNumber")}
maxLength={3}
className="w-[50px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none no-border-on-focus p-2 mr-[-10px] shadow-none text-center border-none outline-none bg-transparent text-neutral-800 dark:text-neutral-200"
aria-label={t("phoneNumber")}
/>
<span className="px-2 text-neutral-500">|</span>
<input
@ -167,65 +179,88 @@ const PageSignUp: FC<PageSignUpProps> = () => {
onChange={(e) => setPhoneNumber(e.target.value)}
type="number"
className="[appearance:textfield] rounded-full [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 p-2 no-border-on-focus border-none outline-none bg-transparent text-neutral-800 dark:text-neutral-200"
placeholder={t("enterPhoneNumber")}
aria-label={t("phoneNumber")}
/>
</div>
</label>
{/* Password Field */}
<label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
Password
{t("password")}
</span>
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type={`${showPassword ? "text" : "password"}`}
className={`mt-1 ${errors.password ? "border-red-600" : ""}`}
placeholder={t("enterPassword")}
aria-label={t("password")}
/>
{showPassword ? (
<BiShow
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
onClick={(e) => setShowPassword((prev) => !prev)}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("showPassword")}
/>
) : (
<BiHide
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
onClick={(e) => setShowPassword((prev) => !prev)}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("hidePassword")}
/>
)}
</label>
{/* Confirm Password Field */}
<label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
Confirm Password
{t("confirmPassword")}
</span>
<Input
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
type={`${showConfirm ? "text" : "password"}`}
className={`mt-1 ${
errors.confirmPassword ? "border-red-600" : ""
}`}
className={`mt-1 ${errors.confirmPassword ? "border-red-600" : ""}`}
placeholder={t("enterConfirmPassword")}
aria-label={t("confirmPassword")}
/>
{showConfirm ? (
<BiShow
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
onClick={(e) => setShowConfirm((prev) => !prev)}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowConfirm((prev) => !prev)}
aria-label={t("showConfirmPassword")}
/>
) : (
<BiHide
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
onClick={(e) => setShowConfirm((prev) => !prev)}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowConfirm((prev) => !prev)}
aria-label={t("hideConfirmPassword")}
/>
)}
</label>
{/* Continue Button */}
<ButtonPrimary loading={loading} onClick={submitHandler}>
Continue
{t("submit")}
</ButtonPrimary>
</form>
{/* Link to Sign In */}
<p className="text-center text-sm text-neutral-500 mt-4">
{t("alreadyHaveAccount")}{" "}
<Link href="/signin" className="text-primary-600 hover:underline">
{t("signIn")}
</Link>
</p>
</div>
</div>
<ToastContainer />
</div>
);
};

78
src/app/[locale]/login/page.tsx

@ -1,6 +1,6 @@
"use client";
import React, { FC, useState, useEffect } from "react";
import React, { FC, useEffect, useState } from "react";
import Input from "@/shared/Input";
import ButtonPrimary from "@/shared/ButtonPrimary";
import Link from "next/link";
@ -11,10 +11,12 @@ import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { PhoneNumberUtil } from "google-libphonenumber";
import { BiShow, BiHide } from "react-icons/bi";
import { useTranslation } from "react-i18next";
export interface PageLoginProps {}
const PageLogin: FC<PageLoginProps> = () => {
const { t } = useTranslation("form");
const { user, setUser } = useUserContext();
const router = useRouter();
const phoneUtil = PhoneNumberUtil.getInstance();
@ -58,11 +60,11 @@ const PageLogin: FC<PageLoginProps> = () => {
try {
const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + countryCode + phoneNumber, countryCode);
if (!phoneUtil.isValidNumber(parsedNumber)) {
return "Invalid phone number.";
return t("invalidPhoneNumber");
}
return null;
} catch (error) {
return "Invalid phone number format.";
return t("invalidPhoneNumberFormat");
}
};
@ -75,7 +77,7 @@ const PageLogin: FC<PageLoginProps> = () => {
}
if (!password) {
newErrors.password = "Password is required.";
newErrors.password = t("passwordRequired");
}
setErrors(newErrors);
@ -98,44 +100,23 @@ const PageLogin: FC<PageLoginProps> = () => {
});
if (response.status === 201) {
toast.success("Login successful!");
toast.success(t("loginSuccessful"));
setUser(response.data);
router.back(); // Redirect to home or any other page after successful login
} else {
toast.error("Login failed, please check your credentials.", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
toast.error(t("loginFailed"));
}
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
} catch (error: any) {
if (error.response?.data?.errors?.length) {
error.response?.data?.errors?.map((err) => {
if (err.field) {
toast.error(`${err.field} : ${err.message}`);
} else {
toast.error(err.message);
}
});
} else {
toast.error("An unknown error occurred.", {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
toast.error(error.message || t("errorOccurred"));
}
} finally {
setLoading(false);
@ -146,23 +127,23 @@ const PageLogin: FC<PageLoginProps> = () => {
<div className="nc-PageLogin">
<div className="container mb-24 lg:mb-32">
<h2 className="my-20 flex items-center text-3xl leading-[115%] md:text-5xl md:leading-[115%] font-semibold text-neutral-900 dark:text-neutral-100 justify-center">
Login
{t("login")}
</h2>
<div className="max-w-md mx-auto space-y-6">
<form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}>
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">Phone Number</span>
<span className="text-neutral-800 dark:text-neutral-200">{t("phoneNumber")}</span>
<div
className={`flex items-center mt-1 rounded-2xl ${
errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"
} bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}
} bg-white dark:border-neutral-700 dark:bg-neutral-900 focus-within:border-primary-300 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50`}
>
<span className="px-2 mr-[-15px] text-neutral-800 dark:text-neutral-200">+</span>
<input
value={countryCode}
onChange={countryCodeHandler}
type="number"
placeholder="000"
placeholder={t("phoneNumberCountryCodePlaceholder")}
maxLength={3}
className="w-[50px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none no-border-on-focus p-2 mr-[-10px] shadow-none text-center border-none outline-none bg-transparent text-neutral-800 dark:text-neutral-200"
/>
@ -171,6 +152,7 @@ const PageLogin: FC<PageLoginProps> = () => {
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
type="number"
placeholder={t("enterPhoneNumber")}
className="[appearance:textfield] rounded-full [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 p-2 no-border-on-focus border-none outline-none bg-transparent text-neutral-800 dark:text-neutral-200"
/>
</div>
@ -178,9 +160,9 @@ const PageLogin: FC<PageLoginProps> = () => {
<label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
Password
{t("password")}
<Link href="/forgot-password" className="text-sm underline font-medium">
Forgot password?
{t("forgotPassword")}
</Link>
</span>
<Input
@ -188,34 +170,38 @@ const PageLogin: FC<PageLoginProps> = () => {
onChange={(e) => setPassword(e.target.value)}
type={`${showPassword ? "text" : "password"}`}
className={`mt-1 ${errors.password ? "border-red-600" : "border-neutral-300"}`}
placeholder={t("enterPassword")}
/>
{showPassword ? (
<BiShow
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("showPassword")}
/>
) : (
<BiHide
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("hidePassword")}
/>
)}
</label>
<ButtonPrimary onClick={submitHandler} loading={loading}>
Continue
{t("continue")}
</ButtonPrimary>
</form>
<span className="block text-center text-neutral-700 dark:text-neutral-300">
<Link href="/signup" className="text-bronze font-semibold">
Create an account
{t("createAccount")}
</Link>
</span>
</div>
</div>
<ToastContainer />
</div>
);
};

49
src/app/[locale]/signup/methodes/page.tsx

@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState, ChangeEvent } from "react";
import { FaWhatsapp } from "react-icons/fa";
import { MdOutlineTextsms } from "react-icons/md";
import { useTranslation } from "react-i18next";
interface Form {
phoneNumber: string;
@ -19,32 +20,33 @@ interface Form {
}
interface EnabledMethods {
watsapp: boolean;
whatsapp: boolean; // Corrected spelling from 'watsapp' to 'whatsapp'
sms: boolean;
}
function SelectMethods() {
const { t } = useTranslation("form");
const router = useRouter();
const {form, setForm } = useUserContext()
const { form, setForm } = useUserContext();
const [selectedMethod, setSelectedMethod] = useState<string>("");
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [enabled, setEnabled] = useState<EnabledMethods>({ watsapp: false, sms: false });
const [enabled, setEnabled] = useState<EnabledMethods>({ whatsapp: false, sms: false });
// Initialize enabled methods based on the form
useEffect(() => {
setEnabled({
watsapp: form?.verification_methodes?.includes("watsapp") ?? false,
whatsapp: form?.verification_methodes?.includes("whatsapp") ?? false,
sms: form?.verification_methodes?.includes("sms") ?? false,
});
}, [form]);
// Handle method change
const handleMethodChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!enabled.watsapp && !enabled.sms) {
if (!enabled.whatsapp && !enabled.sms) {
setSelectedMethod("");
} else if (!enabled.watsapp) {
} else if (!enabled.whatsapp) {
setSelectedMethod("sms");
} else if (!enabled.sms) {
setSelectedMethod("whatsapp");
@ -74,9 +76,9 @@ function SelectMethods() {
});
if (response.status === 202) {
setForm((prev : Record<string, any>) => ({ ...prev, verification_method: selectedMethod }));
setForm((prev: Record<string, any>) => ({ ...prev, verification_method: selectedMethod }));
setLoading(false);
router.replace("signup/otp-code");
router.replace("/signup/otp-code");
}
}
// Handle Password Reset
@ -89,13 +91,23 @@ function SelectMethods() {
});
if (response.status === 202) {
setForm((prev : Record<string, any>) => ({ ...prev, verification_method: selectedMethod }));
setForm((prev: Record<string, any>) => ({ ...prev, verification_method: selectedMethod }));
setLoading(false);
router.replace("signup/otp-code");
router.replace("/signup/otp-code");
}
}
} catch (error: any) {
setError(error.response?.data?.detail || "An error occurred.");
setError(error.response?.data?.detail || t("errorOccurred"));
toast.error(error.response?.data?.detail || t("errorOccurred"), {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
setLoading(false);
}
};
@ -103,14 +115,14 @@ function SelectMethods() {
return (
<div className="w-[550px] container mb-24 lg:mb-32 p-4 space-y-4">
<h2 className="my-20 flex items-center text-3xl leading-[115%] md:text-5xl md:leading-[115%] font-semibold text-neutral-900 dark:text-neutral-100 justify-center">
Verification Method
{t("verificationMethod")}
</h2>
<form className="grid grid-cols-1 gap-6">
{/* WhatsApp Option */}
<div
className={`${
!enabled.watsapp ? "opacity-40" : ""
!enabled.whatsapp ? "opacity-40" : ""
} h-15 flex items-center justify-between p-3 border rounded-xl shadow-sm bg-white dark:bg-neutral-800`}
>
<div className="flex items-center">
@ -118,10 +130,10 @@ function SelectMethods() {
<label
htmlFor="whatsapp"
className={`${
!enabled.watsapp ? "cursor-not-allowed" : "cursor-pointer"
!enabled.whatsapp ? "cursor-not-allowed" : "cursor-pointer"
} text-neutral-800 dark:text-neutral-200 font-medium`}
>
Send via WhatsApp
{t("sendViaWhatsApp")}
</label>
</div>
<input
@ -131,7 +143,7 @@ function SelectMethods() {
value="whatsapp"
checked={selectedMethod === "whatsapp"}
onChange={handleMethodChange}
disabled={!enabled.watsapp}
disabled={!enabled.whatsapp}
className="cursor-pointer form-radio accent-black text-black focus:ring-primary-500 focus:ring-2"
/>
</div>
@ -150,7 +162,7 @@ function SelectMethods() {
!enabled.sms ? "cursor-not-allowed" : "cursor-pointer"
} text-neutral-800 dark:text-neutral-200 font-medium`}
>
Send via SMS
{t("sendViaSMS")}
</label>
</div>
<input
@ -175,8 +187,9 @@ function SelectMethods() {
e.preventDefault();
handleSubmit();
}}
disabled={loading || !selectedMethod}
>
Continue
{t("continue")}
</ButtonPrimary>
</form>
</div>

112
src/app/[locale]/signup/otp-code/page.tsx

@ -1,24 +1,22 @@
"use client";
import React, { FC, useContext, useState, useEffect, useRef } from "react";
import React, { FC, useState, useEffect, useRef } from "react";
import ButtonPrimary from "@/shared/ButtonPrimary";
import axiosInstance from "@/components/api/axios";
import { useUserContext } from "@/components/contexts/userContext";
import { useRouter } from "next/navigation";
import { toast } from "react-toastify";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { useTranslation } from "react-i18next";
export interface PageSignUpProps {}
const PageSignUp: FC<PageSignUpProps> = () => {
// Initialize translation with the specific namespace
const { t } = useTranslation("form");
const router = useRouter();
const { form, user, setUser } = useUserContext();
useEffect(() => {
if (Object.keys(user).length) {
router.back();
}
}, [user, router]);
const [otp, setOtp] = useState(["", "", "", "", ""]);
const [time, setTime] = useState(30);
const [error, setError] = useState<string>("");
@ -26,6 +24,14 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const [loading, setLoading] = useState(false);
// Redirect to previous page if the user is already logged in
useEffect(() => {
if (Object.keys(user).length) {
router.back();
}
}, [user, router]);
// Handle OTP input change
const handleOtpChange = (value: string, index: number) => {
if (/^[0-9]?$/.test(value)) {
const newOtp = [...otp];
@ -37,6 +43,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
}
};
// Handle backspace navigation in OTP inputs
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
index: number
@ -46,6 +53,7 @@ const PageSignUp: FC<PageSignUpProps> = () => {
}
};
// Countdown timer for resend button
useEffect(() => {
if (time > 0) {
const timer = setInterval(
@ -55,25 +63,27 @@ const PageSignUp: FC<PageSignUpProps> = () => {
return () => clearInterval(timer);
}
}, [time]);
console.log(form);
// Handle resend OTP
const handleResend = async () => {
if (time === 0) {
setTime(30);
setLoading(true);
try {
const payload : Record<string, any> = {
phone_number: form.phoneNumber,
const payload: Record<string, any> = {
phone_number: form.phone_number,
verification_method: form.verification_method,
range_phone: form.countryCode,
range_phone: form.range_phone,
};
if (form.method === "register") {
payload.fullname = form.name;
payload.fullname = form.fullname;
payload.password = form.password;
payload.password_confirmation = form.confirmPassword;
payload.password_confirmation = form.password_confirmation;
const response = await axiosInstance.post(
`/api/account/reister/`,
`/api/account/register/`,
payload,
{
headers: { Accept: "application/json" },
@ -81,15 +91,19 @@ const PageSignUp: FC<PageSignUpProps> = () => {
);
if (response.status === 202) {
setForm((prev: Record<string, any>) => ({
...prev,
verification_method: form.verification_method,
}));
setLoading(false);
router.replace("signup/otp-code");
router.replace("/signup/otp-code");
}
} else if (form.method === "reset") {
payload.password = form.password;
payload.password_confirmation = form.confirmPassword;
payload.password_confirmation = form.password_confirmation;
const response = await axiosInstance.post(
`/api/account/recoer/`,
`/api/account/recover/`,
payload,
{
headers: { Accept: "application/json" },
@ -98,35 +112,63 @@ const PageSignUp: FC<PageSignUpProps> = () => {
if (response.status === 202) {
setLoading(false);
router.replace("/signup/otp-code");
}
}
} catch (error : any) {
setError(error.message);
console.log(error);
} catch (error: any) {
setError(error.response?.data?.detail || t("errorOccurred"));
toast.error(error.response?.data?.detail || t("errorOccurred"), {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
setLoading(false);
}
}
};
console.log(form);
console.log(form);
// Handle OTP submission
const submitHandler = async () => {
console.log(otp);
if (otp.some((digit) => digit === "")) {
toast.error(t("otpNotComplete"));
return; // Exit the function if OTP is incomplete
}
setError("");
try {
const response = await axiosInstance.post("/api/account/verify/", {
method: form.method,
phone_number: form.phoneNumber,
phone_number: "+" + form.range_phone + form.phone_number,
code: otp.join(""),
});
if (response.status === 201) {
setUser(response.data);
toast.success("Your Sign In was successful");
toast.success(t("signInSuccessful"));
router.back();
} else {
toast.error("Something went wrong. Please try again.");
toast.error(t("somethingWentWrong"));
}
} catch (error: any) {
// Cast to 'any' or a specific error type
toast.error(error.response?.data?.message || "An error occurred.");
if (error.response?.data?.errors?.length) {
error.response?.data?.errors?.map((err) => {
if (err.field) {
toast.error(`${err.field} : ${err.message}`);
} else {
toast.error(err.message);
}
});
} else {
toast.error(error.message || t("errorOccurred"));
}
}
};
@ -134,11 +176,10 @@ console.log(form);
<div className={`nc-PageSignUp`}>
<div className="container mb-24 lg:mb-32">
<h2 className="my-10 text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
Verification Code
{t("verificationCode")}
</h2>
<p className="text-center text-sm text-neutral-500 mb-4">
Enter the 5-digit code that we sent to complete your account
registration
{t("enterOtpDescription")}
</p>
<div className="max-w-sm mx-auto space-y-6">
<div className="flex justify-center space-x-2 mb-4">
@ -152,11 +193,12 @@ console.log(form);
onChange={(e) => handleOtpChange(e.target.value, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
className="w-12 h-12 border rounded-lg text-center text-lg font-semibold border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-2 focus:ring-primary-500"
aria-label={t("otpInput", { index: index + 1 })}
/>
))}
</div>
<p className="text-center text-sm text-neutral-500 mb-4">
Haven't got the confirmation code yet?{" "}
{t("haventGotCode")}{" "}
<button
className={`text-primary-600 hover:underline ${
time > 0 ? "cursor-not-allowed opacity-50" : "cursor-pointer"
@ -164,10 +206,12 @@ console.log(form);
onClick={handleResend}
disabled={time > 0}
>
Resend
{t("resend")}
</button>
{time > 0 && (
<span className="text-xs text-neutral-400">({time} Seconds)</span>
<span className="text-xs text-neutral-400">
{`(${t("seconds")} ${time} )`}
</span>
)}
</p>
{error && <p className="text-red-500 text-xs">{error}</p>}
@ -177,11 +221,13 @@ console.log(form);
e.preventDefault();
submitHandler();
}}
disabled={loading}
>
Confirm
{t("confirm")}
</ButtonPrimary>
</div>
</div>
<ToastContainer />
</div>
);
};

106
src/app/[locale]/signup/page.tsx

@ -11,10 +11,12 @@ import { useRouter } from "next/navigation";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { BiShow, BiHide } from "react-icons/bi";
import { useTranslation } from "react-i18next";
export interface PageSignUpProps {}
const PageSignUp: FC<PageSignUpProps> = () => {
const { t } = useTranslation("form");
const router = useRouter();
const { user, setForm } = useUserContext();
const [name, setName] = useState<string>("");
@ -35,12 +37,14 @@ const PageSignUp: FC<PageSignUpProps> = () => {
}
};
// Redirect to home if the user is already logged in
useEffect(() => {
if (Object.keys(user).length) {
router.replace("/");
}
}, [user, router]);
// Display validation errors as toast notifications
useEffect(() => {
Object.values(errors).forEach((error) => {
toast.error(error, {
@ -58,12 +62,12 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const submitHandler = async () => {
const form = {
name,
countryCode,
phoneNumber,
password,
confirmPassword,
verification_methodes: "",
fullname : name,
range_phone : countryCode,
phone_number : phoneNumber,
password : password,
password_confirmation : confirmPassword,
verification_method: "whatsapp",
method: "register",
};
@ -71,14 +75,36 @@ const PageSignUp: FC<PageSignUpProps> = () => {
setLoading(true);
setForm(form);
try {
const response = await axiosInstance.get(
`/api/account/verfication/?range_phone=${countryCode}&phone_number=${phoneNumber}`
const response = await axiosInstance.post(
`/api/account/register/` , {
fullname : name,
range_phone : countryCode,
phone_number : "+" + countryCode + phoneNumber,
password : password,
password_confirmation : confirmPassword,
verification_method: "whatsapp",
method: "register",
}
);
form.verification_methodes = response.data.verification_method;
router.replace("/signup/methodes");
router.replace("/signup/otp-code");
} catch (error: any) {
console.error("Error fetching data:", error);
setFailed(error.message || "An error occurred.");
if (error.response?.data?.errors?.length) {
error.response?.data?.errors?.map((err)=>{
if (err.field) {
toast.error(
`${err.field} : ${err.message}`
);
}else{
toast.error(
err.message
);
}
})
}
else{
toast.error(error.message || t("errorOccurred"))
}
} finally {
setLoading(false);
}
@ -89,22 +115,25 @@ const PageSignUp: FC<PageSignUpProps> = () => {
<div className={`nc-PageSignUp`}>
<div className="container mb-24 lg:mb-32">
<h2 className="my-20 flex items-center text-3xl leading-[115%] md:text-5xl md:leading-[115%] font-semibold text-neutral-900 dark:text-neutral-100 justify-center">
Signup
{t("signup")}
</h2>
<div className="max-w-md mx-auto space-y-6">
<form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}>
{/* Full Name Field */}
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">Full Name</span>
<span className="text-neutral-800 dark:text-neutral-200">{t("fullName")}</span>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
type="text"
placeholder="Full Name"
placeholder={t("enterFullName")}
className={`mt-1 ${errors.name ? "border-red-600" : "border-neutral-300"}`}
/>
</label>
{/* Phone Number Field */}
<label className="block">
<span className="text-neutral-800 dark:text-neutral-200">Phone Number</span>
<span className="text-neutral-800 dark:text-neutral-200">{t("phoneNumber")}</span>
<div
className={`flex items-center mt-1 rounded-2xl ${
errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"
@ -124,67 +153,92 @@ const PageSignUp: FC<PageSignUpProps> = () => {
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
type="number"
placeholder={t("enterPhoneNumber")}
className="[appearance:textfield] rounded-full [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none flex-1 p-2 no-border-on-focus border-none outline-none bg-transparent text-neutral-800 dark:text-neutral-200"
/>
</div>
</label>
{/* Password Field */}
<label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">Password</span>
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
{t("password")}
{/* <Link href="/forgot-password" className="text-sm underline font-medium">
{t("forgotPassword")}
</Link> */}
</span>
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPassword ? "text" : "password"}
placeholder={t("enterPassword")}
className={`mt-1 ${errors.password ? "border-red-600" : "border-neutral-300"}`}
/>
{showPassword ? (
<BiShow
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("hidePassword")}
/>
) : (
<BiHide
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("showPassword")}
/>
)}
</label>
{/* Confirm Password Field */}
<label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">Confirm Password</span>
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
{t("confirmPassword")}
</span>
<Input
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
type={showConfirmPassword ? "text" : "password"}
className={` mt-1 ${errors.confirmPassword ? "border-red-600" : "border-neutral-300"}`}
placeholder={t("enterConfirmPassword")}
className={`mt-1 ${errors.confirmPassword ? "border-red-600" : "border-neutral-300"}`}
/>
{showConfirmPassword ? (
<BiShow
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowConfirmPassword((prev) => !prev)}
aria-label={t("hideConfirmPassword")}
/>
) : (
<BiHide
size={20}
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4"
className="box-content rounded-full hover:bg-slate-100 p-[5px] absolute top-9 right-4 cursor-pointer"
onClick={() => setShowConfirmPassword((prev) => !prev)}
aria-label={t("showConfirmPassword")}
/>
)}
</label>
{/* Display Failed Message */}
{failed && <p className="text-xs text-red-600">{failed}</p>}
{/* Submit Button */}
<ButtonPrimary loading={loading} onClick={submitHandler} disabled={loading}>
Continue
{t("continue")}
</ButtonPrimary>
</form>
{/* Signup Link */}
<span className="not-italic block text-center text-neutral-700 dark:text-neutral-300">
Already have an account?{" "}
{t("alreadyHaveAccount")}{" "}
<Link href="/login" className="text-primary-600 font-semibold underline">
Sign in
{t("signIn")}
</Link>
</span>
</div>
</div>
<ToastContainer />
</div>
);
};

74
src/app/[locale]/tours/SectionGridFilterCard.tsx

@ -1,14 +1,17 @@
"use client";
import React, { FC, useContext, useEffect, useState } from "react";
import { DEMO_STAY_LISTINGS } from "@/data/listings";
import React, { FC, useEffect, useState } from "react";
import convertNumbThousand from "@/utils/convertNumbThousand";
import Link from "next/link";
import Image from "next/image";
import { useToursContext } from "@/components/contexts/tourDetails";
import { useSearchParams } from "next/navigation";
import Heading2 from "@/shared/Heading2"; // Assuming Heading2 is used elsewhere
import StayCard2 from "./Card";
import { useTranslation } from "react-i18next";
import { StayDataType } from "@/data/types";
import { DEMO_STAY_LISTINGS } from "@/data/listings";
import TabFilters from "./TabFilters";
import Heading2 from "@/shared/Heading2";
import StayCard2 from "./Card";
import { useToursContext } from "@/components/contexts/tourDetails";
import { useParams, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
export interface SectionGridFilterCardProps {
className?: string;
@ -19,27 +22,29 @@ const SectionGridFilterCard: FC<SectionGridFilterCardProps> = ({
className = "",
data = DEMO_STAY_LISTINGS,
}) => {
const { t } = useTranslation("common");
const { countries, tours } = useToursContext();
const [countryTours, setCountryTours] = useState(tours.results || []);
const [checked, setChecked] = useState<{ [key: string]: boolean }>({});
const searchParams = useSearchParams()
console.log(searchParams);
const searchParams = useSearchParams();
console.log(searchParams);
// Get the list of selected countries
const filteredCountries = Object.keys(checked).filter((countryName) => checked[countryName]);
const filteredCountries = Object.keys(checked).filter(
(countryName) => checked[countryName]
);
useEffect(()=>{
const country = searchParams.get("country")
if (searchParams.has("country")){
useEffect(() => {
const country = searchParams.get("country");
if (searchParams.has("country") && country) {
setChecked({
[country] : true
})
[country]: true,
});
}
} , [searchParams])
console.log(checked);
}, [searchParams]);
console.log(checked);
useEffect(() => {
if (!tours.results) return;
@ -60,20 +65,23 @@ console.log(checked);
setCountryTours(filteredTours);
}
}, [checked, countries, tours.results]);
}, [checked, countries, tours.results, filteredCountries]);
return (
<div className={`nc-SectionGridFilterCard container ${className}`} data-nc-id="SectionGridFilterCard">
<div className={`mb-12 lg:mb-16 ${className}`}>
<h2 className="text-4xl font-semibold">{"All Tours"}</h2>
{/* <span className="block text-neutral-500 dark:text-neutral-400 mt-3">
233 stays
<span className="mx-2">·</span>
Aug 12 - 18
<span className="mx-2">·</span>2 Guests
</span> */}
</div>
<div
className={`nc-SectionGridFilterCard container ${className}`}
data-nc-id="SectionGridFilterCard"
>
<div className={`mb-12 lg:mb-16`}>
<h2 className="text-4xl font-semibold">
{t("sectionGridFilterCard.allTours")}
</h2>
{/* Uncomment and internationalize if needed
<span className="block text-neutral-500 dark:text-neutral-400 mt-3">
{t("sectionGridFilterCard.toursInfo", { count: 233, dateRange: "Aug 12 - 18", guests: 2 })}
</span>
*/}
</div>
<div className="mb-8 lg:mb-11">
<TabFilters onChangeCountry={setChecked} data={countries} />
@ -82,11 +90,11 @@ console.log(checked);
{countryTours.length > 0 ? (
countryTours.map((stay) => <StayCard2 key={stay.id} data={stay} />)
) : (
<h2>No tours Available</h2>
<h2>{t("sectionGridFilterCard.noToursAvailable")}</h2>
)}
</div>
</div>
);
};
export default SectionGridFilterCard;
export default SectionGridFilterCard;

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

@ -90,9 +90,9 @@ const ListingStayDetailPage: FC = () => {
{itineraries.map((item, index) => (
<div key={item.id} className="flex">
<div className="mt-2">
<div className="dark:bg-black mx-4 transform -translate-x-1/2 w-8 h-8 bg-white border-secondery-bronze border-[3px] rounded-full"></div>
<div className="dark:bg-black mx-4 transform -translate-x-1/2 w-8 h-8 bg-white border-secondery-bronze border-[3px] rounded-full rtl:translate-x-1/2"></div>
{itineraries.length !== index + 1 && (
<div className="mx-4 mt-[-2px] transform -translate-x-1/2 w-[1px] h-full border-secondery-bronze border-[2px]"></div>
<div className="mx-4 mt-[-2px] transform -translate-x-1/2 w-[1px] h-full border-secondery-bronze border-[2px] rtl:translate-x-1/2"></div>
)}
</div>
<div className="p-5 rounded-3xl mb-4 border-2">
@ -131,13 +131,17 @@ const ListingStayDetailPage: FC = () => {
const renderSidebar = () => {
const total =
details?.final_price && passengers
? ((passengers.guestAdults * Number(details.price)) + (passengers.guestChildren * Number(details.price_child)) + (passengers.guestInfants * Number(details?.price_infant))).toLocaleString("en-US", {
? (
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);
console.log(details);
return (
<div className="listingSectionSidebar__wrap shadow-xl">
{/* Price display */}
@ -153,8 +157,13 @@ const ListingStayDetailPage: FC = () => {
</div>
)}
{/* <StartRating /> */}
{<Badge className="!text-base" name={details?.status} color={details?.status} />}
{
<Badge
className="!text-base"
name={details?.status}
color={details?.status}
/>
}
</div>
{/* Booking Form */}
@ -173,7 +182,7 @@ const ListingStayDetailPage: FC = () => {
{/* Reserve Button */}
<ButtonPrimary
className={
details?.status === "AVAILABLE"
details?.status === "AVAILABLE" && details.is_access && totalGuests
? ""
: "opacity-60 pointer-events-none"
}
@ -213,7 +222,7 @@ const ListingStayDetailPage: FC = () => {
<header className="rounded-md sm:rounded-xl">
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2">
{/* Main Image */}
<div className="col-span-2 row-span-3 sm:row-span-2 relative rounded-md sm:rounded-xl overflow-hidden cursor-pointer">
<div className="col-span-2 min-h-[25vh] lg:min-h-[513px] row-span-3 sm:row-span-2 relative rounded-md sm:rounded-xl overflow-hidden cursor-pointer">
{details && (
<Image
fill
@ -265,7 +274,7 @@ const ListingStayDetailPage: FC = () => {
{/* Main content section */}
<main className="relative z-10 mt-11 flex flex-col lg:flex-row">
{/* Left Content */}
<div className="w-full lg:w-3/5 xl:w-2/3 space-y-8 lg:space-y-10 lg:pr-10">
<div className="w-full lg:w-3/5 xl:w-2/3 space-y-8 lg:space-y-10 lg:pr-10 lg:rtl:pl-10">
{renderSectionDetails()}
{renderTourFeatures()}
{renderItinerarySection()}

13
src/app/globals.css

@ -13,9 +13,14 @@
}
.hero-image{
left: -450px;
left: -550px;
}
@media(max-width : 1600px) {
.hero-image{
left: -350px;
}
}
@media(max-width : 1400px) {
.hero-image{
left: -400px;
@ -26,4 +31,8 @@
left: -300px;
top: 0px;
}
}
}
.container{
max-width: 1440px;
}

2
src/components/BackgroundSection.tsx

@ -11,7 +11,7 @@ const BackgroundSection: FC<BackgroundSectionProps> = ({
}) => {
return (
<div
className={`nc-BackgroundSection absolute inset-y-0 w-screen xl:max-w-[1340px] 2xl:max-w-screen-2xl left-1/2 transform -translate-x-1/2 xl:rounded-[40px] z-0 ${className}`}
className={`nc-BackgroundSection absolute inset-y-0 w-screen xl:max-w-[1340px] 2xl:max-w-screen-2xl left-1/2 transform -translate-x-1/2 xl:rounded-[40px] z-0 ${className}`}
data-nc-id="BackgroundSection"
>
{children}

21
src/components/CardCategory3.tsx

@ -1,19 +1,11 @@
"use client";
import React, { FC, useEffect, useState } from "react";
import convertNumbThousand from "@/utils/convertNumbThousand";
import { TaxonomyType } from "@/data/types";
import Link from "next/link";
import Image from "next/image";
// Define the TaxonomyType, CountryType, and TourType interfaces
interface TaxonomyType {
count: number;
name: string;
href?: string;
thumbnail?: string;
city: { thumbnail: string }[]; // Assuming 'city' is an array of objects with 'thumbnail'
}
import { useTranslation } from "react-i18next";
import convertNumbThousand from "@/utils/convertNumbThousand";
interface CountryType {
name: string;
@ -37,6 +29,7 @@ const CardCategory3: FC<CardCategory3Props> = ({
countries,
tours,
}) => {
const { t } = useTranslation("common");
const { count, name, href = "/", thumbnail } = taxonomy;
// Set the state with proper typing for country tours
@ -65,12 +58,12 @@ const CardCategory3: FC<CardCategory3Props> = ({
<Image
src={taxonomy.city[0].thumbnail}
className="object-cover w-full h-full rounded-2xl"
alt="places"
alt={t("cardCategory3.imageAltPlaces")}
fill
sizes="(max-width: 400px) 100vw, 300px"
/>
) : (
<div className="object-cover w-full h-full rounded-2xl bg-gray-300" />
<div className="object-cover w-full h-full rounded-2xl bg-gray-300" aria-hidden="true" />
)}
<span className="opacity-0 group-hover:opacity-100 absolute inset-0 bg-black bg-opacity-10 transition-opacity"></span>
</div>
@ -83,7 +76,7 @@ const CardCategory3: FC<CardCategory3Props> = ({
<span
className={`block mt-1.5 text-sm text-neutral-6000 dark:text-neutral-400`}
>
{convertNumbThousand(countryTours.length || 0)} Tours
{convertNumbThousand(countryTours.length || 0)} {t("cardCategory3.tours")}
</span>
</div>
</Link>

96
src/components/Footer.tsx

@ -1,10 +1,12 @@
"use client";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import Logo from "@/shared/Logo";
import SocialsList1 from "@/shared/SocialsList1";
import { CustomLink } from "@/data/types";
import React from "react";
import FooterNav from "./FooterNav";
import Image from "next/image";
export interface WidgetFooterMenu {
id: string;
@ -12,65 +14,32 @@ export interface WidgetFooterMenu {
menus: CustomLink[];
}
const widgetMenus: WidgetFooterMenu[] = [
{
id: "5",
title: "Getting started",
menus: [
{ href: "#", label: "Installation" },
{ href: "#", label: "Release Notes" },
{ href: "#", label: "Upgrade Guide" },
{ href: "#", label: "Browser Support" },
{ href: "#", label: "Editor Support" },
],
},
{
id: "1",
title: "Explore",
menus: [
{ href: "#", label: "Design features" },
{ href: "#", label: "Prototyping" },
{ href: "#", label: "Design systems" },
{ href: "#", label: "Pricing" },
{ href: "#", label: "Security" },
],
},
{
id: "2",
title: "Resources",
menus: [
{ href: "#", label: "Best practices" },
{ href: "#", label: "Support" },
{ href: "#", label: "Developers" },
{ href: "#", label: "Learn design" },
{ href: "#", label: "Releases" },
],
},
{
id: "4",
title: "Community",
menus: [
{ href: "#", label: "Discussion Forums" },
{ href: "#", label: "Code of Conduct" },
{ href: "#", label: "Community Resources" },
{ href: "#", label: "Contributing" },
{ href: "#", label: "Concurrent Mode" },
],
},
];
const Footer: React.FC = () => {
const { t } = useTranslation("footer");
const widgetMenus: WidgetFooterMenu[] = [
{
id: "5",
title: "Quick Links",
menus: [
{ href: "tours", label: t("widgetMenus.Quick Links.menus.0.label") },
{ href: "blog", label: t("widgetMenus.Quick Links.menus.1.label") },
{ href: "faq", label: t("widgetMenus.Quick Links.menus.2.label") },
{ href: "about", label: t("widgetMenus.Quick Links.menus.3.label") },
],
}
];
const renderWidgetMenuItem = (menu: WidgetFooterMenu, index: number) => {
return (
<div key={index} className="text-sm">
<div key={menu.id} className="text-sm">
<h2 className="font-semibold text-neutral-700 dark:text-neutral-200">
{menu.title}
{t(`widgetMenus.${menu.title}.title`)}
</h2>
<ul className="mt-5 space-y-4">
{menu.menus.map((item, index) => (
<li key={index}>
{menu.menus.map((item, idx) => (
<li key={idx}>
<a
key={index}
className="text-neutral-6000 dark:text-neutral-300 hover:text-black dark:hover:text-white"
href={item.href}
>
@ -88,16 +57,25 @@ const Footer: React.FC = () => {
<FooterNav />
<div className="nc-Footer relative py-24 lg:py-28 border-t border-neutral-200 dark:border-neutral-700">
<div className="container grid grid-cols-2 gap-y-10 gap-x-5 sm:gap-x-8 md:grid-cols-4 lg:grid-cols-5 lg:gap-x-10 ">
<div className="grid grid-cols-4 gap-5 col-span-2 md:col-span-4 lg:md:col-span-1 lg:flex lg:flex-col">
<div className="col-span-2 md:col-span-1">
<div className="container grid grid-cols-2 gap-y-10 gap-x-5 sm:gap-x-8 md:grid-cols-4 lg:grid-cols-6 lg:gap-x-10 ">
<div className="grid grid-cols-4 gap-5 col-span-2 md:col-span-4 lg:md:col-span-2 lg:flex lg:flex-col">
<div className="">
<Logo />
</div>
<div className="col-span-2 flex items-center md:col-span-3">
<SocialsList1 className="flex items-center space-x-3 lg:space-x-0 lg:flex-col lg:space-y-2.5 lg:items-start" />
</div>
<p className="font-extralight text-sm text-[#A2ABB8]">
{t("footerNav.description")}
</p>
</div>
<div className="col-span-1 flex items-center md:col-span-1">
<SocialsList1 className="flex items-center space-x-3 lg:space-x-0 lg:flex-col lg:space-y-2.5 lg:items-start" />
</div>
{widgetMenus.map(renderWidgetMenuItem)}
<div className="grid grid-cols-4 gap-5 col-span-2 md:col-span-4 lg:md:col-span-2 lg:flex lg:flex-col">
<h1>{t("aboutUs.title")}</h1>
<p className="font-extralight text-sm text-[#A2ABB8]">
{t("aboutUs.description")}
</p>
</div>
</div>
</div>
</>

10
src/components/HeaderFilter.tsx

@ -6,6 +6,7 @@ import Nav from "@/shared/Nav";
import NavItem from "@/shared/NavItem";
import ButtonSecondary from "@/shared/ButtonSecondary";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import { Tab } from "@headlessui/react";
export interface HeaderFilterProps {
tabActive: string;
@ -33,6 +34,7 @@ const HeaderFilter: FC<HeaderFilterProps> = ({
onClickTab(item); // Trigger the onClickTab function passed from parent
setTabActiveState(item);
};
console.log(tabs);
return (
<div className="flex flex-col mb-8 relative">
@ -50,11 +52,11 @@ const HeaderFilter: FC<HeaderFilterProps> = ({
</NavItem>
{tabs?.map((item) => (
<NavItem
key={item.id}
isActive={tabActiveState === item.name}
onClick={() => handleClickTab(item.name)} // Pass the country name
key={item}
isActive={tabActiveState === item}
onClick={() => handleClickTab(item)} // Pass the country name
>
{item.name}
{item}
</NavItem>
))}
</Nav>

93
src/components/SectionClientSay.tsx

@ -1,7 +1,7 @@
"use client";
import Heading from "@/shared/Heading";
import React, { FC, useState } from "react";
import Heading from "@/shared/Heading";
import clientSayMain from "@/images/clientSayMain.png";
import clientSay1 from "@/images/clientSay1.png";
import clientSay2 from "@/images/clientSay2.png";
@ -16,40 +16,18 @@ import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import Image from "next/image";
import { useSwipeable } from "react-swipeable";
import { variants } from "@/utils/animationVariants";
import { useTranslation } from "react-i18next";
export interface SectionClientSayProps {
className?: string;
data?: typeof DEMO_DATA;
}
const DEMO_DATA = [
{
id: 1,
clientName: "Tiana Abie",
clientAddress: "Malaysia",
content:
"This place is exactly like the picture posted on Chisfis. Great service, we had a great stay!",
},
{
id: 2,
clientName: "Lennie Swiffan",
clientAddress: "London",
content:
"This place is exactly like the picture posted on Chisfis. Great service, we had a great stay!",
},
{
id: 3,
clientName: "Berta Emili",
clientAddress: "Tokyo",
content:
"This place is exactly like the picture posted on Chisfis. Great service, we had a great stay!",
},
];
const SectionClientSay: FC<SectionClientSayProps> = ({
className = "",
data = DEMO_DATA,
}) => {
const { t } = useTranslation("common");
const [index, setIndex] = useState(0);
const [direction, setDirection] = useState(0);
@ -84,55 +62,55 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
<Image
className="absolute top-9 -left-20"
src={clientSay1}
alt="client 1"
alt={t("sectionClientSay.imageAltClient1")}
/>
<Image
className="absolute bottom-[100px] right-full mr-40"
src={clientSay2}
alt="client 2"
alt={t("sectionClientSay.imageAltClient2")}
/>
<Image
className="absolute top-full left-[140px]"
src={clientSay3}
alt="client 3"
alt={t("sectionClientSay.imageAltClient3")}
/>
<Image
className="absolute -bottom-10 right-[140px]"
src={clientSay4}
alt="client 4"
alt={t("sectionClientSay.imageAltClient4")}
/>
<Image
className="absolute left-full ml-32 bottom-[80px]"
src={clientSay5}
alt="client 5"
alt={t("sectionClientSay.imageAltClient5")}
/>
<Image
className="absolute -right-10 top-10 "
className="absolute -right-10 top-10"
src={clientSay6}
alt="client 6"
alt={t("sectionClientSay.imageAltClient6")}
/>
</div>
);
};
return (
<div className={`nc-SectionClientSay relative ${className} `}>
<Heading desc="Let's see what people think of Chisfis" isCenter>
Good news from far away
<div className={`nc-SectionClientSay relative ${className}`}>
<Heading className="" desc="" isCenter>
{t("sectionClientSay.heading")}
</Heading>
<div className="relative md:mb-16 max-w-2xl mx-auto">
{renderBg()}
<Image className="mx-auto" src={clientSayMain} alt="" />
<div className={`mt-12 lg:mt-16 relative `}>
<Image className="mx-auto" src={clientSayMain} alt={t("sectionClientSay.imageAltMain")} />
<div className={`mt-12 lg:mt-16 relative`}>
<Image
className="opacity-50 md:opacity-100 absolute -mr-16 lg:mr-3 right-full top-1"
src={quotationImg}
alt=""
alt={t("sectionClientSay.imageAltQuotation1")}
/>
<Image
className="opacity-50 md:opacity-100 absolute -ml-16 lg:ml-3 left-full top-1"
src={quotationImg2}
alt=""
alt={t("sectionClientSay.imageAltQuotation2")}
/>
<MotionConfig
@ -152,19 +130,19 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
variants={variants(200, 1)}
initial="enter"
animate="center"
// exit="exit"
exit="exit"
className="inline-flex flex-col items-center text-center whitespace-normal"
>
<>
<span className="block text-2xl">
{currentItem.content}
{t(`sectionClientSay.testimonials.${index}.content`)}
</span>
<span className="block mt-8 text-2xl font-semibold">
{currentItem.clientName}
{t(`sectionClientSay.testimonials.${index}.clientName`)}
</span>
<div className="flex items-center space-x-2 text-lg mt-2 text-neutral-400">
<MapPinIcon className="h-5 w-5" />
<span>{currentItem.clientAddress}</span>
<span>{t(`sectionClientSay.testimonials.${index}.clientAddress`)}</span>
</div>
</>
</motion.div>
@ -174,10 +152,11 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
{data.map((item, i) => (
<button
className={`w-2 h-2 rounded-full ${
i === index ? "bg-black/70" : "bg-black/10 "
i === index ? "bg-black/70" : "bg-black/10"
}`}
onClick={() => changeItemId(i)}
key={i}
aria-label={t(`sectionClientSay.testimonials.${i}.clientName`)}
/>
))}
</div>
@ -189,4 +168,30 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
);
};
// Move DEMO_DATA inside the component to use the t function
const DEMO_DATA = [
{
id: 1,
clientName: "Tiana Abie",
clientAddress: "Malaysia",
content:
"Traveling with Aqila was a spiritually enriching experience. The knowledgeable guides and warm staff made every moment memorable.",
},
{
id: 2,
clientName: "Lennie Swiffan",
clientAddress: "London",
content:
"Everything was perfectly arranged, from accommodations to visits to sacred sites. I felt well taken care of throughout the entire tour.",
},
{
id: 3,
clientName: "Berta Emili",
clientAddress: "Tokyo",
content:
"Joining this tour allowed me to connect deeply with fellow travelers and my heritage. Highly recommend Aqila for a meaningful trip!",
},
];
export default SectionClientSay;

31
src/components/SectionCustomTour.tsx

@ -1,40 +1,49 @@
import BackgroundSection from "@/components/BackgroundSection";
"use client";
import React from "react";
import BackgroundSection from "@/components/BackgroundSection";
import BackgroundImage from "@/images/Frame-412.webp";
import Image from "next/image";
import ButtonPrimary from "@/shared/ButtonPrimary";
import { useTranslation } from "react-i18next";
const SectionDownloadApp = () => {
const { t } = useTranslation("common");
return (
<BackgroundSection className="relative h-[455px] lg:py-32 xl:py-40 rounded-lg flex items-center justify-center sm:justify-normal sm:text-left sm:pl-12 lg:pl-56 xl:overflow-hidden bg-gray-900 text-center">
<BackgroundSection
className="relative h-[455px] lg:py-32 xl:py-40 rounded-lg flex items-center justify-center sm:justify-normal sm:text-left sm:pl-12 lg:pl-56 rtl:lg:pr-56 xl:overflow-hidden bg-gray-900 text-center rtl:w-auto "
data-nc-id="SectionDownloadApp"
>
{/* Background Image */}
<Image
className="absolute inset-0 object-cover object-right z-0" // Dim the background image for contrast
alt="Custom Tour Background"
alt={t("imageAltCustomTourBackground")}
src={BackgroundImage}
fill
quality={100}
/>
{/* Content Wrapper */}
<div className="relative z-10 ">
<div className="relative z-10 rtl:text-right">
<h1 className="text-white text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
Create
{t("create")}
</h1>
<h1 className="text-white text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
Custom Tour
{t("customTour")}
</h1>
<p className="text-white text-lg mt-4 opacity-80">
Create your personalized tour and design the
{t("createPersonalizedTourLine1")}
</p>
<p className="text-white text-lg mt-4 opacity-80">
perfect travel experience tailored to your preferences.
{t("createPersonalizedTourLine2")}
</p>
<ButtonPrimary href="custom-trip" className="mt-8 px-8 py-3 text-lg">Custom Tour</ButtonPrimary>
<ButtonPrimary href="/custom-trip" className="mt-8 px-8 py-3 text-lg">
{t("customTour")}
</ButtonPrimary>
</div>
</BackgroundSection>
);
};
export default SectionDownloadApp;
export default SectionDownloadApp;

35
src/components/SectionGridFeaturePlaces.tsx

@ -1,6 +1,6 @@
"use client";
import React, { FC, ReactNode, useContext, useEffect, useState } from "react";
import React, { FC, ReactNode, useEffect, useState } from "react";
import { StayDataType } from "@/data/types";
import ButtonPrimary from "@/shared/ButtonPrimary";
import HeaderFilter from "./HeaderFilter";
@ -8,11 +8,12 @@ import StayCard2 from "./StayCard2";
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { useSwipeable } from "react-swipeable";
import { useWindowSize } from "react-use";
import { Context, useToursContext } from "./contexts/tourDetails";
import { useToursContext } from "./contexts/tourDetails";
import { variants } from "@/utils/animationVariants";
import PrevBtn from "./PrevBtn";
import NextBtn from "./NextBtn";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
export interface SectionGridFeaturePlacesProps {
stayListings?: StayDataType[];
@ -26,21 +27,21 @@ export interface SectionGridFeaturePlacesProps {
const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
gridClass = "",
heading = "List of Tours",
subHeading = "Explore tours and accommodations tailored for a spiritual and memorable journey",
heading = "listOfTours",
subHeading = "exploreTours",
}) => {
const { countries, tours } = useToursContext()
const { countries, tours } = useToursContext();
const [countryTours, setCountryTours] = useState<any>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [numberOfItems, setNumberOfItems] = useState(0);
const router = useRouter();
const { t } = useTranslation("common");
const windowWidth = useWindowSize().width;
useEffect(() => {
handleChange("All");
}, [tours, countries]);
handleChange(t("All"));
}, [tours, countries, t]);
useEffect(() => {
if (windowWidth < 320) {
@ -60,7 +61,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
}, [windowWidth]);
const handleChange = (item: string) => {
if (item === "All") {
if (item === t("All")) {
setCountryTours(tours.results);
} else {
const selected = countries.find((country) => country.name === item);
@ -80,9 +81,9 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
console.log(`Changing index from ${currentIndex} to ${newVal}`); // Debug log
setCurrentIndex(newVal);
}
console.log(tours);
const handlers = useSwipeable({
onSwipedLeft: () => {
if (currentIndex < countryTours?.length - 1) {
@ -102,10 +103,10 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
return (
<div className="nc-SectionGridFeaturePlaces relative">
<HeaderFilter
tabActive={"All"}
subHeading={subHeading}
tabs={countries}
heading={heading}
tabActive={t("All")}
subHeading={subHeading ? t(subHeading as string) : undefined}
tabs={countries.map(country => country.name)}
heading={heading ? t(heading as string) : undefined}
onClickTab={(item) => handleChange(item)}
/>
<MotionConfig
@ -128,7 +129,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
initial={{ x: `${(currentIndex - 1) * -100}%` }}
animate={{ x: `${currentIndex * -100}%` }}
variants={variants(200, 1)}
key={indx}
key={stay.id}
style={{ width: `calc(1/${numberOfItems} * 100%)` }}
>
<StayCard2 key={stay.id} data={stay} />
@ -158,7 +159,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
<div className="flex mt-16">
<ButtonPrimary onClick={() => router.push("/tours")}>
Show me more
{t("showMore")}
</ButtonPrimary>
</div>
</div>

76
src/components/SectionHowItWork.tsx

@ -1,10 +1,13 @@
"use client";
import React, { FC } from "react";
import HIW1img from "@/images/HIW1.png";
import HIW2img from "@/images/HIW2.png";
import HIW3img from "@/images/HIW3.png";
import HIW1img from "@/images/HIW1.webp";
import HIW2img from "@/images/HIW2.webp";
import HIW3img from "@/images/HIW3.webp";
import VectorImg from "@/images/VectorHIW.svg";
import Image, { StaticImageData } from "next/image";
import Heading from "@/shared/Heading";
import { useTranslation } from "react-i18next";
export interface SectionHowItWorkProps {
className?: string;
@ -17,46 +20,51 @@ export interface SectionHowItWorkProps {
}[];
}
const DEMO_DATA: SectionHowItWorkProps["data"] = [
{
id: 1,
img: HIW1img,
title: "Book & relax",
desc: "Let each trip be an inspirational journey, each room a peaceful space",
},
{
id: 2,
img: HIW2img,
title: "Smart checklist",
desc: "Let each trip be an inspirational journey, each room a peaceful space",
},
{
id: 3,
img: HIW3img,
title: "Save more",
desc: "Let each trip be an inspirational journey, each room a peaceful space",
},
];
const SectionHowItWork: FC<SectionHowItWorkProps> = ({
className = "",
data = DEMO_DATA,
data,
}) => {
const { t } = useTranslation("common");
// Define the default data with translations
const DEMO_DATA: SectionHowItWorkProps["data"] = [
{
id: 1,
img: HIW1img,
title: t("howItWorks.bookAndRelax.title"),
desc: t("howItWorks.bookAndRelax.desc"),
},
{
id: 2,
img: HIW2img,
title: t("howItWorks.smartChecklist.title"),
desc: t("howItWorks.smartChecklist.desc"),
},
{
id: 3,
img: HIW3img,
title: t("howItWorks.saveMore.title"),
desc: t("howItWorks.saveMore.desc"),
},
];
const contentData = data || DEMO_DATA;
return (
<div
className={`nc-SectionHowItWork ${className}`}
className={`nc-SectionHowItWork ${className}`}
data-nc-id="SectionHowItWork"
>
<Heading isCenter desc="Keep calm & travel on">
How it work
<Heading isCenter desc={t("howItWorks.desc")}>
{t("howItWorks.title")}
</Heading>
<div className="mt-20 relative grid md:grid-cols-3 gap-20">
<Image
className="hidden md:block absolute inset-x-0 top-10"
src={VectorImg}
alt=""
alt={t("howItWorks.vectorAlt")}
/>
{data.map((item) => (
{contentData.reverse().map((item) => (
<div
key={item.id}
className="relative flex flex-col items-center max-w-xs mx-auto"
@ -64,19 +72,19 @@ const SectionHowItWork: FC<SectionHowItWorkProps> = ({
{item.imgDark ? (
<>
<Image
className="dark:hidden block mb-8 max-w-[180px] mx-auto"
className="dark:hidden block mb-8 max-w-[237px] mx-auto"
src={item.img}
alt=""
alt={t(`howItWorks.item${item.id}.imageAlt`)}
/>
<Image
alt=""
alt={t(`howItWorks.item${item.id}.imageAltDark`)}
className="hidden dark:block mb-8 max-w-[180px] mx-auto"
src={item.imgDark}
/>
</>
) : (
<Image
alt=""
alt={t(`howItWorks.item${item.id}.imageAlt`)}
className="mb-8 max-w-[180px] mx-auto"
src={item.img}
/>

34
src/components/SectionOurFeatures.tsx

@ -1,7 +1,10 @@
"use client";
import React, { FC } from "react";
import rightImgPng from "@/images/our-features.png";
import Image, { StaticImageData } from "next/image";
import Badge from "@/shared/Badge";
import { useTranslation } from "react-i18next";
export interface SectionOurFeaturesProps {
className?: string;
@ -14,6 +17,8 @@ const SectionOurFeatures: FC<SectionOurFeaturesProps> = ({
rightImg = rightImgPng,
type = "type1",
}) => {
const { t } = useTranslation("common");
return (
<div
className={`nc-SectionOurFeatures relative flex flex-col items-center ${
@ -22,7 +27,7 @@ const SectionOurFeatures: FC<SectionOurFeaturesProps> = ({
data-nc-id="SectionOurFeatures"
>
<div className="flex-grow">
<Image src={rightImg} alt="" />
<Image src={rightImg} alt={t("imageAltOurFeatures")} />
</div>
<div
className={`max-w-2xl flex-shrink-0 mt-10 lg:mt-0 lg:w-2/5 ${
@ -30,39 +35,38 @@ const SectionOurFeatures: FC<SectionOurFeaturesProps> = ({
}`}
>
<span className="uppercase text-sm text-gray-400 tracking-widest">
BENnefits
{t("benefits")}
</span>
<h2 className="font-semibold text-4xl mt-5">Happening cities </h2>
<h2 className="font-semibold text-4xl mt-5">
{t("happeningCities")}
</h2>
<ul className="space-y-10 mt-16">
<li className="space-y-4">
{/* <Badge name="Advertising" /> */}
{/* <Badge name={t("advertising")} /> */}
<span className="block text-xl font-semibold">
Cost-effective advertising
{t("costEffectiveAdvertising")}
</span>
<span className="block mt-5 text-neutral-500 dark:text-neutral-400">
With a free listing, you can advertise your rental with no upfront
costs
{t("costEffectiveDescription")}
</span>
</li>
<li className="space-y-4">
{/* <Badge color="green" name="Exposure " /> */}
{/* <Badge color="green" name={t("exposure")} /> */}
<span className="block text-xl font-semibold">
Reach millions with Chisfis
{t("reachMillions")}
</span>
<span className="block mt-5 text-neutral-500 dark:text-neutral-400">
Millions of people are searching for unique places to stay around
the world
{t("reachMillionsDescription")}
</span>
</li>
<li className="space-y-4">
{/* <Badge color="red" name="Secure" />/ */}
{/* <Badge color="red" name={t("secure")} /> */}
<span className="block text-xl font-semibold">
Secure and simple
{t("secureAndSimple")}
</span>
<span className="block mt-5 text-neutral-500 dark:text-neutral-400">
A Holiday Lettings listing gives you a secure and easy way to take
bookings and payments online
{t("secureDescription")}
</span>
</li>
</ul>

149
src/components/TourSuggestion.tsx

@ -1,6 +1,6 @@
"use client";
import React, { FC, useContext, useEffect, useState } from "react";
import React, { FC, useEffect, useState } from "react";
import { TaxonomyType } from "@/data/types";
import CardCategory3 from "@/components/CardCategory3";
import CardCategory4 from "@/components/CardCategory4";
@ -12,9 +12,8 @@ import PrevBtn from "./PrevBtn";
import NextBtn from "./NextBtn";
import { variants } from "@/utils/animationVariants";
import { useWindowSize } from "react-use";
import axiosInstance from "./api/axios";
import { Context, useToursContext } from "./contexts/tourDetails";
import CardCategory1 from "./CardCategory1";
import { useToursContext } from "./contexts/tourDetails";
import { useTranslation } from "react-i18next";
export interface TourSuggestionProps {
className?: string;
@ -26,128 +25,41 @@ export interface TourSuggestionProps {
sliderStyle?: "style1" | "style2";
}
const DEMO_CATS: TaxonomyType[] = [
{
id: "1",
href: "/listing-stay-map",
name: "Nature House",
taxonomy: "category",
count: 17288,
thumbnail:
"https://images.pexels.com/photos/2581922/pexels-photo-2581922.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260",
},
{
id: "2",
href: "/listing-stay-map",
name: "Wooden house",
taxonomy: "category",
count: 2118,
thumbnail:
"https://images.pexels.com/photos/2351649/pexels-photo-2351649.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "3",
href: "/listing-stay-map",
name: "Houseboat",
taxonomy: "category",
count: 36612,
thumbnail:
"https://images.pexels.com/photos/962464/pexels-photo-962464.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "4",
href: "/listing-stay-map",
name: "Farm House",
taxonomy: "category",
count: 18188,
thumbnail:
"https://images.pexels.com/photos/248837/pexels-photo-248837.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "5",
href: "/listing-stay-map",
name: "Dome House",
taxonomy: "category",
count: 22288,
thumbnail:
"https://images.pexels.com/photos/3613236/pexels-photo-3613236.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
},
{
id: "6",
href: "/listing-stay-map",
name: "Dome House",
taxonomy: "category",
count: 188288,
thumbnail:
"https://images.pexels.com/photos/14534337/pexels-photo-14534337.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load",
},
{
id: "7",
href: "/listing-stay-map",
name: "Wooden house",
taxonomy: "category",
count: 2118,
thumbnail:
"https://images.pexels.com/photos/2351649/pexels-photo-2351649.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
},
{
id: "8",
href: "/listing-stay-map",
name: "Wooden Dome",
taxonomy: "category",
count: 515,
thumbnail:
"https://images.pexels.com/photos/9039238/pexels-photo-9039238.jpeg?auto=compress&cs=tinysrgb&w=1600&lazy=load",
},
];
const TourSuggestion: FC<TourSuggestionProps> = ({
heading = "Countries ",
subHeading = "Popular places to recommends for you",
heading = "Countries",
subHeading = "Popular places to recommend for you",
className = "",
itemClassName = "",
itemPerRow = 5,
categoryCardType = "card3",
sliderStyle = "style1",
}) => {
const { t } = useTranslation("common");
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [numberOfItems, setNumberOfitem] = useState(0);
const { tours , countries } = useToursContext()
const [numberOfItems, setNumberOfItems] = useState(0);
const { tours, countries } = useToursContext();
const windowWidth = useWindowSize().width;
useEffect(() => {
if (windowWidth < 320) {
return setNumberOfitem(1);
return setNumberOfItems(1);
}
if (windowWidth < 500) {
return setNumberOfitem(itemPerRow - 3);
return setNumberOfItems(itemPerRow - 3);
}
if (windowWidth < 1024) {
return setNumberOfitem(itemPerRow - 2);
return setNumberOfItems(itemPerRow - 2);
}
if (windowWidth < 1280) {
return setNumberOfitem(itemPerRow - 1);
return setNumberOfItems(itemPerRow - 1);
}
setNumberOfitem(itemPerRow);
setNumberOfItems(itemPerRow);
}, [itemPerRow, windowWidth]);
// useEffect(() => {
// axiosInstance
// .get("/api/tours/")
// .then((response) => {
// setTours(response.data);
// })
// .catch((error) => {
// console.error("Error fetching data:", error);
// });
// }, []);
console.log(countries);
console.log(countries);
function changeItemId(newVal: number) {
if (newVal > currentIndex) {
@ -172,25 +84,12 @@ console.log(countries);
trackMouse: true,
});
// const renderCard = (item: TaxonomyType) => {
// switch (categoryCardType) {
// case "card3":
// return <CardCategory3 taxonomy={item} />;
// case "card4":
// return <CardCategory4 taxonomy={item} />;
// case "card5":
// return <CardCategory5 taxonomy={item} />;
// default:
// return <CardCategory3 taxonomy={item} />;
// }
// };
if (!numberOfItems) return null;
return (
<div className={`nc-SectionSliderNewCategories ${className}`}>
<Heading desc={subHeading} isCenter={sliderStyle === "style2"}>
{heading}
<div className={`nc-SectionSliderNewCategories ${className}`} data-nc-id="SectionSliderNewCategories">
<Heading desc={t("tourSuggestion.subHeading")} isCenter={sliderStyle === "style2"}>
{t("tourSuggestion.heading")}
</Heading>
<MotionConfig
transition={{
@ -205,7 +104,7 @@ console.log(countries);
className="relative whitespace-nowrap -mx-2 xl:-mx-4"
>
<AnimatePresence initial={false} custom={direction}>
{countries?.map((item, indx) => (
{countries?.map((item, indx) => (
<motion.li
className={`relative inline-block px-2 xl:px-4 ${itemClassName}`}
custom={direction}
@ -221,28 +120,28 @@ console.log(countries);
width: `calc(1/${numberOfItems} * 100%)`,
}}
>
<CardCategory3 tours = {tours} countries = {countries} taxonomy={item} />
<CardCategory3 tours={tours} countries={countries} taxonomy={item} />
</motion.li>
))}
</AnimatePresence>
</motion.ul>
</div>
{currentIndex ? (
{currentIndex > 0 && (
<PrevBtn
style={{ transform: "translate3d(0, 0, 0)" }}
onClick={() => changeItemId(currentIndex - 1)}
className="w-9 h-9 xl:w-12 xl:h-12 text-lg absolute -left-3 xl:-left-6 top-1/3 -translate-y-1/2 z-[1]"
/>
) : null}
)}
{countries?.length > currentIndex + numberOfItems ? (
{countries?.length > currentIndex + numberOfItems && (
<NextBtn
style={{ transform: "translate3d(0, 0, 0)" }}
onClick={() => changeItemId(currentIndex + 1)}
className="w-9 h-9 xl:w-12 xl:h-12 text-lg absolute -right-3 xl:-right-6 top-1/3 -translate-y-1/2 z-[1]"
/>
) : null}
)}
</div>
</MotionConfig>
</div>

10
src/data/navigation.ts

@ -168,28 +168,28 @@ export const NAVIGATION_DEMO: NavItemType[] = [
{
id: ncNanoId(),
href: "/",
name: "Home",
name: "text-home",
},
{
id: ncNanoId(),
href: "/tours",
name: "All Tours",
name: "text-all-tours",
type: "dropdown",
},
{
id: ncNanoId(),
href: "/blog",
name: "Blogs",
name: "text-blog",
},
{
id: ncNanoId(),
href: "/faq",
name: "FAQ",
name: "text-faq",
},
{
id: ncNanoId(),
href: "/about",
name: "AboutUs",
name: "text-about",
},
// {
// id: ncNanoId(),

24
src/hooks/FormValidation.ts

@ -9,22 +9,22 @@ const useFormValidation = () => {
const validateForm = (form: SignUpForm) => {
let newErrors: Record<string, string> = {};
if (!form.name) {
newErrors.name = 'Full Name is required';
if (!form.fullname) {
newErrors.fullname = 'Full Name is required';
}
if (!form.countryCode || !/^\d{1,3}$/.test(form.countryCode)) {
newErrors.countryCode = 'Country Code must be a number with up to 3 digits';
if (!form.range_phone || !/^\d{1,3}$/.test(form.range_phone)) {
newErrors.range_phone = 'Country Code must be a number with up to 3 digits';
}
// Validate phone number using google-libphonenumber
try {
const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + form.countryCode + form.phoneNumber, form.countryCode);
const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + form.range_phone + form.phone_number, form.range_phone);
if (!phoneUtil.isValidNumber(parsedNumber)) {
newErrors.phoneNumber = 'Invalid phone number for the selected country';
newErrors.phone_number = 'Invalid phone number for the selected country';
}
} catch (error) {
newErrors.phoneNumber = 'Invalid phone number format';
newErrors.phone_number = 'Invalid phone number format';
}
if (!form.password) {
@ -33,10 +33,10 @@ const useFormValidation = () => {
newErrors.password = 'Password must be at least 8 characters';
}
if (!form.confirmPassword) {
newErrors.confirmPassword = 'Confirm Password is required';
} else if (form.password !== form.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
if (!form.password_confirmation) {
newErrors.password_confirmation = 'Confirm Password is required';
} else if (form.password !== form.password_confirmation) {
newErrors.password_confirmation = 'Passwords do not match';
}
setErrors(newErrors);
@ -46,4 +46,4 @@ const useFormValidation = () => {
return { errors, validateForm, setErrors };
};
export default useFormValidation;
export default useFormValidation;

25
src/i18n.ts

@ -5,14 +5,31 @@ 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 enNavigation from '../public/locales/en/navigation.json';
import enForm from '../public/locales/en/form.json';
import enFooter from '../public/locales/en/footer.json';
import ruCommon from '../public/locales/ru/common.json';
import ruNavigation from '../public/locales/ru/navigation.json';
import ruForm from '../public/locales/ru/form.json';
import ruFooter from '../public/locales/ru/footer.json';
import ruFAQ from '../public/locales/ru/FAQ.json';
import idCommon from '../public/locales/id/common.json';
import idNavigation from '../public/locales/id/navigation.json';
import idForm from '../public/locales/id/form.json';
import idFooter from '../public/locales/id/footer.json';
import idFAQ from '../public/locales/id/FAQ.json';
import arCommon from '../public/locales/ar/common.json';
import arNavigation from '../public/locales/ar/navigation.json';
import arForm from '../public/locales/ar/form.json';
import arFooter from '../public/locales/ar/footer.json';
import arFAQ from '../public/locales/ar/FAQ.json';
// import viCommon from '../public/locales/vi/common.json';
const resources = {
en: { common: enCommon , FAQ: enFAQ },
fr: { common: frCommon },
ar: { common: arCommon },
en: { common: enCommon , FAQ: enFAQ , navigation : enNavigation , form : enForm , footer : enFooter},
ar: { common: arCommon , FAQ: arFAQ , navigation : arNavigation , form : arForm , footer : arFooter},
ru: { common: ruCommon , FAQ: ruFAQ , navigation : ruNavigation , form : ruForm , footer : ruFooter},
id: { common: idCommon , FAQ: idFAQ , navigation : idNavigation , form : idForm , footer : idFooter},
// vi: { common: viCommon },
};

BIN
src/images/HIW1.webp

BIN
src/images/HIW2.webp

BIN
src/images/HIW3.webp

27
src/middleware.ts

@ -2,37 +2,42 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/;
const locales = ['en', 'vi', 'fr', 'ar'];
const locales = ['en', 'ru', 'id', 'ar'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl;
// Ignore API routes and public files
if (pathname.startsWith('/api') || PUBLIC_FILE.test(pathname)) {
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 cookieLocale = request.cookies.get('locale')?.value;
const pathnameParts = pathname.split('/');
const hasLocale = locales.includes(pathnameParts[1]);
// Prefer locale in the path; fallback to cookie or defaultLocale
const locale = hasLocale
? pathnameParts[1]
: cookieLocale || defaultLocale;
if (!hasLocale) {
// Handle root path separately to avoid double slashes
const newPathname =
pathname === '/' ? `/${locale}` : `/${locale}${pathname}`;
const newPathname = pathname === '/' ? `/${locale}` : `/${locale}${pathname}`;
const url = new URL(`${newPathname}${search}`, request.url);
return NextResponse.redirect(url);
// Set cookie if missing
const response = NextResponse.redirect(url);
if (!cookieLocale) {
response.cookies.set('locale', locale);
}
return response;
}
// If locale is present, continue
return NextResponse.next();
}
// Matcher configuration remains the same
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],

20
src/routers/types.ts

@ -1,12 +1,18 @@
import type { Route as NextRoute } from "next";
import { ComponentType } from "react";
// Get ready to update to nextjs version 13.2 with X typedRoutes
export type Route<T = string> = NextRoute;
export type PathName = Route;
// Define a union of allowed route strings for static typing
export type Route = {
path: "/account" | "/my-trips" | "/passengers-list" | "/bills" | string; // Enumerate common routes, allow dynamic
label: string; // Human-readable label for navigation or display purposes
};
// Alias PathName to Route for clarity
export type PathName = Route["path"];
// Interface for pages with optional exact matching and associated component
export interface Page {
path: PathName;
exact?: boolean;
component: ComponentType<Object>;
path: PathName; // Typed path
exact?: boolean; // Indicates if the route requires an exact match
label: string; // Label for the route (e.g., for UI)
component: ComponentType<any>; // Replace "any" with specific props type if known
}

2
src/shared/Button.tsx

@ -32,7 +32,7 @@ const Button: FC<ButtonProps> = ({
loading,
onClick = () => {},
}) => {
const CLASSES = `nc-Button relative h-auto inline-flex items-center justify-center rounded-full transition-colors ${fontSize} ${sizeClass} ${translate} ${className} `;
const CLASSES = `nc-Button relative h-auto inline-flex items-center justify-center rounded-full transition-colors rtl:!ml-0 rtl:!mr-4 ${fontSize} ${sizeClass} ${translate} ${className} `;
const _renderLoading = () => {
return (

2
src/shared/Logo.tsx

@ -19,7 +19,7 @@ const Logo: React.FC<LogoProps> = ({
return (
<Link
href="/"
className={`ttnc-logo flex text-primary-6000 focus:outline-none focus:ring-0 mr-10 ${className}`}
className={`ttnc-logo flex text-primary-6000 focus:outline-none focus:ring-0 mr-10 rtl:mr-0 rtl:ml-10 ${className}`}
>
{/* <LogoSvgLight /> */}
{/* THIS USE FOR MY CLIENT */}

5
src/shared/Navigation/Navigation.tsx

@ -4,14 +4,17 @@ import { NAVIGATION_DEMO } from "@/data/navigation";
import ButtonPrimary from "../ButtonPrimary";
import ButtonSecondary from "../ButtonSecondary";
import Link from "next/link";
import { useTranslation } from "react-i18next";
function Navigation() {
const {t} = useTranslation("navigation");
return (
<ul className="nc-Navigation hidden lg:flex lg:flex-wrap lg:space-x-1 relative ">
{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">{t("customTrip")}</Link>
</ul>
);
}

6
src/shared/Navigation/NavigationItem.tsx

@ -9,6 +9,7 @@ import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React, { FC, Fragment, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
// <--- NavItemType --->
export interface MegamenuItem {
@ -37,6 +38,7 @@ type NavigationItemWithRouterProps = NavigationItemProps;
const NavigationItem: FC<NavigationItemWithRouterProps> = ({ menuItem }) => {
const [menuCurrentHovers, setMenuCurrentHovers] = useState<string[]>([]);
const { countries } = useToursContext();
const { t } = useTranslation("navigation"); // Initialize useTranslation
// CLOSE ALL MENU OPENING WHEN CHANGE HISTORY
const locationPathName = usePathname();
@ -272,10 +274,10 @@ const NavigationItem: FC<NavigationItemWithRouterProps> = ({ menuItem }) => {
} py-2 px-4 xl:px-5 rounded-full hover:text-neutral-900 hover:bg-neutral-100 dark:hover:bg-neutral-800 dark:hover:text-neutral-200`}
href={item.href || "/"}
>
{item.name}
{t(item.name)}
{item.type && (
<ChevronDownIcon
className="ml-1 -mr-1 h-4 w-4 text-neutral-400"
className="ml-1 -mr-1 h-4 w-4 text-neutral-400 rtl:mr-1"
aria-hidden="true"
/>
)}

Loading…
Cancel
Save