From 55c9a4c6b8421e558394f4c48eb354d61b3c2f1b Mon Sep 17 00:00:00 2001 From: LNHA Date: Tue, 3 Feb 2026 10:28:15 +0700 Subject: [PATCH] feat: UI appointment contact pricing page --- app/appointment/page.tsx | 902 +++++++++++++++++++-------------------- app/contact/page.tsx | 394 +++++++++++------ app/pricing/page.tsx | 518 ++++++++++++++-------- 3 files changed, 1052 insertions(+), 762 deletions(-) diff --git a/app/appointment/page.tsx b/app/appointment/page.tsx index d1667bc..aa24873 100644 --- a/app/appointment/page.tsx +++ b/app/appointment/page.tsx @@ -1,501 +1,487 @@ "use client"; -import { useState } from "react"; -import appointmentData from "./appointment.json"; +import { useState, useEffect, FormEvent } from "react"; + +// Types for appointment data from CMS +interface AppointmentHero { + title: string; + backgroundImage: string; + subtitle: string; + heading: string; + description: string; +} + +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; + }; +} + +interface AppointmentData { + hero: AppointmentHero; + visaOptions: string[]; + form: AppointmentForm; +} 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} -

- - {/* Benefits */} -
- {appointmentData.hero.benefits.map((benefit, index) => ( -
-

{benefit}

-
- ))} + <> + {/* Breadcrumb-Wrapper Section Start */} +
+
+ img +
+
+
+

{hero.title}

+
    +
  • + Home +
  • +
  • + +
  • +
  • {hero.title}
  • +
+
-
- {/* 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} -

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

+ {hero.heading} +

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

{hero.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/contact/page.tsx b/app/contact/page.tsx index 4021e82..0cf7719 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,157 +1,291 @@ "use client"; -import { useState } from "react"; -import contactData from "./contact.json"; +import { useState, useEffect, FormEvent } from "react"; + +interface ContactCard { + type: string; + title: string; + content: string[]; + iconType: string; +} + +interface ContactData { + hero: { + title: string; + backgroundImage: string; + }; + contactCards: ContactCard[]; + map: { + embedUrl: string; + }; + form: { + heading: string; + description: string; + fields: { + name: string; + label: string; + type: string; + placeholder: string; + required: boolean; + colClass: string; + }[]; + submitButton: { + text: string; + icon: string; + buttonClass: string; + }; + }; +} 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 */} +
+
+ img
+
+
+

{hero.title}

+
    +
  • + Home +
  • +
  • + +
  • +
  • Contact Us
  • +
+
+
+
-
- {/* 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/pricing/page.tsx b/app/pricing/page.tsx index cae73ce..7e6579a 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,188 +1,358 @@ -import pricingData from "./pricing.json"; - export default function PricingPage() { return ( -
-
- {/* Header */} -
-

- {pricingData.title} -

-

{pricingData.subtitle}

-

- 💡 {pricingData.note} -

+ <> + {/* Breadcrumb-Wrapper Section Start */} +
+
+ img
- - {/* Pricing Packages */} -
- {pricingData.packages.map((pkg) => ( -
- {/* Popular Badge */} - {pkg.popular && ( -
- - Phổ biến nhất - -
- )} - - {/* Package Header */} -
-

- {pkg.name} -

-

{pkg.description}

-
- {parseInt(pkg.price).toLocaleString()} {pkg.currency} -
-
- - {/* Features */} -
-

Bao gồm:

-
    - {pkg.features.map((feature, index) => ( -
  • - - {feature} -
  • - ))} -
-
- - {/* Limitations */} - {pkg.limitations.length > 0 && ( -
-

Lưu ý:

-
    - {pkg.limitations.map((limitation, index) => ( -
  • - - {limitation} -
  • - ))} -
-
- )} - - {/* CTA Button */} - -
- ))} -
- - {/* Additional Services */} -
-

- Dịch Vụ Bổ Sung -

-
- {pricingData.additionalServices.map((service, index) => ( -
-

- {service.name} -

-
- {parseInt(service.price).toLocaleString()} VNĐ -
-
- /{service.unit} -
-

{service.description}

-
- ))} +
+
+

pricing plan

+
    +
  • + Home +
  • +
  • + +
  • +
  • Pricing Plan
  • +
+
- {/* Country Pricing */} -
-

- Phí Visa Theo Quốc Gia -

-
-
- - - - - - - - - - - {pricingData.countries.map((country, index) => ( - - - - - - - ))} - -
- Quốc gia - - Phí visa - - Thời gian xử lý - - Tỷ lệ thành công -
- {country.name} - - {parseInt(country.visaFee).toLocaleString()} VNĐ - - {country.processingTime} - - - {country.successRate} - -
+ {/* Pricing Section Start */} +
+
+
+
+
+
+
+ + pricing plan + +

+ Flexible Plans to Suit Every Traveler +

+
+

+ Choose the plan that fits your visa needs and enjoy expert + guidance every step of the way. +

+
+
+ +
+
+
+
+
+
+ +
+
+
- {/* Contact CTA */} -
-

- Cần Tư Vấn Chi Tiết? -

-

- Liên hệ với chúng tôi để được tư vấn miễn phí và nhận báo giá phù - hợp nhất -

-
- - + {/* Testimonial Section Start */} +
+
+
+
+ + What Our Clients Say + +

+ Immigration Success Stories +

+
+ + View All Review + + +
+
+
+
+
+ img +
+
+
+
+
+
+
+
+
+ + + + + +
+

+ “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.” +

+
+
+ +
+
+
Mohammed Ali,
+ Family Visa +
+
+
+
+
+
+
+ + + + + +
+

+ “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.” +

+
+
+ +
+
+
Mohammed Ali,
+ Family Visa +
+
+
+
+
+
+
+ + +
+
+
+
-
-
+ + ); }