diff --git a/api/servicesApi.ts b/api/servicesApi.ts new file mode 100644 index 0000000..1fcf7dc --- /dev/null +++ b/api/servicesApi.ts @@ -0,0 +1,562 @@ +/** + * Service Page API Functions + * Fetch data for Service page from external API + */ + +/* ======================= + Types +======================= */ + +export interface ServiceItem { + slug: string; + name: string; + description: string; + image: string; + layout: "left" | "right"; + details: { + title: string; + description: string; + mainImage: string; + overviewTitle: string; + overviewDescription: string; + additionalDescription: string; + features: { + icon: string; + title: string; + description: string; + }[]; + faq: { + id: string; + question: string; + answer: string; + isExpanded: boolean; + }[]; + }; +} + +export interface ServiceSection { + title: { + subTitle: string; + mainTitle: string; + }; + items: ServiceItem[]; +} + +export interface CountryItem { + id: string; + name: string; + description: string; + image: string; + icon: string; + link: string; +} + +export interface DestinationSection { + backgroundImage: string; + title: { + subTitle: string; + mainTitle: string; + }; +} + +export interface VisaItem { + id: string; + number: string; + name: string; + description: string; + buttonText: string; + buttonLink: string; +} + +export interface VisaSection { + items: VisaItem[]; +} + +export interface ClientReviewItem { + id: string; + rating: number; + content: string; + author: { + name: string; + type: string; + }; + icon: string; +} + +export interface ReviewSection { + title: { + subTitle: string; + mainTitle: string; + }; + thumb: string; + items: ClientReviewItem[]; +} + +export interface ServicePageData { + pageTitle: string; + services: ServiceSection; + destinations: DestinationSection; + visas: VisaSection; + reviews: ReviewSection; +} + +/* ======================= + Utils +======================= */ + +const getApiUrl = (): string => { + return process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; +}; + +/* ======================= + Fetch API +======================= */ + +export const fetchServicePageData = async (): Promise => { + try { + const apiUrl = getApiUrl(); + const endpoint = `${apiUrl}/api/service`; + + console.log("Fetching services from endpoint:", endpoint); + + const response = await fetch(endpoint, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + console.log("Services API response status:", response.status); + + if (!response.ok) { + console.error("Services API failed, using fallback data"); + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = (await response.json()) as ServicePageData; + console.log("Services data received successfully"); + return data; + } catch (error) { + console.error("Error fetching service page data:", error); + console.log("Using fallback service data"); + return getFallbackServicePageData(); + } +}; + +export const fetchServiceBySlug = async (slug: string): Promise => { + console.log("=== fetchServiceBySlug called ==="); + console.log("Input slug:", slug); + console.log("Slug type:", typeof slug); + console.log("Slug length:", slug?.length); + + if (!slug || slug === "undefined") { + console.error("Invalid slug provided:", slug); + throw new Error("Invalid slug parameter"); + } + + try { + const apiUrl = getApiUrl(); + const endpoint = `${apiUrl}/api/service/${slug}`; + + console.log("Fetching service from endpoint:", endpoint); + + const response = await fetch(endpoint, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + console.log("Response status:", response.status); + console.log("Response ok:", response.ok); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Error response body:", errorText); + + // If it's a 404, try to get the service from fallback data + if (response.status === 404) { + console.log("Service not found in backend, checking fallback data"); + const fallbackData = getFallbackServicePageData(); + const service = fallbackData.services.items.find( + (item) => item.slug === slug, + ); + + if (service) { + console.log( + "Found service in fallback data, creating response structure", + ); + // Create a response structure that matches what the backend should return + return { + pageTitle: fallbackData.pageTitle, + breadcrumb: { + title: "Service Details", + items: [ + { label: "Home", href: "/" }, + { label: "Services", href: "/services" }, + { label: service.name, href: `/services/details/${slug}` }, + ], + }, + serviceDetails: { + content: service.details, + keyFeatures: { + title: "Key Features", + sideImage: + "/assets/img/inner-page/service-details/details-2.jpg", + items: service.details.features || [], + }, + faq: { + title: "Frequently Asked Questions", + sideImage: + "/assets/img/inner-page/service-details/details-3.jpg", + items: service.details.faq || [], + }, + }, + }; + } + } + + throw new Error( + `HTTP error! status: ${response.status}, body: ${errorText}`, + ); + } + + const data = await response.json(); + console.log("Service data received:", data); + return data; + } catch (error) { + console.error("Error fetching service by slug:", error); + throw error; + } +}; + +/* ======================= + Fallback Data +======================= */ + +export const getFallbackServicePageData = (): ServicePageData => ({ + pageTitle: "Visaway – Immigration & Visa Consulting HTML Template", + + services: { + title: { + subTitle: "What We Offer", + mainTitle: "Our Immigration Services", + }, + items: [ + { + slug: "immigration-appeal", + name: "Immigration Appeal & Legal Support", + description: + "Our experts provide professional guidance for immigration appeals and legal matters, helping clients overcome visa rejections with personalized strategies and strong case representation.", + image: "/assets/img/home-3/service/01.jpg", + layout: "left", + details: { + title: "Immigration Appeal & Legal Support", + description: + "Our experts provide professional guidance for immigration appeals and legal matters, helping clients overcome visa rejections with personalized strategies and strong case representation.", + mainImage: "/assets/img/inner-page/service-details/details-1.jpg", + overviewTitle: "Service Overview", + overviewDescription: + "Our Immigration Appeal & Legal Support service is designed to help clients navigate complex immigration challenges. We provide expert legal guidance, case analysis, and strategic representation to maximize your chances of success.", + additionalDescription: + "With years of experience in immigration law, our team is committed to turning your immigration challenges into success stories.", + features: [ + { + icon: "fa-solid fa-chevrons-right", + title: "Legal Case Analysis", + description: + "Thorough review and analysis of your immigration case.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Appeal Strategy Development", + description: "Custom strategies to overcome visa rejections.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Document Preparation", + description: + "Professional preparation of all required legal documents.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Court Representation", + description: "Expert legal representation in immigration courts.", + }, + ], + faq: [ + { + id: "faq-appeal-1", + question: "01. What are the chances of a successful appeal?", + answer: + "Success rates vary by case type, but our experienced legal team significantly improves your chances through thorough case analysis and strategic representation.", + isExpanded: false, + }, + { + id: "faq-appeal-2", + question: "02. How long does the appeal process take?", + answer: + "Appeal timelines vary by jurisdiction and case complexity, typically ranging from 6-18 months. We keep you informed throughout the process.", + isExpanded: true, + }, + ], + }, + }, + { + slug: "scholarship-guidance", + name: "Scholarship & Study Grant Guidance", + description: + "We help students identify suitable scholarships and study grants, assist with applications, and provide expert guidance to maximize chances of securing financial support abroad.", + image: "/assets/img/home-3/service/02.jpg", + layout: "right", + details: { + title: "Scholarship & Study Grant Guidance", + description: + "We help students unlock opportunities to study abroad with the right financial support. Our expert advisors guide you in finding scholarships, grants, and funding options that match your academic background, chosen destination, and career goals.", + mainImage: "/assets/img/inner-page/service-details/details-1.jpg", + overviewTitle: "Service Overview", + overviewDescription: + "Our Education Visa Consultancy is dedicated to guiding students in achieving their study abroad dreams. We provide complete support including university selection, application assistance, scholarship guidance, visa documentation, and interview preparation.", + additionalDescription: + "From start to finish, we are committed to turning your education journey into a successful international experience.", + features: [ + { + icon: "fa-solid fa-chevrons-right", + title: "Scholarship Research", + description: + "Comprehensive research to find suitable funding opportunities.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Application Assistance", + description: + "Expert help with scholarship and grant applications.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Essay Writing Support", + description: + "Professional guidance for compelling scholarship essays.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Interview Preparation", + description: + "Coaching for scholarship interviews and presentations.", + }, + ], + faq: [ + { + id: "faq-scholarship-1", + question: "01. Do you help find scholarships for all countries?", + answer: + "Yes, we have extensive databases and partnerships worldwide to help you find scholarships in your preferred destination country.", + isExpanded: false, + }, + { + id: "faq-scholarship-2", + question: "02. What are the eligibility requirements?", + answer: + "Eligibility varies by scholarship type and provider. We assess your profile and match you with suitable opportunities based on your academic background and goals.", + isExpanded: true, + }, + ], + }, + }, + { + slug: "permanent-residency", + name: "Permanent Residency (PR) Services", + description: + "Our PR services guide clients through every step of the residency process, including documentation, eligibility assessment, and application support, ensuring a smooth and successful approval.", + image: "/assets/img/home-3/service/03.jpg", + layout: "left", + details: { + title: "Permanent Residency (PR) Services", + description: + "Our PR services guide clients through every step of the residency process, including documentation, eligibility assessment, and application support, ensuring a smooth and successful approval.", + mainImage: "/assets/img/inner-page/service-details/details-1.jpg", + overviewTitle: "Service Overview", + overviewDescription: + "Our Permanent Residency services provide comprehensive support for individuals seeking to establish permanent residence in their chosen country. We handle all aspects of the PR application process with expertise and care.", + additionalDescription: + "Our experienced team ensures that your PR application is handled professionally and efficiently, maximizing your chances of approval.", + features: [ + { + icon: "fa-solid fa-chevrons-right", + title: "Eligibility Assessment", + description: + "Comprehensive evaluation of your PR eligibility and options.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Points Calculation", + description: + "Accurate calculation and optimization of your points score.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Document Verification", + description: + "Thorough verification and preparation of all required documents.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Application Tracking", + description: + "Regular updates and tracking of your PR application status.", + }, + ], + faq: [ + { + id: "faq-pr-1", + question: "01. How long does the PR process take?", + answer: + "Processing times vary by country and program, typically ranging from 12-24 months. We provide realistic timelines based on current processing standards.", + isExpanded: false, + }, + { + id: "faq-pr-2", + question: "02. What documents are required for PR application?", + answer: + "Document requirements vary by country but typically include educational credentials, work experience, language test results, and medical examinations. We provide a complete checklist.", + isExpanded: true, + }, + ], + }, + }, + { + slug: "citizenship-naturalization", + name: "Citizenship & Naturalization Guidance", + description: + "We provide expert guidance for citizenship and naturalization processes, assisting clients with documentation, eligibility, and legal procedures to achieve a smooth and successful application.", + image: "/assets/img/home-3/service/04.jpg", + layout: "right", + details: { + title: "Citizenship & Naturalization Guidance", + description: + "We provide expert guidance for citizenship and naturalization processes, assisting clients with documentation, eligibility, and legal procedures to achieve a smooth and successful application.", + mainImage: "/assets/img/inner-page/service-details/details-1.jpg", + overviewTitle: "Service Overview", + overviewDescription: + "Our Citizenship & Naturalization service helps individuals navigate the complex process of becoming a citizen. We provide step-by-step guidance, documentation support, and legal expertise throughout the entire process.", + additionalDescription: + "With our comprehensive approach, we make the path to citizenship clear, manageable, and successful for every client.", + features: [ + { + icon: "fa-solid fa-chevrons-right", + title: "Citizenship Test Preparation", + description: + "Comprehensive preparation for citizenship knowledge tests.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Language Requirements", + description: + "Guidance on meeting language proficiency requirements.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Residency Verification", + description: + "Assistance with proving residency and physical presence requirements.", + }, + { + icon: "fa-solid fa-chevrons-right", + title: "Ceremony Preparation", + description: + "Support and guidance for the citizenship ceremony process.", + }, + ], + faq: [ + { + id: "faq-citizenship-1", + question: "01. What are the basic requirements for citizenship?", + answer: + "Requirements typically include permanent residency, physical presence, language proficiency, and knowledge of the country's history and government. Specific requirements vary by country.", + isExpanded: false, + }, + { + id: "faq-citizenship-2", + question: "02. How do I prepare for the citizenship test?", + answer: + "We provide comprehensive study materials, practice tests, and coaching sessions to help you prepare for both the knowledge test and language requirements.", + isExpanded: true, + }, + ], + }, + }, + ], + }, + + destinations: { + backgroundImage: "/assets/img/home-3/choose-us/bg.png", + title: { + subTitle: "Countries we offer", + mainTitle: "Choose Your Immigration Destination", + }, + }, + + visas: { + items: [ + { + id: "family-visa", + number: "01", + name: "Family Visa", + description: + "Our Family Visa services help reunite loved ones by providing expert guidance.", + buttonText: "service _ 02", + buttonLink: "/services/details/family-visa", + }, + { + id: "student-visa", + number: "02", + name: "Student Visa", + description: + "We provide expert guidance for student visa applications.", + buttonText: "service _ 02", + buttonLink: "/services/details/student-visa", + }, + { + id: "work-visa", + number: "03", + name: "Work Visa", + description: + "Collaboratively disintermediate one to one functionalities and long term.", + buttonText: "service _ 02", + buttonLink: "/services/details/work-visa", + }, + ], + }, + + reviews: { + title: { + subTitle: "What Our Clients Say", + mainTitle: "Immigration Success Stories", + }, + thumb: "/assets/img/home-3/test-thumb.jpg", + items: [ + { + id: "client-review-1", + rating: 5, + content: + "The team provided exceptional guidance throughout my immigration process. Their expertise, personalized support, and attention to detail ensured a smooth, stress-free experience and successful visa approval.", + author: { + name: "Mohammed Ali,", + type: "Family Visa", + }, + icon: "fa-solid fa-quote-right", + }, + { + id: "client-review-2", + rating: 5, + content: + "The team provided exceptional guidance throughout my immigration process. Their expertise, personalized support, and attention to detail ensured a smooth, stress-free experience and successful visa approval.", + author: { + name: "Mohammed Ali,", + type: "Family Visa", + }, + icon: "fa-solid fa-quote-right", + }, + ], + }, +}); diff --git a/app/appointment/page.tsx b/app/appointment/page.tsx index d1667bc..084edcb 100644 --- a/app/appointment/page.tsx +++ b/app/appointment/page.tsx @@ -1,501 +1,437 @@ "use client"; -import { useState } from "react"; -import appointmentData from "./appointment.json"; +import { useState, useEffect, FormEvent } from "react"; +import { AppointmentData } from "./types"; +import Breadcrumb from "../components/Breadcrumb"; + export default function AppointmentPage() { - const [selectedType, setSelectedType] = useState("consultation"); - const [selectedConsultant, setSelectedConsultant] = useState(""); - const [selectedDate, setSelectedDate] = useState(""); - const [selectedTime, setSelectedTime] = useState(""); - const [selectedOffice, setSelectedOffice] = useState("online"); - const [openFaq, setOpenFaq] = useState(null); - + const [appointmentData, setAppointmentData] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [formData, setFormData] = useState({ - fullName: "", + name: "", email: "", phone: "", - visaType: "", - country: "", - travelDate: "", - previousVisa: "", - notes: "", + address: "", + appointmentDate: "", + message: "", + visaTypes: [] as string[], }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitStatus, setSubmitStatus] = useState<{ + type: "success" | "error" | null; + message: string; + }>({ type: null, message: "" }); - const handleInputChange = ( - e: React.ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement - >, + // Calendar state + const [currentDate, setCurrentDate] = useState(new Date()); + + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + + // Calendar helper functions + const months = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + const getDaysInMonth = (year: number, month: number) => { + return new Date(year, month + 1, 0).getDate(); + }; + + const getFirstDayOfMonth = (year: number, month: number) => { + const day = new Date(year, month, 1).getDay(); + // Convert Sunday (0) to 7 for Monday-first calendar + return day === 0 ? 7 : day; + }; + + const generateCalendarDays = () => { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + const daysInMonth = getDaysInMonth(year, month); + const firstDay = getFirstDayOfMonth(year, month); + const today = new Date(); + + const days: React.ReactNode[] = []; + + // Empty cells for days before the first day of the month + for (let i = 1; i < firstDay; i++) { + days.push(
); + } + + // Days of the month + for (let day = 1; day <= daysInMonth; day++) { + const isToday = + day === today.getDate() && + month === today.getMonth() && + year === today.getFullYear(); + + days.push( +
handleDateClick(year, month, day)} + style={{ cursor: "pointer" }} + > + {day} +
+ ); + } + + return days; + }; + + const handleDateClick = (year: number, month: number, day: number) => { + const selectedDate = new Date(year, month, day); + const formattedDate = selectedDate.toISOString().split("T")[0]; + setFormData((prev) => ({ ...prev, appointmentDate: formattedDate })); + }; + + const handlePrevMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)); + }; + + const handleNextMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)); + }; + + // Fetch appointment data from CMS + useEffect(() => { + const fetchAppointmentData = async () => { + try { + const response = await fetch(`${apiUrl}/api/appointment`); + const result = await response.json(); + + if (result.success && result.data) { + setAppointmentData(result.data); + } + } catch (error) { + console.error("Error fetching appointment data:", error); + } finally { + setIsLoading(false); + } + }; + + fetchAppointmentData(); + }, [apiUrl]); + + const handleChange = ( + e: React.ChangeEvent ) => { const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleCheckboxChange = (visaType: string, checked: boolean) => { setFormData((prev) => ({ ...prev, - [name]: value, + visaTypes: checked + ? [...prev.visaTypes, visaType] + : prev.visaTypes.filter((v) => v !== visaType), })); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - console.log("Appointment booked:", { - type: selectedType, - consultant: selectedConsultant, - date: selectedDate, - time: selectedTime, - office: selectedOffice, - ...formData, - }); - alert( - "Đặt lịch thành công! Chúng tôi sẽ liên hệ xác nhận trong thời gian sớm nhất.", + setIsSubmitting(true); + setSubmitStatus({ type: null, message: "" }); + + try { + const response = await fetch(`${apiUrl}/api/appointment/submit`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setSubmitStatus({ + type: "success", + message: data.message || "Thank you! Your appointment has been submitted.", + }); + // Reset form + setFormData({ + name: "", + email: "", + phone: "", + address: "", + appointmentDate: "", + message: "", + visaTypes: [], + }); + } else { + setSubmitStatus({ + type: "error", + message: data.error || "Something went wrong. Please try again.", + }); + } + } catch (error) { + console.error("Submit error:", error); + setSubmitStatus({ + type: "error", + message: "Network error. Please check your connection and try again.", + }); + } finally { + setIsSubmitting(false); + } + }; + + // Get data with fallbacks + const hero = appointmentData?.hero || { + title: "Make Appointment", + backgroundImage: "/assets/img/inner-page/breadcrumb.jpg", + subtitle: "About Our Consultancy", + heading: "Want to meet us for your need?", + description: "24/7 customer support is always ready to answer all your questions", + }; + + const visaOptions = appointmentData?.visaOptions || [ + "Canada Immigration", + "Tourist Visa", + "Medical Visa", + "Coaching", + "Student Visa", + "Spouse Visa", + "Job Opportunity", + "Exam", + ]; + + const formSettings = appointmentData?.form || { + heading: "Request Appointment", + submitButton: { + text: "Request Appointment", + icon: "fa-solid fa-arrow-right", + buttonClass: "theme-btn", + }, + }; + + // Get background image URL + const backgroundImage = hero.backgroundImage?.startsWith("http") + ? hero.backgroundImage + : hero.backgroundImage?.startsWith("/") + ? hero.backgroundImage + : `/assets/img/inner-page/breadcrumb.jpg`; + + if (isLoading) { + return ( +
+
+ Loading... +
+
); - }; - - const toggleFaq = (index: number) => { - setOpenFaq(openFaq === index ? null : index); - }; - - const selectedAppointmentType = appointmentData.appointmentTypes.find( - (type) => type.id === selectedType, - ); - const selectedConsultantData = appointmentData.consultants.find( - (c) => c.id === selectedConsultant, - ); + } return ( -
-
- {/* Header */} -
-

- {appointmentData.title} -

-

- {appointmentData.subtitle} -

+ <> + {/* Breadcrumb-Wrapper Section Start */} + - {/* Benefits */} -
- {appointmentData.hero.benefits.map((benefit, index) => ( -
-

{benefit}

+ {/* Appointment Section Start */} +
+
+
+
+
+
+
+ {hero.subtitle && ( + {hero.subtitle} + )} + {hero.heading && ( +

+ {hero.heading} +

+ )} +
+
Have any questions?
+ {hero.description &&

{hero.description}

} +
- ))} -
-
- -
- {/* Booking Form */} -
-
-

- Đặt Lịch Hẹn -

- -
- {/* Appointment Type */} -
- -
- {appointmentData.appointmentTypes.map((type) => ( -
setSelectedType(type.id)} - > -
-
- {type.icon} -
-

{type.name}

- {type.popular && ( - - Phổ biến - - )} -
-
-
-
- {type.price} -
-
- {type.duration} -
-
-
-

- {type.description} -

-
- ))} -
-
- - {/* Consultant Selection */} -
- -
-
setSelectedConsultant("")} - > -
- Để chúng tôi chọn chuyên gia phù hợp -
-
- Dựa trên loại visa và quốc gia bạn quan tâm -
+
+
+
+

{months[currentDate.getMonth()]} {currentDate.getFullYear()}

+
+ +
- - {appointmentData.consultants.map((consultant) => ( -
setSelectedConsultant(consultant.id)} - > -
-
- 👤 -
-
-
- {consultant.name} -
-
- {consultant.title} -
-
- - ⭐ {consultant.rating} - - - ({consultant.reviews} đánh giá) - - {consultant.experience} -
-
-
-
- ))}
-
- - {/* Date & Time */} -
-
- - setSelectedDate(e.target.value)} - min={new Date().toISOString().split("T")[0]} - required - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> +
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
Sun
- -
- - +
+ {generateCalendarDays()}
- - {/* Office Selection */} -
- -
-
setSelectedOffice("online")} - > -
- 💻 -
-
Tư vấn online
-
- Video call qua Zoom/Google Meet -
-
-
-
- - {appointmentData.offices.map((office, index) => ( -
setSelectedOffice(office.name)} - > -
- 🏢 -
-
- Văn phòng {office.name} -
-
- {office.address} -
-
- {office.hours} -
-
-
-
- ))} -
-
- - {/* Personal Information */} -
-

- Thông Tin Cá Nhân -

-
- {appointmentData.formFields.map((field) => ( -
- - - {field.type === "textarea" ? ( - +
+
+
+ + {visaOptions.length > 0 && ( +
+
+ {visaOptions.slice(0, Math.ceil(visaOptions.length / 2)).map((visa, index) => ( +
+ + handleCheckboxChange(visa, e.target.checked) + } + /> + +
+ ))} +
+
+ {visaOptions.slice(Math.ceil(visaOptions.length / 2)).map((visa, index) => ( +
+ + handleCheckboxChange(visa, e.target.checked) + } + /> + +
+ ))} +
+
+ )} + + +
+
+
-
+ ); } diff --git a/app/appointment/types.ts b/app/appointment/types.ts new file mode 100644 index 0000000..1218aed --- /dev/null +++ b/app/appointment/types.ts @@ -0,0 +1,30 @@ +export interface AppointmentHero { + title: string; + backgroundImage: string; + subtitle: string; + heading: string; + description: string; +} + +export interface AppointmentForm { + heading: string; + fields: Array<{ + name: string; + label: string; + type: string; + placeholder: string; + required: boolean; + colClass: string; + }>; + submitButton: { + text: string; + icon: string; + buttonClass: string; + }; +} + +export interface AppointmentData { + hero: AppointmentHero; + visaOptions: string[]; + form: AppointmentForm; +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 4021e82..cd1f004 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,157 +1,237 @@ "use client"; -import { useState } from "react"; -import contactData from "./contact.json"; +import { useState, useEffect, FormEvent } from "react"; +import { ContactData } from "./types"; +import Breadcrumb from "../components/Breadcrumb"; export default function ContactPage() { + const [contactData, setContactData] = useState(null); + const [loading, setLoading] = useState(true); const [formData, setFormData] = useState({ - fullName: "", + name: "", email: "", phone: "", - service: "", + address: "", + date: "", message: "", }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitStatus, setSubmitStatus] = useState<{ + type: "success" | "error" | null; + message: string; + }>({ type: null, message: "" }); - const handleInputChange = ( - e: React.ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement - >, + const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + + // Fetch contact data from CMS + useEffect(() => { + const fetchContactData = async () => { + try { + const response = await fetch(`${apiUrl}/api/contact`, { cache: 'no-store' }); + if (response.ok) { + const data = await response.json(); + setContactData(data); + } + } catch (error) { + console.error("Error fetching contact data:", error); + } finally { + setLoading(false); + } + }; + + fetchContactData(); + }, [apiUrl]); + + const handleChange = ( + e: React.ChangeEvent ) => { const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); + setFormData((prev) => ({ ...prev, [name]: value })); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - console.log("Form submitted:", formData); - // Handle form submission here - alert( - "Cảm ơn bạn đã liên hệ! Chúng tôi sẽ phản hồi trong thời gian sớm nhất.", + setIsSubmitting(true); + setSubmitStatus({ type: null, message: "" }); + + try { + const response = await fetch(`${apiUrl}/api/contact/submit`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setSubmitStatus({ + type: "success", + message: data.message || "Thank you! We will contact you soon.", + }); + setFormData({ + name: "", + email: "", + phone: "", + address: "", + date: "", + message: "", + }); + } else { + setSubmitStatus({ + type: "error", + message: data.error || "Something went wrong. Please try again.", + }); + } + } catch (error) { + console.error("Submit error:", error); + setSubmitStatus({ + type: "error", + message: "Network error. Please check your connection and try again.", + }); + } finally { + setIsSubmitting(false); + } + }; + + if (loading) { + return ( +
+

Loading...

+
); + } + + // Default values if API fails + const hero = contactData?.hero || { title: "Contact Us", backgroundImage: "/assets/img/inner-page/breadcrumb.jpg" }; + const contactCards = contactData?.contactCards || []; + const map = contactData?.map || { embedUrl: "" }; + const form = contactData?.form || { + heading: "Send Us Message", + description: "Have questions? Send us a message today.", + fields: [], + submitButton: { text: "SEND MESSAGE", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" } }; return ( -
-
- {/* Header */} -
-

- {contactData.title} -

-

{contactData.subtitle}

-
+ <> + {/* Breadcrumb-Wrapper Section Start */} + -
- {/* Contact Information */} -
-

- Thông Tin Liên Hệ -

- - {/* Contact Details */} -
- {Object.entries(contactData.contactInfo).map(([key, info]) => ( -
- {info.icon} -
-

- {info.label} -

-

- {info.value} -

+ {/* Contact Icon Section Start */} +
+
+
+ {contactCards.map((card, index) => ( +
+
+
+ +
+
+

{card.title}

+
+ {card.content.map((line, i) => ( + + {card.type === "email" ? ( + {line} + ) : card.type === "phone" ? ( + {line} + ) : ( + line + )} + {i < card.content.length - 1 &&
} +
+ ))} +
- ))} -
- - {/* Offices */} -

Văn Phòng

-
- {contactData.offices.map((office, index) => ( -
-

- {office.name} -

-

- 📍 {office.address} -

-

- 📞 {office.phone} -

-

✉️ {office.email}

-
- ))} -
-
- - {/* Contact Form */} -
-

- Gửi Tin Nhắn -

-
- {contactData.formFields.map((field) => ( -
- - - {field.type === "textarea" ? ( - + ) : ( + + )} +
+
+ ))} +
+ +
+
+ +
+
+
+
+
+ + {/* Map Section Start */} + {map.embedUrl && ( +
+
+
+ +
+
+
+ )} + ); } diff --git a/app/contact/types.ts b/app/contact/types.ts new file mode 100644 index 0000000..8bdffd5 --- /dev/null +++ b/app/contact/types.ts @@ -0,0 +1,44 @@ +export interface ContactCard { + type: string; + title: string; + content: string[]; + iconType: string; +} + +export interface ContactHero { + title: string; + backgroundImage: string; +} + +export interface ContactMap { + embedUrl: string; +} + +export interface ContactFormField { + name: string; + label: string; + type: string; + placeholder: string; + required: boolean; + colClass: string; +} + +export interface ContactSubmitButton { + text: string; + icon: string; + buttonClass: string; +} + +export interface ContactForm { + heading: string; + description: string; + fields: ContactFormField[]; + submitButton: ContactSubmitButton; +} + +export interface ContactData { + hero: ContactHero; + contactCards: ContactCard[]; + map: ContactMap; + form: ContactForm; +} diff --git a/app/globals.css b/app/globals.css index a461c50..24d681c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1 +1,8 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; +.collapse { + visibility: visible !important; +} + +.collapse.show { + visibility: visible; +} diff --git a/app/layout.tsx b/app/layout.tsx index f4d1e35..b15b35c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,73 +3,76 @@ import "./globals.css"; import Header from "./components/layout/Header/Header"; import Footer from "./components/layout/Footer/Footer"; + import Loader from "./components/Loader"; import BackToTop from "./components/BackToTop"; import MouseCursor from "./components/MouseCursor"; import Script from "next/script"; export const metadata: Metadata = { - title: "Visaway – Immigration & Visa Consulting HTML Template", - description: "Visaway – Immigration & Visa Consulting HTML Template", + title: "Visaway – Immigration & Visa Consulting HTML Template", + description: "Visaway – Immigration & Visa Consulting HTML Template", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - {/* Favicon */} - + return ( + + + {/* Favicon */} + - {/* Bootstrap min.css */} - - {/* All Min Css */} - - {/* Animate.css */} - - {/* Magnific Popup.css */} - - {/* MeanMenu.css */} - - {/* Odometer.css */} - - {/* Swiper Bundle.css */} - - {/* Nice Select.css */} - - {/* Main.css */} - - - - - - -
+ {/* Bootstrap min.css */} + + {/* All Min Css */} + + {/* Animate.css */} + + {/* Magnific Popup.css */} + + {/* MeanMenu.css */} + + {/* Odometer.css */} + + {/* Swiper Bundle.css */} + + {/* Nice Select.css */} + + {/* Main.css */} + + - {children} + + + + -