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. 3
      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. 118
      src/app/[locale]/(account-pages)/account/page.tsx
  25. 62
      src/app/[locale]/(account-pages)/bills/BillCard.tsx
  26. 99
      src/app/[locale]/(account-pages)/bills/[slug]/page.tsx
  27. 16
      src/app/[locale]/(account-pages)/bills/page.tsx
  28. 46
      src/app/[locale]/(account-pages)/my-trips/page.tsx
  29. 13
      src/app/[locale]/(account-pages)/passengers-list/PassengerTable.tsx
  30. 289
      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. 15
      src/app/[locale]/(client-components)/(Header)/SearchDropdown.tsx
  34. 7
      src/app/[locale]/(client-components)/(HeroSearchForm)/(stay-search-form)/StayDatesRangeInput.tsx
  35. 4
      src/app/[locale]/(client-components)/(HeroSearchForm)/(stay-search-form)/StaySearchForm.tsx
  36. 35
      src/app/[locale]/(client-components)/(HeroSearchForm)/GuestsInput.tsx
  37. 30
      src/app/[locale]/(home)/SectionDowloadApp.tsx
  38. 2
      src/app/[locale]/about/SectionHero.tsx
  39. 2
      src/app/[locale]/about/page.tsx
  40. 4
      src/app/[locale]/add-listing/[[...stepIndex]]/page.tsx
  41. 107
      src/app/[locale]/forgot-password/page.tsx
  42. 76
      src/app/[locale]/login/page.tsx
  43. 45
      src/app/[locale]/signup/methodes/page.tsx
  44. 106
      src/app/[locale]/signup/otp-code/page.tsx
  45. 104
      src/app/[locale]/signup/page.tsx
  46. 64
      src/app/[locale]/tours/SectionGridFilterCard.tsx
  47. 25
      src/app/[locale]/tours/[slug]/page.tsx
  48. 11
      src/app/globals.css
  49. 21
      src/components/CardCategory3.tsx
  50. 82
      src/components/Footer.tsx
  51. 10
      src/components/HeaderFilter.tsx
  52. 85
      src/components/SectionClientSay.tsx
  53. 29
      src/components/SectionCustomTour.tsx
  54. 31
      src/components/SectionGridFeaturePlaces.tsx
  55. 50
      src/components/SectionHowItWork.tsx
  56. 34
      src/components/SectionOurFeatures.tsx
  57. 141
      src/components/TourSuggestion.tsx
  58. 10
      src/data/navigation.ts
  59. 22
      src/hooks/FormValidation.ts
  60. 25
      src/i18n.ts
  61. BIN
      src/images/HIW1.webp
  62. BIN
      src/images/HIW2.webp
  63. BIN
      src/images/HIW3.webp
  64. 27
      src/middleware.ts
  65. 20
      src/routers/types.ts
  66. 2
      src/shared/Button.tsx
  67. 2
      src/shared/Logo.tsx
  68. 5
      src/shared/Navigation/Navigation.tsx
  69. 6
      src/shared/Navigation/NavigationItem.tsx

2
next-i18next.config.js

@ -2,7 +2,7 @@ const path = require("path");
module.exports = { module.exports = {
i18n: { 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 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+", "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": "الفواتير"
}

3
public/locales/en/FAQ.json

@ -1,6 +1,6 @@
{ {
"faqTitle": "Frequently Asked Questions", "faqTitle": "Frequently Asked Questions",
"faqSubtitle": "Have Questions? We are here to help you!",
"faqSubtitle": "Have questions? We are here to help!",
"faqQuestion1": "How can I book a tour on your site?", "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.", "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?", "faqQuestion2": "Can I customize my tour?",
@ -22,4 +22,3 @@
"faqQuestion10": "How can I contact customer support?", "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." "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", "beginAdventure": "Begin your",
"beginAdventure1": "spiritual", "beginAdventure1": "spiritual",
"beginAdventure2": "adventure", "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", "startJourney": "Start your journey",
"listOfTours": "List of Tours", "listOfTours": "List of Tours",
"exploreTours": "Explore tours and accommodations tailored for a spiritual and memorable journey", "exploreTours": "Explore tours and accommodations tailored for a spiritual and memorable journey",
"tourPeriod": "Tour period",
"tourPeriod": "Tour Period",
"tourPeriodDescription": "Start - End", "tourPeriodDescription": "Start - End",
"guests": "Guests", "guests": "Guests",
"addGuests": "Add guests", "addGuests": "Add guests",
"available": "Available", "available": "Available",
"soldOut": "Sold Out",
"soldOut": "Sold out",
"showMore": "Show me more", "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", "costEffectiveDescription": "With a free listing, you can advertise your rental with no upfront costs",
"reachMillions": "Reach millions with Chisfis", "reachMillions": "Reach millions with Chisfis",
"reachMillionsDescription": "Millions of people are searching for unique places to stay around the world", "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", "secureDescription": "A Holiday Lettings listing gives you a secure and easy way to take bookings and payments online",
"mobileApps": "Mobile Apps", "mobileApps": "Mobile Apps",
"mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.", "mobileAppsDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dapibus porttitor nisl, sit amet finibus libero.",
@ -35,22 +35,22 @@
"upgradeGuide": "Upgrade Guide", "upgradeGuide": "Upgrade Guide",
"browserSupport": "Browser Support", "browserSupport": "Browser Support",
"editorSupport": "Editor Support", "editorSupport": "Editor Support",
"designFeatures": "Design features",
"designFeatures": "Design Features",
"prototyping": "Prototyping", "prototyping": "Prototyping",
"designSystems": "Design systems",
"designSystems": "Design Systems",
"pricing": "Pricing", "pricing": "Pricing",
"security": "Security", "security": "Security",
"bestPractices": "Best practices",
"bestPractices": "Best Practices",
"support": "Support", "support": "Support",
"developers": "Developers", "developers": "Developers",
"learnDesign": "Learn design",
"learnDesign": "Learn Design",
"releases": "Releases", "releases": "Releases",
"discussionForums": "Discussion Forums", "discussionForums": "Discussion Forums",
"codeOfConduct": "Code of Conduct", "codeOfConduct": "Code of Conduct",
"communityResources": "Community Resources", "communityResources": "Community Resources",
"contributing": "Contributing", "contributing": "Contributing",
"concurrentMode": "Concurrent Mode", "concurrentMode": "Concurrent Mode",
"goodNews": "Good news from far away",
"goodNews": "Good News from Far Away",
"whatPeopleThink": "Let's see what people think of Chisfis", "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!", "testimonial": "This place is exactly like the picture posted on Chisfis. Great service, we had a great stay!",
"clientName": "Tiana Abie", "clientName": "Tiana Abie",
@ -58,11 +58,11 @@
"myTrips": "My Trips", "myTrips": "My Trips",
"account": "Account", "account": "Account",
"menu": "Menu", "menu": "Menu",
"gettingStarted": "Getting started",
"gettingStarted": "Getting Started",
"explore": "Explore", "explore": "Explore",
"resources": "Resources", "resources": "Resources",
"community": "Community", "community": "Community",
"placeType": "Type of place",
"placeType": "Type of Place",
"noTours": "No tours available", "noTours": "No tours available",
"itinerary": "Itinerary", "itinerary": "Itinerary",
"itineraryTitle": "Itinerary", "itineraryTitle": "Itinerary",
@ -85,34 +85,139 @@
"submit": "Submit", "submit": "Submit",
"cancel": "Cancel", "cancel": "Cancel",
"relatedPosts": "Related Posts", "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!", "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", "statisticTitle": "🚀 Fast Facts",
"statisticDescription": "We’re impartial and independent, and every day we create distinctive, world-class programmes and content.", "statisticDescription": "We’re impartial and independent, and every day we create distinctive, world-class programmes and content.",
"statisticHeading1": "10 million", "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", "statisticHeading2": "100,000",
"statisticSubHeading2": "Registered users account (as of Sept. 30, 2021)",
"statisticSubHeading2": "Registered user accounts (as of Sept. 30, 2021)",
"statisticHeading3": "220+", "statisticHeading3": "220+",
"statisticSubHeading3": "Countries and regions have our presence (as of Sept. 30, 2021)", "statisticSubHeading3": "Countries and regions have our presence (as of Sept. 30, 2021)",
"customTrip": "Custom Trip", "customTrip": "Custom Trip",
"guide": "Guide", "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.", "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", "beginYourTrip": "Begin your trip",
"startDate": "Start Date",
"numberOfPassengers": "Number Of Passengers",
"startDate": "Start date",
"numberOfPassengers": "Number of passengers",
"destination": "Destination", "destination": "Destination",
"selectCity": "Select City",
"selectCity": "Select city",
"transportation": "Transportation", "transportation": "Transportation",
"selectTransport": "Select Transport",
"selectTransport": "Select transport",
"hotel": "Hotel", "hotel": "Hotel",
"selectHotel": "Select Hotel",
"selectHotel": "Select hotel",
"duration": "Duration", "duration": "Duration",
"finishDate": "Finish date", "finishDate": "Finish date",
"addDestination": "Add Destination",
"addDestination": "Add destination",
"continue": "Continue", "continue": "Continue",
"successMessage": "Successfully Registered",
"successMessage": "Successfully registered",
"to": "to", "to": "to",
"login": "LogIn",
"signup": "Sign Up"
"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 Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
export const Nav = () => { export const Nav = () => {
const { t } = useTranslation("navigation");
const pathname = usePathname(); const pathname = usePathname();
const listNav: Route[] = [ 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 ( return (
<div className="container"> <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 ( return (
<Link <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 isActive
? "border-primary-500 font-medium" ? "border-primary-500 font-medium"
: "border-transparent" : "border-transparent"
}`} }`}
> >
{item.replace("-", " ").replace("/", " ")}
{label}
</Link> </Link>
); );
})} })}

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

@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useState, ChangeEvent, FC, useEffect } from "react"; import React, { useState, ChangeEvent, FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import Label from "@/components/Label"; import Label from "@/components/Label";
import Avatar from "@/shared/Avatar"; import Avatar from "@/shared/Avatar";
import ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonPrimary from "@/shared/ButtonPrimary";
@ -14,55 +15,28 @@ import { toast } from "react-toastify";
export interface AccountPageProps {} 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 AccountPage: FC<AccountPageProps> = () => {
const { t } = useTranslation("common");
const router = useRouter(); const router = useRouter();
const { user, setUser, clerUser } = useUserContext(); const { user, setUser, clerUser } = useUserContext();
// Redirect to home if user object is empty
useEffect(() => { useEffect(() => {
if (!Object.keys(user).length) { if (!Object.keys(user).length) {
router.replace("/"); router.replace("/");
} }
}, [user, router]); }, [user, router]);
// Return null to avoid rendering when redirecting
// if (!Object.keys(user).length) {
// return null;
// }
const [name, setName] = useState<string>(user.fullname || ""); const [name, setName] = useState<string>(user.fullname || "");
const [email, setEmail] = useState<string>(user.email || ""); const [email, setEmail] = useState<string>(user.email || "");
const [number, setNumber] = useState<string>(user.phone_number || ""); const [number, setNumber] = useState<string>(user.phone_number || "");
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
const [imageURL, setImageURL] = useState<string | null>(null); const [imageURL, setImageURL] = useState<string | null>(null);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<LoadingState>({});
const [loading, setLoading] = useState({ delete: false, change: false });
const deleteHandler = async (): Promise<void> => { const deleteHandler = async (): Promise<void> => {
setError(""); setError("");
setLoading({ delete: true });
setLoading({ ...loading, delete: true });
try { try {
const response = await axiosInstance.delete(`/api/account/profile/delete/`, { const response = await axiosInstance.delete(`/api/account/profile/delete/`, {
headers: { headers: {
@ -71,47 +45,38 @@ const AccountPage: FC<AccountPageProps> = () => {
}); });
if (response.status === 204) { if (response.status === 204) {
clerUser(); clerUser();
toast.success("Your Delete account was successful")
toast.success(t("deleteSuccess"));
} else { } else {
setError("Something went wrong");
setError(t("errorGeneric"));
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
toast.error(error.message); toast.error(error.message);
} else { } else {
setError("An unknown error occurred");
setError(t("errorUnknown"));
} }
} finally { } finally {
setLoading({ delete: false });
setLoading({ ...loading, delete: false });
} }
}; };
const signOutHandler = (): void => { const signOutHandler = (): void => {
clerUser(); clerUser();
toast.success("Your sign out was successful")
toast.success(t("signOutSuccess"));
}; };
const changeHandler = async (): Promise<void> => { const changeHandler = async (): Promise<void> => {
setError(""); setError("");
setLoading({ change: true });
setLoading({ ...loading, change: true });
const formData = new FormData(); 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")) { if (formData.has("fullname") || formData.has("email") || formData.has("avatar")) {
try { try {
const response = await axiosInstance.put<APIResponse>(
const response = await axiosInstance.put(
`/api/account/profile/update/`, `/api/account/profile/update/`,
formData, formData,
{ {
@ -122,37 +87,33 @@ const AccountPage: FC<AccountPageProps> = () => {
} }
); );
if (response.status === 200) { if (response.status === 200) {
console.log(response);
toast.success("Updated successfully");
toast.success(t("updateSuccess"));
setUser({ setUser({
...user, ...user,
avatar: response.data.avatar,
email: response.data.email,
fullname: response.data.fullname, fullname: response.data.fullname,
phone_number: response.data.phone_number,
email: response.data.email,
avatar: response.data.avatar,
}); });
} else { } else {
setError("Something went wrong");
setError(t("errorGeneric"));
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
setError(error.message); setError(error.message);
} else { } else {
setError("An unknown error occurred");
setError(t("errorUnknown"));
} }
} finally { } finally {
setLoading({ change: false });
setLoading({ ...loading, change: false });
} }
} else { } 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 handleFileChange = async (e: ChangeEvent<HTMLInputElement>): Promise<void> => {
const file : File | undefined = e.target.files?.[0];
const file = e.target.files?.[0];
if (file) { if (file) {
const uploadedFile = await getImageURL(file); const uploadedFile = await getImageURL(file);
setImageURL(uploadedFile.url); setImageURL(uploadedFile.url);
@ -162,7 +123,7 @@ const AccountPage: FC<AccountPageProps> = () => {
return ( return (
<div className="space-y-6 sm:space-y-8"> <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="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row">
<div className="flex-shrink-0 flex items-start"> <div className="flex-shrink-0 flex items-start">
@ -172,22 +133,7 @@ const AccountPage: FC<AccountPageProps> = () => {
sizeClass="w-32 h-32" 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"> <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> </div>
<input <input
type="file" type="file"
@ -197,9 +143,9 @@ const AccountPage: FC<AccountPageProps> = () => {
/> />
</div> </div>
</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> <div>
<Label>Name</Label>
<Label>{t("nameLabel")}</Label>
<Input <Input
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
@ -207,7 +153,7 @@ const AccountPage: FC<AccountPageProps> = () => {
/> />
</div> </div>
<div> <div>
<Label>Email</Label>
<Label>{t("emailLabel")}</Label>
<Input <Input
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
@ -215,7 +161,7 @@ const AccountPage: FC<AccountPageProps> = () => {
/> />
</div> </div>
<div className="max-w-lg"> <div className="max-w-lg">
<Label>Phone</Label>
<Label>{t("phoneLabel")}</Label>
<Input <Input
value={number} value={number}
onChange={(e) => setNumber(e.target.value)} onChange={(e) => setNumber(e.target.value)}
@ -225,17 +171,17 @@ const AccountPage: FC<AccountPageProps> = () => {
</div> </div>
<div className="flex flex-col pt-2 sm:flex-row"> <div className="flex flex-col pt-2 sm:flex-row">
<ButtonPrimary className="mt-8" loading={loading.change} onClick={changeHandler}> <ButtonPrimary className="mt-8" loading={loading.change} onClick={changeHandler}>
Update info
{t("updateButton")}
</ButtonPrimary> </ButtonPrimary>
<ButtonPrimary className="mt-8 sm:ml-8" onClick={signOutHandler}> <ButtonPrimary className="mt-8 sm:ml-8" onClick={signOutHandler}>
Sign out
{t("signOutButton")}
</ButtonPrimary> </ButtonPrimary>
<ButtonSecondary <ButtonSecondary
loading={loading.delete} loading={loading.delete}
onClick={deleteHandler} onClick={deleteHandler}
className="opacity-60 mt-8 hover:bg-red-500 hover:text-white hover:opacity-100 sm:ml-8" className="opacity-60 mt-8 hover:bg-red-500 hover:text-white hover:opacity-100 sm:ml-8"
> >
Delete account
{t("deleteButton")}
</ButtonSecondary> </ButtonSecondary>
</div> </div>
</div> </div>

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

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

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

@ -1,4 +1,4 @@
"use client"
"use client";
import React, { useState } from "react"; import React, { useState } from "react";
import axiosInstance from "@/components/api/axios"; import axiosInstance from "@/components/api/axios";
@ -9,6 +9,7 @@ import { useUserContext } from "@/components/contexts/userContext";
import getImageURL from "@/components/api/getImageURL"; import getImageURL from "@/components/api/getImageURL";
import { toast, ToastContainer } from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import Image from "next/image"; import Image from "next/image";
import { useTranslation } from "react-i18next";
export type BillStatus = export type BillStatus =
| "awaiting_payment" | "awaiting_payment"
@ -44,22 +45,22 @@ export type BillStatus =
const statusStyles: { [key in BillStatus]: JSX.Element } = { const statusStyles: { [key in BillStatus]: JSX.Element } = {
awaiting_payment: ( awaiting_payment: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-yellow-100 text-yellow-700"> <span className="px-2 py-1 text-sm rounded-full opacity-70 bg-yellow-100 text-yellow-700">
Awaiting Payment
{"t('awaitingPayment')"}
</span> </span>
), ),
approved: ( approved: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700"> <span className="px-2 py-1 text-sm rounded-full opacity-70 bg-green-200 text-green-700">
Approved
{"t('approved')"}
</span> </span>
), ),
rejected: ( rejected: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700"> <span className="px-2 py-1 text-sm rounded-full opacity-70 bg-red-200 text-red-700">
Rejected
{"t('rejected')"}
</span> </span>
), ),
pending: ( pending: (
<span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700"> <span className="px-2 py-1 text-sm rounded-full opacity-70 bg-blue-200 text-blue-700">
Pending
{"t('pending')"}
</span> </span>
), ),
}; };
@ -69,6 +70,7 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
const [loadingUpload, setLoadingUpload] = useState(false); const [loadingUpload, setLoadingUpload] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { user } = useUserContext(); const { user } = useUserContext();
const { t } = useTranslation("form");
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setLoadingUpload(true); setLoadingUpload(true);
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@ -82,24 +84,30 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
const handleSubmit = () => { const handleSubmit = () => {
setLoading(true); setLoading(true);
if (uploadedFile) { if (uploadedFile) {
const formData = new FormData();
formData.append("transaction_receipt", uploadedFile);
axiosInstance axiosInstance
.patch(`/api/factors/update/${bill.id}/`, formData, {
.patch(
`/api/factors/update/${bill.id}/`,
{
transaction_receipt: uploadedFile,
},
{
headers: { headers: {
Authorization: `token ${user.token}`, Authorization: `token ${user.token}`,
}, },
})
}
)
.then(() => { .then(() => {
toast.success("Transaction receipt updated successfully");
toast.success(t("transactionReceiptUpdated"));
setLoading(false); setLoading(false);
}) })
.catch(() => {
toast.error("Error updating transaction receipt");
.catch((error) => {
console.log(error);
toast.error(t("updateFailed"));
setLoading(false); setLoading(false);
}); });
} else { } else {
console.log("No new file to upload");
console.log(t("noFileSelected"));
} }
}; };
@ -112,58 +120,56 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
{bill.message && ( {bill.message && (
<div className="bg-red-100 text-red-700 p-3 mt-4 rounded-md"> <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> <p className="text-sm mt-1">{bill.message}</p>
</div> </div>
)} )}
<div className="mt-4 text-sm text-gray-600 dark:text-neutral-400"> <div className="mt-4 text-sm text-gray-600 dark:text-neutral-400">
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span>Issued Date:</span>
<span>{t("issuedDate")}:</span>
<span>{bill.created_at}</span> <span>{bill.created_at}</span>
</div> </div>
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<span>Expiration Date:</span>
<span>{t("expirationDate")}:</span>
<span>{bill.expiration_date}</span> <span>{bill.expiration_date}</span>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<h3 className="font-semibold">Number of Passengers</h3>
<h3 className="font-semibold">{t("numberOfPassengers")}</h3>
<div className="flex justify-between"> <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>
<div className="flex justify-between"> <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>
<div className="flex justify-between"> <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> </div>
<div className="flex justify-between mt-4 font-semibold"> <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> <span className="text-orange-600">${bill.amount}</span>
</div> </div>
<div className="mt-4"> <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"> <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>
</div> </div>
{bill.uploadedImage ? ( {bill.uploadedImage ? (
<div className="mt-4"> <div className="mt-4">
<h3 className="font-semibold">Uploaded Image</h3>
<h3 className="font-semibold">{t("uploadedImage")}</h3>
<img <img
src={bill.uploadedImage.src} src={bill.uploadedImage.src}
alt="Uploaded" alt="Uploaded"
@ -171,16 +177,14 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
/> />
<div className="flex justify-between mt-2"> <div className="flex justify-between mt-2">
<span>{bill.uploadedImage.name}</span> <span>{bill.uploadedImage.name}</span>
<span className="text-gray-500 text-sm">
{bill.uploadedImage.size}
</span>
<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> </div>
) : ( ) : (
<div className="mt-4"> <div className="mt-4">
<label className="block font-semibold mb-2"> <label className="block font-semibold mb-2">
Upload Passport Image
{t("uploadPassportImage")}
</label> </label>
<div className="flex items-center bg-gray-100 p-2 rounded-md dark:bg-neutral-900"> <div className="flex items-center bg-gray-100 p-2 rounded-md dark:bg-neutral-900">
<Input <Input
@ -188,14 +192,19 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
type="file" type="file"
onChange={handleFileChange} onChange={handleFileChange}
/> />
{loadingUpload && <p>Loading ...</p>}
{loadingUpload && <p>{t("loading")}</p>}
{bill.transaction_receipt ? ( {bill.transaction_receipt ? (
<span className="flex-grow text-gray-500 text-sm ml-4"> <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>
) : ( ) : (
<span className="flex-grow text-gray-500 text-sm ml-4"> <span className="flex-grow text-gray-500 text-sm ml-4">
No File Selected
{t("noFileSelected")}
</span> </span>
)} )}
</div> </div>
@ -203,8 +212,12 @@ const BillDetailCard: React.FC<BillDetailCardProps> = ({ bill }) => {
)} )}
</div> </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> </ButtonPrimary>
<ToastContainer <ToastContainer
position="top-right" position="top-right"

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

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

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

@ -1,37 +1,26 @@
"use client"; "use client";
import { Tab } from "@headlessui/react"; 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 StayCard2 from "@/components/StayCard2";
import React, { useEffect, useState } from "react";
import axiosInstance from "@/components/api/axios";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useUserContext } from "@/components/contexts/userContext"; import { useUserContext } from "@/components/contexts/userContext";
import { useTranslation } from "react-i18next";
import { StayDataType } from "@/data/types"; import { StayDataType } from "@/data/types";
// interface stay {
// id : string
// }
const MyTrips = () => { 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(() => { useEffect(() => {
if (!Object.keys(user).length) { if (!Object.keys(user).length) {
router.replace("/signup"); router.replace("/signup");
} }
}, [user, router]); }, [user, router]);
useEffect(() => { useEffect(() => {
axiosInstance axiosInstance
.get("/api/tours/orders/", { .get("/api/tours/orders/", {
@ -41,20 +30,17 @@ const MyTrips = () => {
}) })
.then((response) => { .then((response) => {
setTours(response.data.results); setTours(response.data.results);
console.log(response);
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
}); });
}, []);
}, [user.token]);
const renderSection1 = () => { const renderSection1 = () => {
return ( return (
<div className="space-y-6 sm:space-y-8"> <div className="space-y-6 sm:space-y-8">
<div> <div>
<h2 className="text-3xl font-semibold">Save lists</h2>
<h2 className="text-3xl font-semibold">{t("myTrips")}</h2>
</div> </div>
<div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div> <div className="w-14 border-b border-neutral-200 dark:border-neutral-700"></div>
@ -63,11 +49,15 @@ const MyTrips = () => {
<Tab.Panels> <Tab.Panels>
<Tab.Panel className="mt-8"> <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"> <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) => (
{tours.length ? (
tours
.filter((_, i) => i < 8)
.map((stay: StayDataType) => (
<StayCard2 key={stay.id} data={stay} /> <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">
))
) : (
<h1 className="text-2xl">{t("noTrips")}</h1>
)}
</div> </div>
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>

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

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

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

@ -1,66 +1,269 @@
"use client"
"use client";
import React, { useEffect, useState } from "react"; 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 axiosInstance from "@/components/api/axios";
import Link from "next/link";
import { useUserContext } from "@/components/contexts/userContext";
import { useRouter } from "next/navigation"; 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 PassengersList = () => {
const [passengers , setPassenger ] = useState([])
const {user} = useUserContext()
const router = useRouter()
const [originalPassenger, setOriginalPassenger] = useState({
name: "",
passport: "",
number: "",
date: "",
image: "",
});
useEffect(() => {
if (!Object.keys(user).length) {
router.replace("/signup");
}
}, [user, router]);
const [loading, setLoading] = useState(false);
// Fetch passenger data on component mount
useEffect(() => { useEffect(() => {
axiosInstance.get("/api/account/passengers/" ,{
if (Object.keys(user).length) {
axiosInstance
.get(`/api/account/passengers/${params.id}/`, {
headers: { headers: {
Authorization : `token ${user.token}`
}
Authorization: `token ${user.token}`,
},
}) })
.then((response) => { .then((response) => {
setPassenger(response.data.results);
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);
}
}
};
}).catch((error)=>{
console.error(error);
// 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;
}
if (Object.keys(updatedFields).length === 0) {
toast.info(t("noChanges"));
return;
}
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 ( 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> </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 MdOutlineLanguage from "@/images/material-symbols-light_language.svg";
import Cookies from "js-cookie"; // Import js-cookie import Cookies from "js-cookie"; // Import js-cookie
import Image from "next/image"; import Image from "next/image";
import { useTranslation } from "react-i18next";
// Language options // Language options
const languageOptions = [ const languageOptions = [
@ -15,21 +16,21 @@ const languageOptions = [
icon: MdOutlineLanguage, icon: MdOutlineLanguage,
}, },
{ {
id: "vi",
name: "Vietnamese",
description: "Vietnam",
id: "ar",
name: "Arabic",
description: "Arabic",
icon: MdOutlineLanguage, icon: MdOutlineLanguage,
}, },
{ {
id: "fr",
name: "French",
description: "France",
id: "ru",
name: "Russian",
description: "Russian",
icon: MdOutlineLanguage, icon: MdOutlineLanguage,
}, },
{ {
id: "ar",
name: "Arabic",
description: "Arabic",
id: "id",
name: "Indonesian",
description: "Indonesian",
icon: MdOutlineLanguage, icon: MdOutlineLanguage,
}, },
]; ];
@ -44,16 +45,16 @@ function classNames(...classes: string[]) {
} }
const LangDropdown: React.FC<LangDropdownProps> = ({ 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", className = "hidden md:flex",
}) => { }) => {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [selectedItem, setSelectedItem] = useState(languageOptions[0]); const [selectedItem, setSelectedItem] = useState(languageOptions[0]);
const {t} = useTranslation("navigation");
// Update selectedItem based on the current locale in the URL // Update selectedItem based on the current locale in the URL
useEffect(() => { useEffect(() => {
const locales = ["en", "vi", "fr", "ar"];
const locales = ["en", "ru", "id", "ar"];
const segments = pathname.split("/").filter(Boolean); const segments = pathname.split("/").filter(Boolean);
const currentLocale = locales.includes(segments[0]) ? segments[0] : "en"; const currentLocale = locales.includes(segments[0]) ? segments[0] : "en";
const newSelectedItem = const newSelectedItem =
@ -66,13 +67,13 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
const segments = pathname.split("/").filter(Boolean); const segments = pathname.split("/").filter(Boolean);
// Remove the current locale if present // Remove the current locale if present
const locales = ["en", "vi", "fr", "ar"];
const locales = ["en", "ru", "id", "ar"];
if (locales.includes(segments[0])) { if (locales.includes(segments[0])) {
segments.shift(); segments.shift();
} }
// Prepend the new locale // Prepend the new locale
const newPathname = `/${locale}/${segments.join("/")}`;
const newPathname = `/${locale}/`;
// Set the locale cookie // Set the locale cookie
Cookies.set("locale", locale, { expires: 365 }); Cookies.set("locale", locale, { expires: 365 });
@ -124,7 +125,7 @@ const LangDropdown: React.FC<LangDropdownProps> = ({
) )
} }
> >
Language
{t("text-language")}
</Tab> </Tab>
</Tab.List> </Tab.List>

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

@ -50,7 +50,7 @@ interface LangDropdownProps {
} }
const LangDropdown: FC<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 ( return (
<div className="LangDropdown"> <div className="LangDropdown">

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 axiosInstance from "@/components/api/axios";
import SearchImage from "@/images/mynaui_search.svg" import SearchImage from "@/images/mynaui_search.svg"
import Image from "next/image"; import Image from "next/image";
import { useTranslation } from "react-i18next";
import { LuSearch } from "react-icons/lu";
interface Props { interface Props {
className?: string; className?: string;
} }
@ -37,7 +38,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [toursDetails, setToursDetail] = useState<Tour[]>([]); const [toursDetails, setToursDetail] = useState<Tour[]>([]);
const { tours } = useToursContext(); const { tours } = useToursContext();
const {t} = useTranslation("navigation");
useEffect(() => { useEffect(() => {
tours?.results?.forEach((item) => { tours?.results?.forEach((item) => {
axiosInstance axiosInstance
@ -78,8 +79,8 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
return ( 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> </Popover.Button>
<Transition <Transition
@ -94,7 +95,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
> >
<Popover.Panel <Popover.Panel
static static
className="absolute right-0 z-10 top-full w-screen max-w-sm "
className="absolute right-0 z-10 top-full w-screen max-w-sm rtl:left-0 rtl:right-auto"
> >
<Input <Input
value={value} value={value}
@ -104,7 +105,7 @@ const SearchDropdown: FC<Props> = ({ className = "" }) => {
ref={inputRef} ref={inputRef}
rounded="rounded-full" rounded="rounded-full"
type="text" type="text"
placeholder="Type and search"
placeholder={t("search-placeholder")}
/> />
{value.replaceAll(" ", "") && ( {value.replaceAll(" ", "") && (
<Popover.Panel className="absolute right-0 z-10 top-full w-screen max-w-sm bg-white shadow-md rounded-3xl mt-1 dark:bg-neutral-900"> <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> </div>
)) ))
) : ( ) : (
<h3 className="p-4 text-center">No Matches Found</h3>
<h3 className="p-4 text-center">{t("text-no-match")}</h3>
)} )}
</Popover.Panel> </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 { Popover } from "@headlessui/react";
import { CalendarIcon } from "@heroicons/react/24/outline"; import { CalendarIcon } from "@heroicons/react/24/outline";
import { Context } from "@/components/contexts/tourDetails"; import { Context } from "@/components/contexts/tourDetails";
import { useTranslation } from "react-i18next";
export interface StayDatesRangeInputProps { export interface StayDatesRangeInputProps {
className?: string; className?: string;
@ -15,7 +16,7 @@ const StayDatesRangeInput: FC<StayDatesRangeInputProps> = ({
fieldClassName = "[ nc-hero-field-padding ]", fieldClassName = "[ nc-hero-field-padding ]",
}) => { }) => {
const { details } = useContext(Context); const { details } = useContext(Context);
const { t } = useTranslation("form");
return ( return (
<Popover className={`StayDatesRangeInput z-10 relative flex ${className}`}> <Popover className={`StayDatesRangeInput z-10 relative flex ${className}`}>
{({ open }) => ( {({ open }) => (
@ -29,11 +30,11 @@ const StayDatesRangeInput: FC<StayDatesRangeInputProps> = ({
<span <span
className="block xl:text-lg font-semibold whitespace-nowrap " 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("-", "/")} {details?.ended_at && " - " + details?.ended_at.replaceAll("-", "/")}
</span> </span>
<span className="block mt-1 text-sm text-neutral-400 leading-none font-light"> <span className="block mt-1 text-sm text-neutral-400 leading-none font-light">
{"Start - End"}
{t("startEndDate")}
</span> </span>
</div> </div>
</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 LocationInput from "../LocationInput";
import GuestsInput from "../GuestsInput"; import GuestsInput from "../GuestsInput";
import StayDatesRangeInput from "./StayDatesRangeInput"; import StayDatesRangeInput from "./StayDatesRangeInput";
import { useTranslation } from "react-i18next";
const StaySearchForm: FC<{}> = ({}) => { const StaySearchForm: FC<{}> = ({}) => {
const {t} = useTranslation("form");
const renderForm = () => { const renderForm = () => {
return ( return (
<form dir="ltr" className="w-full relative mt-8 flex rounded-full shadow-xl dark:shadow-2xl bg-white dark:bg-neutral-800 "> <form dir="ltr" className="w-full relative mt-8 flex rounded-full shadow-xl dark:shadow-2xl bg-white dark:bg-neutral-800 ">
<LocationInput className="flex-[1.5]" />
<LocationInput desc={t("selectTour")} className="flex-[1.5]" />
<div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div> <div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div>
<StayDatesRangeInput className="flex-1" /> <StayDatesRangeInput className="flex-1" />
<div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div> <div className="self-center border-r border-slate-200 dark:border-slate-700 h-8"></div>

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

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

@ -1,5 +1,7 @@
import BackgroundSection from "@/components/BackgroundSection";
"use client";
import React from "react"; import React from "react";
import BackgroundSection from "@/components/BackgroundSection";
import Map from "@/images/map.png"; import Map from "@/images/map.png";
import appSvg2 from "@/images/appSvg2.png"; import appSvg2 from "@/images/appSvg2.png";
import appRightImgTree from "@/images/appRightImgTree.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 btnIosPng from "@/images/btn-ios.png";
import btnAndroidPng from "@/images/btn-android.png"; import btnAndroidPng from "@/images/btn-android.png";
import Image from "next/image"; import Image from "next/image";
import { useTranslation } from "react-i18next";
const SectionDowloadApp = () => { const SectionDowloadApp = () => {
const { t } = useTranslation("common");
return ( return (
<div className="relative h-[455px] pb-0 pt-24 lg:py-32 xl:py-40 2xl:py-48 flex items-center"> <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 <Image
className="object-cover object-left xl:rounded-[40px]" className="object-cover object-left xl:rounded-[40px]"
src={Map} 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>
{/* <div className="absolute right-0 top-0 max-w-2xl"> {/* <div className="absolute right-0 top-0 max-w-2xl">
<Image src={appRightImgTree} alt="" />
<Image src={appRightImgTree} alt={t("imageAltAppRightImgTree")} />
</div> </div>
<div className="absolute left-0 bottom-10 max-w-2xl"> <div className="absolute left-0 bottom-10 max-w-2xl">
<Image src={appSvg1} alt="" />
<Image src={appSvg1} alt={t("imageAltAppSvg1")} />
</div> */} </div> */}
</BackgroundSection> </BackgroundSection>
<div className="relative flex flex-col"> <div className="relative flex flex-col">
<h2 className="text-5xl md:text-6xl xl:text-7xl font-medium text-bronze"> <h2 className="text-5xl md:text-6xl xl:text-7xl font-medium text-bronze">
Mobile Apps
{t("mobileApps")}
</h2> </h2>
<span className="block mt-7 max-w-2xl text-neutral-6000"> <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> </span>
<div className="flex space-x-3 mt-10 sm:mt-14"> <div className="flex space-x-3 mt-10 sm:mt-14">
<a href="##" target="_blank" rel="noopener noreferrer"> <a href="##" target="_blank" rel="noopener noreferrer">
<Image src={btnIosPng} alt="" />
<Image src={btnIosPng} alt={t("buttonDownloadOnAppStore")} />
</a> </a>
<a href="##" target="_blank" rel="noopener noreferrer"> <a href="##" target="_blank" rel="noopener noreferrer">
<Image src={btnAndroidPng} alt="" />
<Image src={btnAndroidPng} alt={t("buttonGetItOnGooglePlay")} />
</a> </a>
</div> </div>
{/* <Image {/* <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} src={appSvg2}
alt=""
alt={t("imageAltAppSvg2")}
/> */} /> */}
<div className="block lg:hidden mt-10 max-w-2xl rounded-3xl overflow-hidden"> <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> </div>
</div> </div>

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

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

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

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

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

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

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

@ -9,12 +9,15 @@ import { useRouter } from "next/navigation";
import { useUserContext } from "@/components/contexts/userContext"; import { useUserContext } from "@/components/contexts/userContext";
import { ToastContainer, toast } from "react-toastify"; import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; 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 { BiShow, BiHide } from "react-icons/bi";
import { useTranslation } from "react-i18next";
export interface PageSignUpProps {} export interface PageSignUpProps {}
const PageSignUp: FC<PageSignUpProps> = () => { const PageSignUp: FC<PageSignUpProps> = () => {
// Initialize translation with the specific namespace
const { t } = useTranslation("form");
const router = useRouter(); const router = useRouter();
const { setForm, setMethod } = useUserContext(); const { setForm, setMethod } = useUserContext();
const phoneUtil = PhoneNumberUtil.getInstance(); const phoneUtil = PhoneNumberUtil.getInstance();
@ -34,12 +37,14 @@ const PageSignUp: FC<PageSignUpProps> = () => {
countryCode?: string; countryCode?: string;
}>({}); }>({});
// Handle country code input with maximum 3 digits
const countryCodeHandler = (e: React.ChangeEvent<HTMLInputElement>) => { const countryCodeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= 3) { if (e.target.value.length <= 3) {
setCountryCode(e.target.value); setCountryCode(e.target.value);
} }
}; };
// Validate phone number using libphonenumber
const validatePhoneNumber = () => { const validatePhoneNumber = () => {
try { try {
const number = phoneUtil.parseAndKeepRawInput( const number = phoneUtil.parseAndKeepRawInput(
@ -47,14 +52,15 @@ const PageSignUp: FC<PageSignUpProps> = () => {
countryCode countryCode
); );
if (!phoneUtil.isValidNumber(number)) { if (!phoneUtil.isValidNumber(number)) {
return "Invalid phone number.";
return t("invalidPhoneNumber");
} }
return null; return null;
} catch (error) { } catch (error) {
return "Invalid phone number.";
return t("invalidPhoneNumber");
} }
}; };
// Validate form fields
const validateForm = () => { const validateForm = () => {
const newErrors: { const newErrors: {
phoneNumber?: string; phoneNumber?: string;
@ -64,14 +70,15 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const phoneError = validatePhoneNumber(); const phoneError = validatePhoneNumber();
if (phoneError) newErrors.phoneNumber = phoneError; if (phoneError) newErrors.phoneNumber = phoneError;
if (!password) newErrors.password = "Password is required.";
if (!password) newErrors.password = t("passwordRequired");
if (password !== confirmPassword) if (password !== confirmPassword)
newErrors.confirmPassword = "Passwords do not match.";
newErrors.confirmPassword = t("confirmPasswordMismatch");
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; // Return true if no errors return Object.keys(newErrors).length === 0; // Return true if no errors
}; };
// Display error toasts whenever errors change
useEffect(() => { useEffect(() => {
Object.values(errors).forEach((error) => { Object.values(errors).forEach((error) => {
toast.error(error, { toast.error(error, {
@ -85,13 +92,15 @@ const PageSignUp: FC<PageSignUpProps> = () => {
theme: "light", theme: "light",
}); });
}); });
}, [errors]);
}, [errors, t]);
// Handle form submission
const submitHandler = async () => { const submitHandler = async () => {
setLoading(true); setLoading(true);
setErrors({}); // Clear previous errors setErrors({}); // Clear previous errors
if (!validateForm()) { if (!validateForm()) {
setLoading(false);
return; // Prevent submission if there are validation errors 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}` `/api/account/verfication/?range_phone=${countryCode}&phone_number=${phoneNumber}`
); );
const form = {
const formData = {
verification_methodes: response.data.verification_method, verification_methodes: response.data.verification_method,
method: "reset", method: "reset",
countryCode, countryCode,
@ -109,20 +118,21 @@ const PageSignUp: FC<PageSignUpProps> = () => {
confirmPassword, confirmPassword,
}; };
setForm(form);
setForm(formData);
router.push("/signup/methodes"); router.push("/signup/methodes");
} catch (error) {
} catch (error: any) {
console.error("Error fetching data:", error); 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 { } finally {
setLoading(false); setLoading(false);
} }
@ -132,20 +142,21 @@ const PageSignUp: FC<PageSignUpProps> = () => {
<div className={`nc-PageSignUp`}> <div className={`nc-PageSignUp`}>
<div className="container mb-24 lg:mb-32"> <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"> <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> </h2>
<div className="max-w-md mx-auto space-y-6"> <div className="max-w-md mx-auto space-y-6">
<form <form
className="grid grid-cols-1 gap-6" className="grid grid-cols-1 gap-6"
onSubmit={(e) => e.preventDefault()} onSubmit={(e) => e.preventDefault()}
> >
{/* Phone Number Field */}
<label className="block"> <label className="block">
<span className="text-neutral-800 dark:text-neutral-200"> <span className="text-neutral-800 dark:text-neutral-200">
Phone Number
{t("phoneNumber")}
</span> </span>
<div <div
className={`flex items-center mt-1 rounded-2xl ${ className={`flex items-center mt-1 rounded-2xl ${
errors.countryCode || errors.phoneNumber
errors.phoneNumber || errors.countryCode
? "border border-red-600" ? "border border-red-600"
: "border border-neutral-200" : "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`}
@ -157,9 +168,10 @@ const PageSignUp: FC<PageSignUpProps> = () => {
value={countryCode} value={countryCode}
onChange={countryCodeHandler} onChange={countryCodeHandler}
type="number" type="number"
placeholder="000"
placeholder={t("enterPhoneNumber")}
maxLength={3} 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" 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> <span className="px-2 text-neutral-500">|</span>
<input <input
@ -167,65 +179,88 @@ const PageSignUp: FC<PageSignUpProps> = () => {
onChange={(e) => setPhoneNumber(e.target.value)} onChange={(e) => setPhoneNumber(e.target.value)}
type="number" 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" 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> </div>
</label> </label>
{/* Password Field */}
<label className="block relative"> <label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200"> <span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
Password
{t("password")}
</span> </span>
<Input <Input
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
type={`${showPassword ? "text" : "password"}`} type={`${showPassword ? "text" : "password"}`}
className={`mt-1 ${errors.password ? "border-red-600" : ""}`} className={`mt-1 ${errors.password ? "border-red-600" : ""}`}
placeholder={t("enterPassword")}
aria-label={t("password")}
/> />
{showPassword ? ( {showPassword ? (
<BiShow <BiShow
size={20} 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 <BiHide
size={20} 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> </label>
{/* Confirm Password Field */}
<label className="block relative"> <label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200"> <span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200">
Confirm Password
{t("confirmPassword")}
</span> </span>
<Input <Input
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
type={`${showConfirm ? "text" : "password"}`} 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 ? ( {showConfirm ? (
<BiShow <BiShow
size={20} 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 <BiHide
size={20} 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> </label>
{/* Continue Button */}
<ButtonPrimary loading={loading} onClick={submitHandler}> <ButtonPrimary loading={loading} onClick={submitHandler}>
Continue
{t("submit")}
</ButtonPrimary> </ButtonPrimary>
</form> </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>
</div> </div>
<ToastContainer />
</div> </div>
); );
}; };

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

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { FC, useState, useEffect } from "react";
import React, { FC, useEffect, useState } from "react";
import Input from "@/shared/Input"; import Input from "@/shared/Input";
import ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonPrimary from "@/shared/ButtonPrimary";
import Link from "next/link"; import Link from "next/link";
@ -11,10 +11,12 @@ import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import { PhoneNumberUtil } from "google-libphonenumber"; import { PhoneNumberUtil } from "google-libphonenumber";
import { BiShow, BiHide } from "react-icons/bi"; import { BiShow, BiHide } from "react-icons/bi";
import { useTranslation } from "react-i18next";
export interface PageLoginProps {} export interface PageLoginProps {}
const PageLogin: FC<PageLoginProps> = () => { const PageLogin: FC<PageLoginProps> = () => {
const { t } = useTranslation("form");
const { user, setUser } = useUserContext(); const { user, setUser } = useUserContext();
const router = useRouter(); const router = useRouter();
const phoneUtil = PhoneNumberUtil.getInstance(); const phoneUtil = PhoneNumberUtil.getInstance();
@ -58,11 +60,11 @@ const PageLogin: FC<PageLoginProps> = () => {
try { try {
const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + countryCode + phoneNumber, countryCode); const parsedNumber = phoneUtil.parseAndKeepRawInput("+" + countryCode + phoneNumber, countryCode);
if (!phoneUtil.isValidNumber(parsedNumber)) { if (!phoneUtil.isValidNumber(parsedNumber)) {
return "Invalid phone number.";
return t("invalidPhoneNumber");
} }
return null; return null;
} catch (error) { } catch (error) {
return "Invalid phone number format.";
return t("invalidPhoneNumberFormat");
} }
}; };
@ -75,7 +77,7 @@ const PageLogin: FC<PageLoginProps> = () => {
} }
if (!password) { if (!password) {
newErrors.password = "Password is required.";
newErrors.password = t("passwordRequired");
} }
setErrors(newErrors); setErrors(newErrors);
@ -98,44 +100,23 @@ const PageLogin: FC<PageLoginProps> = () => {
}); });
if (response.status === 201) { if (response.status === 201) {
toast.success("Login successful!");
toast.success(t("loginSuccessful"));
setUser(response.data); setUser(response.data);
router.back(); // Redirect to home or any other page after successful login router.back(); // Redirect to home or any other page after successful login
} else { } 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 { } 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(err.message);
}
}); });
} else {
toast.error(error.message || t("errorOccurred"));
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -146,12 +127,12 @@ const PageLogin: FC<PageLoginProps> = () => {
<div className="nc-PageLogin"> <div className="nc-PageLogin">
<div className="container mb-24 lg:mb-32"> <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"> <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> </h2>
<div className="max-w-md mx-auto space-y-6"> <div className="max-w-md mx-auto space-y-6">
<form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}> <form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}>
<label className="block"> <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 <div
className={`flex items-center mt-1 rounded-2xl ${ className={`flex items-center mt-1 rounded-2xl ${
errors.phoneNumber ? "border border-red-600" : "border border-neutral-200" errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"
@ -162,7 +143,7 @@ const PageLogin: FC<PageLoginProps> = () => {
value={countryCode} value={countryCode}
onChange={countryCodeHandler} onChange={countryCodeHandler}
type="number" type="number"
placeholder="000"
placeholder={t("phoneNumberCountryCodePlaceholder")}
maxLength={3} 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" 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} value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)} onChange={(e) => setPhoneNumber(e.target.value)}
type="number" 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" 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> </div>
@ -178,9 +160,9 @@ const PageLogin: FC<PageLoginProps> = () => {
<label className="block relative"> <label className="block relative">
<span className="flex justify-between items-center text-neutral-800 dark:text-neutral-200"> <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"> <Link href="/forgot-password" className="text-sm underline font-medium">
Forgot password?
{t("forgotPassword")}
</Link> </Link>
</span> </span>
<Input <Input
@ -188,34 +170,38 @@ const PageLogin: FC<PageLoginProps> = () => {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
type={`${showPassword ? "text" : "password"}`} type={`${showPassword ? "text" : "password"}`}
className={`mt-1 ${errors.password ? "border-red-600" : "border-neutral-300"}`} className={`mt-1 ${errors.password ? "border-red-600" : "border-neutral-300"}`}
placeholder={t("enterPassword")}
/> />
{showPassword ? ( {showPassword ? (
<BiShow <BiShow
size={20} 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)} onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("showPassword")}
/> />
) : ( ) : (
<BiHide <BiHide
size={20} 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)} onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("hidePassword")}
/> />
)} )}
</label> </label>
<ButtonPrimary onClick={submitHandler} loading={loading}> <ButtonPrimary onClick={submitHandler} loading={loading}>
Continue
{t("continue")}
</ButtonPrimary> </ButtonPrimary>
</form> </form>
<span className="block text-center text-neutral-700 dark:text-neutral-300"> <span className="block text-center text-neutral-700 dark:text-neutral-300">
<Link href="/signup" className="text-bronze font-semibold"> <Link href="/signup" className="text-bronze font-semibold">
Create an account
{t("createAccount")}
</Link> </Link>
</span> </span>
</div> </div>
</div> </div>
<ToastContainer />
</div> </div>
); );
}; };

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

@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState, ChangeEvent } from "react"; import { useEffect, useState, ChangeEvent } from "react";
import { FaWhatsapp } from "react-icons/fa"; import { FaWhatsapp } from "react-icons/fa";
import { MdOutlineTextsms } from "react-icons/md"; import { MdOutlineTextsms } from "react-icons/md";
import { useTranslation } from "react-i18next";
interface Form { interface Form {
phoneNumber: string; phoneNumber: string;
@ -19,32 +20,33 @@ interface Form {
} }
interface EnabledMethods { interface EnabledMethods {
watsapp: boolean;
whatsapp: boolean; // Corrected spelling from 'watsapp' to 'whatsapp'
sms: boolean; sms: boolean;
} }
function SelectMethods() { function SelectMethods() {
const { t } = useTranslation("form");
const router = useRouter(); const router = useRouter();
const {form, setForm } = useUserContext()
const { form, setForm } = useUserContext();
const [selectedMethod, setSelectedMethod] = useState<string>(""); const [selectedMethod, setSelectedMethod] = useState<string>("");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); 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 // Initialize enabled methods based on the form
useEffect(() => { useEffect(() => {
setEnabled({ setEnabled({
watsapp: form?.verification_methodes?.includes("watsapp") ?? false,
whatsapp: form?.verification_methodes?.includes("whatsapp") ?? false,
sms: form?.verification_methodes?.includes("sms") ?? false, sms: form?.verification_methodes?.includes("sms") ?? false,
}); });
}, [form]); }, [form]);
// Handle method change // Handle method change
const handleMethodChange = (e: ChangeEvent<HTMLInputElement>) => { const handleMethodChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!enabled.watsapp && !enabled.sms) {
if (!enabled.whatsapp && !enabled.sms) {
setSelectedMethod(""); setSelectedMethod("");
} else if (!enabled.watsapp) {
} else if (!enabled.whatsapp) {
setSelectedMethod("sms"); setSelectedMethod("sms");
} else if (!enabled.sms) { } else if (!enabled.sms) {
setSelectedMethod("whatsapp"); setSelectedMethod("whatsapp");
@ -76,7 +78,7 @@ function SelectMethods() {
if (response.status === 202) { if (response.status === 202) {
setForm((prev: Record<string, any>) => ({ ...prev, verification_method: selectedMethod })); setForm((prev: Record<string, any>) => ({ ...prev, verification_method: selectedMethod }));
setLoading(false); setLoading(false);
router.replace("signup/otp-code");
router.replace("/signup/otp-code");
} }
} }
// Handle Password Reset // Handle Password Reset
@ -91,11 +93,21 @@ function SelectMethods() {
if (response.status === 202) { if (response.status === 202) {
setForm((prev: Record<string, any>) => ({ ...prev, verification_method: selectedMethod })); setForm((prev: Record<string, any>) => ({ ...prev, verification_method: selectedMethod }));
setLoading(false); setLoading(false);
router.replace("signup/otp-code");
router.replace("/signup/otp-code");
} }
} }
} catch (error: any) { } 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); setLoading(false);
} }
}; };
@ -103,14 +115,14 @@ function SelectMethods() {
return ( return (
<div className="w-[550px] container mb-24 lg:mb-32 p-4 space-y-4"> <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"> <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> </h2>
<form className="grid grid-cols-1 gap-6"> <form className="grid grid-cols-1 gap-6">
{/* WhatsApp Option */} {/* WhatsApp Option */}
<div <div
className={`${ 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`} } 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"> <div className="flex items-center">
@ -118,10 +130,10 @@ function SelectMethods() {
<label <label
htmlFor="whatsapp" htmlFor="whatsapp"
className={`${ className={`${
!enabled.watsapp ? "cursor-not-allowed" : "cursor-pointer"
!enabled.whatsapp ? "cursor-not-allowed" : "cursor-pointer"
} text-neutral-800 dark:text-neutral-200 font-medium`} } text-neutral-800 dark:text-neutral-200 font-medium`}
> >
Send via WhatsApp
{t("sendViaWhatsApp")}
</label> </label>
</div> </div>
<input <input
@ -131,7 +143,7 @@ function SelectMethods() {
value="whatsapp" value="whatsapp"
checked={selectedMethod === "whatsapp"} checked={selectedMethod === "whatsapp"}
onChange={handleMethodChange} onChange={handleMethodChange}
disabled={!enabled.watsapp}
disabled={!enabled.whatsapp}
className="cursor-pointer form-radio accent-black text-black focus:ring-primary-500 focus:ring-2" className="cursor-pointer form-radio accent-black text-black focus:ring-primary-500 focus:ring-2"
/> />
</div> </div>
@ -150,7 +162,7 @@ function SelectMethods() {
!enabled.sms ? "cursor-not-allowed" : "cursor-pointer" !enabled.sms ? "cursor-not-allowed" : "cursor-pointer"
} text-neutral-800 dark:text-neutral-200 font-medium`} } text-neutral-800 dark:text-neutral-200 font-medium`}
> >
Send via SMS
{t("sendViaSMS")}
</label> </label>
</div> </div>
<input <input
@ -175,8 +187,9 @@ function SelectMethods() {
e.preventDefault(); e.preventDefault();
handleSubmit(); handleSubmit();
}} }}
disabled={loading || !selectedMethod}
> >
Continue
{t("continue")}
</ButtonPrimary> </ButtonPrimary>
</form> </form>
</div> </div>

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

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

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

@ -11,10 +11,12 @@ import { useRouter } from "next/navigation";
import { ToastContainer, toast } from "react-toastify"; import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import { BiShow, BiHide } from "react-icons/bi"; import { BiShow, BiHide } from "react-icons/bi";
import { useTranslation } from "react-i18next";
export interface PageSignUpProps {} export interface PageSignUpProps {}
const PageSignUp: FC<PageSignUpProps> = () => { const PageSignUp: FC<PageSignUpProps> = () => {
const { t } = useTranslation("form");
const router = useRouter(); const router = useRouter();
const { user, setForm } = useUserContext(); const { user, setForm } = useUserContext();
const [name, setName] = useState<string>(""); const [name, setName] = useState<string>("");
@ -35,12 +37,14 @@ const PageSignUp: FC<PageSignUpProps> = () => {
} }
}; };
// Redirect to home if the user is already logged in
useEffect(() => { useEffect(() => {
if (Object.keys(user).length) { if (Object.keys(user).length) {
router.replace("/"); router.replace("/");
} }
}, [user, router]); }, [user, router]);
// Display validation errors as toast notifications
useEffect(() => { useEffect(() => {
Object.values(errors).forEach((error) => { Object.values(errors).forEach((error) => {
toast.error(error, { toast.error(error, {
@ -58,12 +62,12 @@ const PageSignUp: FC<PageSignUpProps> = () => {
const submitHandler = async () => { const submitHandler = async () => {
const form = { 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", method: "register",
}; };
@ -71,14 +75,36 @@ const PageSignUp: FC<PageSignUpProps> = () => {
setLoading(true); setLoading(true);
setForm(form); setForm(form);
try { 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) { } 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 { } finally {
setLoading(false); setLoading(false);
} }
@ -89,22 +115,25 @@ const PageSignUp: FC<PageSignUpProps> = () => {
<div className={`nc-PageSignUp`}> <div className={`nc-PageSignUp`}>
<div className="container mb-24 lg:mb-32"> <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"> <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> </h2>
<div className="max-w-md mx-auto space-y-6"> <div className="max-w-md mx-auto space-y-6">
<form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}> <form className="grid grid-cols-1 gap-6" onSubmit={(e) => e.preventDefault()}>
{/* Full Name Field */}
<label className="block"> <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 <Input
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
type="text" type="text"
placeholder="Full Name"
placeholder={t("enterFullName")}
className={`mt-1 ${errors.name ? "border-red-600" : "border-neutral-300"}`} className={`mt-1 ${errors.name ? "border-red-600" : "border-neutral-300"}`}
/> />
</label> </label>
{/* Phone Number Field */}
<label className="block"> <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 <div
className={`flex items-center mt-1 rounded-2xl ${ className={`flex items-center mt-1 rounded-2xl ${
errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200" errors.countryCode || errors.phoneNumber ? "border border-red-600" : "border border-neutral-200"
@ -124,67 +153,92 @@ const PageSignUp: FC<PageSignUpProps> = () => {
value={phoneNumber} value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)} onChange={(e) => setPhoneNumber(e.target.value)}
type="number" 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" 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> </div>
</label> </label>
{/* Password Field */}
<label className="block relative"> <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 <Input
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder={t("enterPassword")}
className={`mt-1 ${errors.password ? "border-red-600" : "border-neutral-300"}`} className={`mt-1 ${errors.password ? "border-red-600" : "border-neutral-300"}`}
/> />
{showPassword ? ( {showPassword ? (
<BiShow <BiShow
size={20} 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)} onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("hidePassword")}
/> />
) : ( ) : (
<BiHide <BiHide
size={20} 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)} onClick={() => setShowPassword((prev) => !prev)}
aria-label={t("showPassword")}
/> />
)} )}
</label> </label>
{/* Confirm Password Field */}
<label className="block relative"> <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 <Input
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
type={showConfirmPassword ? "text" : "password"} type={showConfirmPassword ? "text" : "password"}
placeholder={t("enterConfirmPassword")}
className={`mt-1 ${errors.confirmPassword ? "border-red-600" : "border-neutral-300"}`} className={`mt-1 ${errors.confirmPassword ? "border-red-600" : "border-neutral-300"}`}
/> />
{showConfirmPassword ? ( {showConfirmPassword ? (
<BiShow <BiShow
size={20} 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)} onClick={() => setShowConfirmPassword((prev) => !prev)}
aria-label={t("hideConfirmPassword")}
/> />
) : ( ) : (
<BiHide <BiHide
size={20} 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)} onClick={() => setShowConfirmPassword((prev) => !prev)}
aria-label={t("showConfirmPassword")}
/> />
)} )}
</label> </label>
{/* Display Failed Message */}
{failed && <p className="text-xs text-red-600">{failed}</p>} {failed && <p className="text-xs text-red-600">{failed}</p>}
{/* Submit Button */}
<ButtonPrimary loading={loading} onClick={submitHandler} disabled={loading}> <ButtonPrimary loading={loading} onClick={submitHandler} disabled={loading}>
Continue
{t("continue")}
</ButtonPrimary> </ButtonPrimary>
</form> </form>
{/* Signup Link */}
<span className="not-italic block text-center text-neutral-700 dark:text-neutral-300"> <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"> <Link href="/login" className="text-primary-600 font-semibold underline">
Sign in
{t("signIn")}
</Link> </Link>
</span> </span>
</div> </div>
</div> </div>
<ToastContainer />
</div> </div>
); );
}; };

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

@ -1,14 +1,17 @@
"use client"; "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 { StayDataType } from "@/data/types";
import { DEMO_STAY_LISTINGS } from "@/data/listings";
import TabFilters from "./TabFilters"; 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 { export interface SectionGridFilterCardProps {
className?: string; className?: string;
@ -19,27 +22,29 @@ const SectionGridFilterCard: FC<SectionGridFilterCardProps> = ({
className = "", className = "",
data = DEMO_STAY_LISTINGS, data = DEMO_STAY_LISTINGS,
}) => { }) => {
const { t } = useTranslation("common");
const { countries, tours } = useToursContext(); const { countries, tours } = useToursContext();
const [countryTours, setCountryTours] = useState(tours.results || []); const [countryTours, setCountryTours] = useState(tours.results || []);
const [checked, setChecked] = useState<{ [key: string]: boolean }>({}); const [checked, setChecked] = useState<{ [key: string]: boolean }>({});
const searchParams = useSearchParams()
const searchParams = useSearchParams();
console.log(searchParams); console.log(searchParams);
// Get the list of selected countries // 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(() => { useEffect(() => {
const country = searchParams.get("country")
if (searchParams.has("country")){
const country = searchParams.get("country");
if (searchParams.has("country") && country) {
setChecked({ setChecked({
[country] : true
})
[country]: true,
});
} }
} , [searchParams])
console.log(checked);
}, [searchParams]);
console.log(checked);
useEffect(() => { useEffect(() => {
if (!tours.results) return; if (!tours.results) return;
@ -60,19 +65,22 @@ console.log(checked);
setCountryTours(filteredTours); setCountryTours(filteredTours);
} }
}, [checked, countries, tours.results]);
}, [checked, countries, tours.results, filteredCountries]);
return ( 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
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>
<div className="mb-8 lg:mb-11"> <div className="mb-8 lg:mb-11">
@ -82,7 +90,7 @@ console.log(checked);
{countryTours.length > 0 ? ( {countryTours.length > 0 ? (
countryTours.map((stay) => <StayCard2 key={stay.id} data={stay} />) countryTours.map((stay) => <StayCard2 key={stay.id} data={stay} />)
) : ( ) : (
<h2>No tours Available</h2>
<h2>{t("sectionGridFilterCard.noToursAvailable")}</h2>
)} )}
</div> </div>
</div> </div>

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

@ -90,9 +90,9 @@ const ListingStayDetailPage: FC = () => {
{itineraries.map((item, index) => ( {itineraries.map((item, index) => (
<div key={item.id} className="flex"> <div key={item.id} className="flex">
<div className="mt-2"> <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 && ( {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>
<div className="p-5 rounded-3xl mb-4 border-2"> <div className="p-5 rounded-3xl mb-4 border-2">
@ -131,7 +131,11 @@ const ListingStayDetailPage: FC = () => {
const renderSidebar = () => { const renderSidebar = () => {
const total = const total =
details?.final_price && passengers 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", style: "currency",
currency: "USD", currency: "USD",
}) })
@ -153,8 +157,13 @@ const ListingStayDetailPage: FC = () => {
</div> </div>
)} )}
{/* <StartRating /> */} {/* <StartRating /> */}
{<Badge className="!text-base" name={details?.status} color={details?.status} />}
{
<Badge
className="!text-base"
name={details?.status}
color={details?.status}
/>
}
</div> </div>
{/* Booking Form */} {/* Booking Form */}
@ -173,7 +182,7 @@ const ListingStayDetailPage: FC = () => {
{/* Reserve Button */} {/* Reserve Button */}
<ButtonPrimary <ButtonPrimary
className={ className={
details?.status === "AVAILABLE"
details?.status === "AVAILABLE" && details.is_access && totalGuests
? "" ? ""
: "opacity-60 pointer-events-none" : "opacity-60 pointer-events-none"
} }
@ -213,7 +222,7 @@ const ListingStayDetailPage: FC = () => {
<header className="rounded-md sm:rounded-xl"> <header className="rounded-md sm:rounded-xl">
<div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2"> <div className="relative grid grid-cols-3 sm:grid-cols-4 gap-1 sm:gap-2">
{/* Main Image */} {/* 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 && ( {details && (
<Image <Image
fill fill
@ -265,7 +274,7 @@ const ListingStayDetailPage: FC = () => {
{/* Main content section */} {/* Main content section */}
<main className="relative z-10 mt-11 flex flex-col lg:flex-row"> <main className="relative z-10 mt-11 flex flex-col lg:flex-row">
{/* Left Content */} {/* 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()} {renderSectionDetails()}
{renderTourFeatures()} {renderTourFeatures()}
{renderItinerarySection()} {renderItinerarySection()}

11
src/app/globals.css

@ -13,9 +13,14 @@
} }
.hero-image{ .hero-image{
left: -450px;
left: -550px;
} }
@media(max-width : 1600px) {
.hero-image{
left: -350px;
}
}
@media(max-width : 1400px) { @media(max-width : 1400px) {
.hero-image{ .hero-image{
left: -400px; left: -400px;
@ -27,3 +32,7 @@
top: 0px; top: 0px;
} }
} }
.container{
max-width: 1440px;
}

21
src/components/CardCategory3.tsx

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

82
src/components/Footer.tsx

@ -1,10 +1,12 @@
"use client"; "use client";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import Logo from "@/shared/Logo"; import Logo from "@/shared/Logo";
import SocialsList1 from "@/shared/SocialsList1"; import SocialsList1 from "@/shared/SocialsList1";
import { CustomLink } from "@/data/types"; import { CustomLink } from "@/data/types";
import React from "react";
import FooterNav from "./FooterNav"; import FooterNav from "./FooterNav";
import Image from "next/image";
export interface WidgetFooterMenu { export interface WidgetFooterMenu {
id: string; id: string;
@ -12,65 +14,32 @@ export interface WidgetFooterMenu {
menus: CustomLink[]; menus: CustomLink[];
} }
const Footer: React.FC = () => {
const { t } = useTranslation("footer");
const widgetMenus: WidgetFooterMenu[] = [ const widgetMenus: WidgetFooterMenu[] = [
{ {
id: "5", 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",
title: "Quick Links",
menus: [ menus: [
{ href: "#", label: "Best practices" },
{ href: "#", label: "Support" },
{ href: "#", label: "Developers" },
{ href: "#", label: "Learn design" },
{ href: "#", label: "Releases" },
{ 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") },
], ],
},
{
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 renderWidgetMenuItem = (menu: WidgetFooterMenu, index: number) => { const renderWidgetMenuItem = (menu: WidgetFooterMenu, index: number) => {
return ( 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"> <h2 className="font-semibold text-neutral-700 dark:text-neutral-200">
{menu.title}
{t(`widgetMenus.${menu.title}.title`)}
</h2> </h2>
<ul className="mt-5 space-y-4"> <ul className="mt-5 space-y-4">
{menu.menus.map((item, index) => (
<li key={index}>
{menu.menus.map((item, idx) => (
<li key={idx}>
<a <a
key={index}
className="text-neutral-6000 dark:text-neutral-300 hover:text-black dark:hover:text-white" className="text-neutral-6000 dark:text-neutral-300 hover:text-black dark:hover:text-white"
href={item.href} href={item.href}
> >
@ -88,16 +57,25 @@ const Footer: React.FC = () => {
<FooterNav /> <FooterNav />
<div className="nc-Footer relative py-24 lg:py-28 border-t border-neutral-200 dark:border-neutral-700"> <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 /> <Logo />
</div> </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" />
<p className="font-extralight text-sm text-[#A2ABB8]">
{t("footerNav.description")}
</p>
</div> </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> </div>
{widgetMenus.map(renderWidgetMenuItem)} {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>
</div> </div>
</> </>

10
src/components/HeaderFilter.tsx

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

85
src/components/SectionClientSay.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import Heading from "@/shared/Heading";
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import Heading from "@/shared/Heading";
import clientSayMain from "@/images/clientSayMain.png"; import clientSayMain from "@/images/clientSayMain.png";
import clientSay1 from "@/images/clientSay1.png"; import clientSay1 from "@/images/clientSay1.png";
import clientSay2 from "@/images/clientSay2.png"; import clientSay2 from "@/images/clientSay2.png";
@ -16,40 +16,18 @@ import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import Image from "next/image"; import Image from "next/image";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { variants } from "@/utils/animationVariants"; import { variants } from "@/utils/animationVariants";
import { useTranslation } from "react-i18next";
export interface SectionClientSayProps { export interface SectionClientSayProps {
className?: string; className?: string;
data?: typeof DEMO_DATA; 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> = ({ const SectionClientSay: FC<SectionClientSayProps> = ({
className = "", className = "",
data = DEMO_DATA, data = DEMO_DATA,
}) => { }) => {
const { t } = useTranslation("common");
const [index, setIndex] = useState(0); const [index, setIndex] = useState(0);
const [direction, setDirection] = useState(0); const [direction, setDirection] = useState(0);
@ -84,32 +62,32 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
<Image <Image
className="absolute top-9 -left-20" className="absolute top-9 -left-20"
src={clientSay1} src={clientSay1}
alt="client 1"
alt={t("sectionClientSay.imageAltClient1")}
/> />
<Image <Image
className="absolute bottom-[100px] right-full mr-40" className="absolute bottom-[100px] right-full mr-40"
src={clientSay2} src={clientSay2}
alt="client 2"
alt={t("sectionClientSay.imageAltClient2")}
/> />
<Image <Image
className="absolute top-full left-[140px]" className="absolute top-full left-[140px]"
src={clientSay3} src={clientSay3}
alt="client 3"
alt={t("sectionClientSay.imageAltClient3")}
/> />
<Image <Image
className="absolute -bottom-10 right-[140px]" className="absolute -bottom-10 right-[140px]"
src={clientSay4} src={clientSay4}
alt="client 4"
alt={t("sectionClientSay.imageAltClient4")}
/> />
<Image <Image
className="absolute left-full ml-32 bottom-[80px]" className="absolute left-full ml-32 bottom-[80px]"
src={clientSay5} src={clientSay5}
alt="client 5"
alt={t("sectionClientSay.imageAltClient5")}
/> />
<Image <Image
className="absolute -right-10 top-10" className="absolute -right-10 top-10"
src={clientSay6} src={clientSay6}
alt="client 6"
alt={t("sectionClientSay.imageAltClient6")}
/> />
</div> </div>
); );
@ -117,22 +95,22 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
return ( return (
<div className={`nc-SectionClientSay relative ${className}`}> <div className={`nc-SectionClientSay relative ${className}`}>
<Heading desc="Let's see what people think of Chisfis" isCenter>
Good news from far away
<Heading className="" desc="" isCenter>
{t("sectionClientSay.heading")}
</Heading> </Heading>
<div className="relative md:mb-16 max-w-2xl mx-auto"> <div className="relative md:mb-16 max-w-2xl mx-auto">
{renderBg()} {renderBg()}
<Image className="mx-auto" src={clientSayMain} alt="" />
<Image className="mx-auto" src={clientSayMain} alt={t("sectionClientSay.imageAltMain")} />
<div className={`mt-12 lg:mt-16 relative`}> <div className={`mt-12 lg:mt-16 relative`}>
<Image <Image
className="opacity-50 md:opacity-100 absolute -mr-16 lg:mr-3 right-full top-1" className="opacity-50 md:opacity-100 absolute -mr-16 lg:mr-3 right-full top-1"
src={quotationImg} src={quotationImg}
alt=""
alt={t("sectionClientSay.imageAltQuotation1")}
/> />
<Image <Image
className="opacity-50 md:opacity-100 absolute -ml-16 lg:ml-3 left-full top-1" className="opacity-50 md:opacity-100 absolute -ml-16 lg:ml-3 left-full top-1"
src={quotationImg2} src={quotationImg2}
alt=""
alt={t("sectionClientSay.imageAltQuotation2")}
/> />
<MotionConfig <MotionConfig
@ -152,19 +130,19 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
variants={variants(200, 1)} variants={variants(200, 1)}
initial="enter" initial="enter"
animate="center" animate="center"
// exit="exit"
exit="exit"
className="inline-flex flex-col items-center text-center whitespace-normal" className="inline-flex flex-col items-center text-center whitespace-normal"
> >
<> <>
<span className="block text-2xl"> <span className="block text-2xl">
{currentItem.content}
{t(`sectionClientSay.testimonials.${index}.content`)}
</span> </span>
<span className="block mt-8 text-2xl font-semibold"> <span className="block mt-8 text-2xl font-semibold">
{currentItem.clientName}
{t(`sectionClientSay.testimonials.${index}.clientName`)}
</span> </span>
<div className="flex items-center space-x-2 text-lg mt-2 text-neutral-400"> <div className="flex items-center space-x-2 text-lg mt-2 text-neutral-400">
<MapPinIcon className="h-5 w-5" /> <MapPinIcon className="h-5 w-5" />
<span>{currentItem.clientAddress}</span>
<span>{t(`sectionClientSay.testimonials.${index}.clientAddress`)}</span>
</div> </div>
</> </>
</motion.div> </motion.div>
@ -178,6 +156,7 @@ const SectionClientSay: FC<SectionClientSayProps> = ({
}`} }`}
onClick={() => changeItemId(i)} onClick={() => changeItemId(i)}
key={i} key={i}
aria-label={t(`sectionClientSay.testimonials.${i}.clientName`)}
/> />
))} ))}
</div> </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; export default SectionClientSay;

29
src/components/SectionCustomTour.tsx

@ -1,37 +1,46 @@
import BackgroundSection from "@/components/BackgroundSection";
"use client";
import React from "react"; import React from "react";
import BackgroundSection from "@/components/BackgroundSection";
import BackgroundImage from "@/images/Frame-412.webp"; import BackgroundImage from "@/images/Frame-412.webp";
import Image from "next/image"; import Image from "next/image";
import ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonPrimary from "@/shared/ButtonPrimary";
import { useTranslation } from "react-i18next";
const SectionDownloadApp = () => { const SectionDownloadApp = () => {
const { t } = useTranslation("common");
return ( 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 */} {/* Background Image */}
<Image <Image
className="absolute inset-0 object-cover object-right z-0" // Dim the background image for contrast 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} src={BackgroundImage}
fill fill
quality={100} quality={100}
/> />
{/* Content Wrapper */} {/* 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"> <h1 className="text-white text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
Create
{t("create")}
</h1> </h1>
<h1 className="text-white text-4xl md:text-5xl lg:text-6xl font-bold leading-tight"> <h1 className="text-white text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
Custom Tour
{t("customTour")}
</h1> </h1>
<p className="text-white text-lg mt-4 opacity-80"> <p className="text-white text-lg mt-4 opacity-80">
Create your personalized tour and design the
{t("createPersonalizedTourLine1")}
</p> </p>
<p className="text-white text-lg mt-4 opacity-80"> <p className="text-white text-lg mt-4 opacity-80">
perfect travel experience tailored to your preferences.
{t("createPersonalizedTourLine2")}
</p> </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> </div>
</BackgroundSection> </BackgroundSection>
); );

31
src/components/SectionGridFeaturePlaces.tsx

@ -1,6 +1,6 @@
"use client"; "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 { StayDataType } from "@/data/types";
import ButtonPrimary from "@/shared/ButtonPrimary"; import ButtonPrimary from "@/shared/ButtonPrimary";
import HeaderFilter from "./HeaderFilter"; import HeaderFilter from "./HeaderFilter";
@ -8,11 +8,12 @@ import StayCard2 from "./StayCard2";
import { AnimatePresence, motion, MotionConfig } from "framer-motion"; import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { useWindowSize } from "react-use"; import { useWindowSize } from "react-use";
import { Context, useToursContext } from "./contexts/tourDetails";
import { useToursContext } from "./contexts/tourDetails";
import { variants } from "@/utils/animationVariants"; import { variants } from "@/utils/animationVariants";
import PrevBtn from "./PrevBtn"; import PrevBtn from "./PrevBtn";
import NextBtn from "./NextBtn"; import NextBtn from "./NextBtn";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
export interface SectionGridFeaturePlacesProps { export interface SectionGridFeaturePlacesProps {
stayListings?: StayDataType[]; stayListings?: StayDataType[];
@ -26,21 +27,21 @@ export interface SectionGridFeaturePlacesProps {
const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
gridClass = "", 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 [countryTours, setCountryTours] = useState<any>([]);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0); const [direction, setDirection] = useState(0);
const [numberOfItems, setNumberOfItems] = useState(0); const [numberOfItems, setNumberOfItems] = useState(0);
const router = useRouter(); const router = useRouter();
const { t } = useTranslation("common");
const windowWidth = useWindowSize().width; const windowWidth = useWindowSize().width;
useEffect(() => { useEffect(() => {
handleChange("All");
}, [tours, countries]);
handleChange(t("All"));
}, [tours, countries, t]);
useEffect(() => { useEffect(() => {
if (windowWidth < 320) { if (windowWidth < 320) {
@ -60,7 +61,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
}, [windowWidth]); }, [windowWidth]);
const handleChange = (item: string) => { const handleChange = (item: string) => {
if (item === "All") {
if (item === t("All")) {
setCountryTours(tours.results); setCountryTours(tours.results);
} else { } else {
const selected = countries.find((country) => country.name === item); const selected = countries.find((country) => country.name === item);
@ -102,10 +103,10 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
return ( return (
<div className="nc-SectionGridFeaturePlaces relative"> <div className="nc-SectionGridFeaturePlaces relative">
<HeaderFilter <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)} onClickTab={(item) => handleChange(item)}
/> />
<MotionConfig <MotionConfig
@ -128,7 +129,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
initial={{ x: `${(currentIndex - 1) * -100}%` }} initial={{ x: `${(currentIndex - 1) * -100}%` }}
animate={{ x: `${currentIndex * -100}%` }} animate={{ x: `${currentIndex * -100}%` }}
variants={variants(200, 1)} variants={variants(200, 1)}
key={indx}
key={stay.id}
style={{ width: `calc(1/${numberOfItems} * 100%)` }} style={{ width: `calc(1/${numberOfItems} * 100%)` }}
> >
<StayCard2 key={stay.id} data={stay} /> <StayCard2 key={stay.id} data={stay} />
@ -158,7 +159,7 @@ const SectionGridFeaturePlaces: FC<SectionGridFeaturePlacesProps> = ({
<div className="flex mt-16"> <div className="flex mt-16">
<ButtonPrimary onClick={() => router.push("/tours")}> <ButtonPrimary onClick={() => router.push("/tours")}>
Show me more
{t("showMore")}
</ButtonPrimary> </ButtonPrimary>
</div> </div>
</div> </div>

50
src/components/SectionHowItWork.tsx

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

34
src/components/SectionOurFeatures.tsx

@ -1,7 +1,10 @@
"use client";
import React, { FC } from "react"; import React, { FC } from "react";
import rightImgPng from "@/images/our-features.png"; import rightImgPng from "@/images/our-features.png";
import Image, { StaticImageData } from "next/image"; import Image, { StaticImageData } from "next/image";
import Badge from "@/shared/Badge"; import Badge from "@/shared/Badge";
import { useTranslation } from "react-i18next";
export interface SectionOurFeaturesProps { export interface SectionOurFeaturesProps {
className?: string; className?: string;
@ -14,6 +17,8 @@ const SectionOurFeatures: FC<SectionOurFeaturesProps> = ({
rightImg = rightImgPng, rightImg = rightImgPng,
type = "type1", type = "type1",
}) => { }) => {
const { t } = useTranslation("common");
return ( return (
<div <div
className={`nc-SectionOurFeatures relative flex flex-col items-center ${ className={`nc-SectionOurFeatures relative flex flex-col items-center ${
@ -22,7 +27,7 @@ const SectionOurFeatures: FC<SectionOurFeaturesProps> = ({
data-nc-id="SectionOurFeatures" data-nc-id="SectionOurFeatures"
> >
<div className="flex-grow"> <div className="flex-grow">
<Image src={rightImg} alt="" />
<Image src={rightImg} alt={t("imageAltOurFeatures")} />
</div> </div>
<div <div
className={`max-w-2xl flex-shrink-0 mt-10 lg:mt-0 lg:w-2/5 ${ 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"> <span className="uppercase text-sm text-gray-400 tracking-widest">
BENnefits
{t("benefits")}
</span> </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"> <ul className="space-y-10 mt-16">
<li className="space-y-4"> <li className="space-y-4">
{/* <Badge name="Advertising" /> */}
{/* <Badge name={t("advertising")} /> */}
<span className="block text-xl font-semibold"> <span className="block text-xl font-semibold">
Cost-effective advertising
{t("costEffectiveAdvertising")}
</span> </span>
<span className="block mt-5 text-neutral-500 dark:text-neutral-400"> <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> </span>
</li> </li>
<li className="space-y-4"> <li className="space-y-4">
{/* <Badge color="green" name="Exposure " /> */}
{/* <Badge color="green" name={t("exposure")} /> */}
<span className="block text-xl font-semibold"> <span className="block text-xl font-semibold">
Reach millions with Chisfis
{t("reachMillions")}
</span> </span>
<span className="block mt-5 text-neutral-500 dark:text-neutral-400"> <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> </span>
</li> </li>
<li className="space-y-4"> <li className="space-y-4">
{/* <Badge color="red" name="Secure" />/ */}
{/* <Badge color="red" name={t("secure")} /> */}
<span className="block text-xl font-semibold"> <span className="block text-xl font-semibold">
Secure and simple
{t("secureAndSimple")}
</span> </span>
<span className="block mt-5 text-neutral-500 dark:text-neutral-400"> <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> </span>
</li> </li>
</ul> </ul>

141
src/components/TourSuggestion.tsx

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { FC, useContext, useEffect, useState } from "react";
import React, { FC, useEffect, useState } from "react";
import { TaxonomyType } from "@/data/types"; import { TaxonomyType } from "@/data/types";
import CardCategory3 from "@/components/CardCategory3"; import CardCategory3 from "@/components/CardCategory3";
import CardCategory4 from "@/components/CardCategory4"; import CardCategory4 from "@/components/CardCategory4";
@ -12,9 +12,8 @@ import PrevBtn from "./PrevBtn";
import NextBtn from "./NextBtn"; import NextBtn from "./NextBtn";
import { variants } from "@/utils/animationVariants"; import { variants } from "@/utils/animationVariants";
import { useWindowSize } from "react-use"; 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 { export interface TourSuggestionProps {
className?: string; className?: string;
@ -26,127 +25,40 @@ export interface TourSuggestionProps {
sliderStyle?: "style1" | "style2"; 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> = ({ const TourSuggestion: FC<TourSuggestionProps> = ({
heading = "Countries", heading = "Countries",
subHeading = "Popular places to recommends for you",
subHeading = "Popular places to recommend for you",
className = "", className = "",
itemClassName = "", itemClassName = "",
itemPerRow = 5, itemPerRow = 5,
categoryCardType = "card3", categoryCardType = "card3",
sliderStyle = "style1", sliderStyle = "style1",
}) => { }) => {
const { t } = useTranslation("common");
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = 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; const windowWidth = useWindowSize().width;
useEffect(() => { useEffect(() => {
if (windowWidth < 320) { if (windowWidth < 320) {
return setNumberOfitem(1);
return setNumberOfItems(1);
} }
if (windowWidth < 500) { if (windowWidth < 500) {
return setNumberOfitem(itemPerRow - 3);
return setNumberOfItems(itemPerRow - 3);
} }
if (windowWidth < 1024) { if (windowWidth < 1024) {
return setNumberOfitem(itemPerRow - 2);
return setNumberOfItems(itemPerRow - 2);
} }
if (windowWidth < 1280) { if (windowWidth < 1280) {
return setNumberOfitem(itemPerRow - 1);
return setNumberOfItems(itemPerRow - 1);
} }
setNumberOfitem(itemPerRow);
setNumberOfItems(itemPerRow);
}, [itemPerRow, windowWidth]); }, [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) { function changeItemId(newVal: number) {
@ -172,25 +84,12 @@ console.log(countries);
trackMouse: true, 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; if (!numberOfItems) return null;
return ( 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> </Heading>
<MotionConfig <MotionConfig
transition={{ transition={{
@ -228,21 +127,21 @@ console.log(countries);
</motion.ul> </motion.ul>
</div> </div>
{currentIndex ? (
{currentIndex > 0 && (
<PrevBtn <PrevBtn
style={{ transform: "translate3d(0, 0, 0)" }} style={{ transform: "translate3d(0, 0, 0)" }}
onClick={() => changeItemId(currentIndex - 1)} 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]" 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 <NextBtn
style={{ transform: "translate3d(0, 0, 0)" }} style={{ transform: "translate3d(0, 0, 0)" }}
onClick={() => changeItemId(currentIndex + 1)} 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]" 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> </div>
</MotionConfig> </MotionConfig>
</div> </div>

10
src/data/navigation.ts

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

22
src/hooks/FormValidation.ts

@ -9,22 +9,22 @@ const useFormValidation = () => {
const validateForm = (form: SignUpForm) => { const validateForm = (form: SignUpForm) => {
let newErrors: Record<string, string> = {}; 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 // Validate phone number using google-libphonenumber
try { 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)) { 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) { } catch (error) {
newErrors.phoneNumber = 'Invalid phone number format';
newErrors.phone_number = 'Invalid phone number format';
} }
if (!form.password) { if (!form.password) {
@ -33,10 +33,10 @@ const useFormValidation = () => {
newErrors.password = 'Password must be at least 8 characters'; 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); setErrors(newErrors);

25
src/i18n.ts

@ -5,14 +5,31 @@ import { initReactI18next } from 'react-i18next';
import enCommon from '../public/locales/en/common.json'; import enCommon from '../public/locales/en/common.json';
import enFAQ from '../public/locales/en/FAQ.json'; import enFAQ from '../public/locales/en/FAQ.json';
import frCommon from '../public/locales/fr/common.json';
import 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 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'; // import viCommon from '../public/locales/vi/common.json';
const resources = { 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 }, // 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'; import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/; const PUBLIC_FILE = /\.(.*)$/;
const locales = ['en', 'vi', 'fr', 'ar'];
const locales = ['en', 'ru', 'id', 'ar'];
const defaultLocale = 'en'; const defaultLocale = 'en';
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl; const { pathname, search } = request.nextUrl;
// Ignore API routes and public files
if (pathname.startsWith('/api') || PUBLIC_FILE.test(pathname)) { if (pathname.startsWith('/api') || PUBLIC_FILE.test(pathname)) {
return NextResponse.next(); return NextResponse.next();
} }
// Get the locale from the cookie or fall back to defaultLocale
const locale = request.cookies.get('locale')?.value || defaultLocale;
// Check if the locale is already set in the URL
const cookieLocale = request.cookies.get('locale')?.value;
const pathnameParts = pathname.split('/'); const pathnameParts = pathname.split('/');
const hasLocale = locales.includes(pathnameParts[1]); 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) { 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); 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(); return NextResponse.next();
} }
// Matcher configuration remains the same // Matcher configuration remains the same
export const config = { export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],

20
src/routers/types.ts

@ -1,12 +1,18 @@
import type { Route as NextRoute } from "next";
import { ComponentType } from "react"; 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 { 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, loading,
onClick = () => {}, 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 = () => { const _renderLoading = () => {
return ( return (

2
src/shared/Logo.tsx

@ -19,7 +19,7 @@ const Logo: React.FC<LogoProps> = ({
return ( return (
<Link <Link
href="/" 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 /> */} {/* <LogoSvgLight /> */}
{/* THIS USE FOR MY CLIENT */} {/* 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 ButtonPrimary from "../ButtonPrimary";
import ButtonSecondary from "../ButtonSecondary"; import ButtonSecondary from "../ButtonSecondary";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "react-i18next";
function Navigation() { function Navigation() {
const {t} = useTranslation("navigation");
return ( return (
<ul className="nc-Navigation hidden lg:flex lg:flex-wrap lg:space-x-1 relative "> <ul className="nc-Navigation hidden lg:flex lg:flex-wrap lg:space-x-1 relative ">
{NAVIGATION_DEMO.map((item) => ( {NAVIGATION_DEMO.map((item) => (
<NavigationItem key={item.id} menuItem={item} /> <NavigationItem key={item.id} menuItem={item} />
))} ))}
<Link href="custom-trip" className="m-5 self-center border border-gray-300 p-1.5 rounded-full px-4 hover:bg-bronze hover:text-white">Custom Tour</Link>
<Link href="custom-trip" className="m-5 self-center border border-gray-300 p-1.5 rounded-full px-4 hover:bg-bronze hover:text-white">{t("customTrip")}</Link>
</ul> </ul>
); );
} }

6
src/shared/Navigation/NavigationItem.tsx

@ -9,6 +9,7 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import React, { FC, Fragment, useEffect, useState } from "react"; import React, { FC, Fragment, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
// <--- NavItemType ---> // <--- NavItemType --->
export interface MegamenuItem { export interface MegamenuItem {
@ -37,6 +38,7 @@ type NavigationItemWithRouterProps = NavigationItemProps;
const NavigationItem: FC<NavigationItemWithRouterProps> = ({ menuItem }) => { const NavigationItem: FC<NavigationItemWithRouterProps> = ({ menuItem }) => {
const [menuCurrentHovers, setMenuCurrentHovers] = useState<string[]>([]); const [menuCurrentHovers, setMenuCurrentHovers] = useState<string[]>([]);
const { countries } = useToursContext(); const { countries } = useToursContext();
const { t } = useTranslation("navigation"); // Initialize useTranslation
// CLOSE ALL MENU OPENING WHEN CHANGE HISTORY // CLOSE ALL MENU OPENING WHEN CHANGE HISTORY
const locationPathName = usePathname(); 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`} } 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 || "/"} href={item.href || "/"}
> >
{item.name}
{t(item.name)}
{item.type && ( {item.type && (
<ChevronDownIcon <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" aria-hidden="true"
/> />
)} )}

Loading…
Cancel
Save