forked from UKSOURCE/hailearning.edu.vn
feat: UI appointment contact pricing page
This commit is contained in:
@@ -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<ContactData | null>(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<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
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 (
|
||||
<div className="loading-wrapper" style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
{contactData.title}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600">{contactData.subtitle}</p>
|
||||
<>
|
||||
{/* Breadcrumb-Wrapper Section Start */}
|
||||
<section
|
||||
className="breadcrumb-wrapper fix bg-cover"
|
||||
style={{ backgroundImage: `url(${hero.backgroundImage || "/assets/img/inner-page/breadcrumb.jpg"})` }}
|
||||
>
|
||||
<div className="shape">
|
||||
<img src="/assets/img/inner-page/shape.png" alt="img" />
|
||||
</div>
|
||||
<div className="container">
|
||||
<div className="page-heading">
|
||||
<h1 className="breadcrumb-title">{hero.title}</h1>
|
||||
<ul className="breadcrumb-list">
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<i className="fa-solid fa-chevron-right"></i>
|
||||
</li>
|
||||
<li>Contact Us</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-12">
|
||||
{/* Contact Information */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
Thông Tin Liên Hệ
|
||||
</h2>
|
||||
|
||||
{/* Contact Details */}
|
||||
<div className="space-y-6 mb-8">
|
||||
{Object.entries(contactData.contactInfo).map(([key, info]) => (
|
||||
<div key={key} className="flex items-start">
|
||||
<span className="text-2xl mr-4">{info.icon}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
{info.label}
|
||||
</h3>
|
||||
<p className="text-gray-700 whitespace-pre-line">
|
||||
{info.value}
|
||||
</p>
|
||||
{/* Contact Icon Section Start */}
|
||||
<section className="contact-us-section-3 section-padding fix">
|
||||
<div className="container">
|
||||
<div className="row g-4">
|
||||
{contactCards.map((card, index) => (
|
||||
<div className="col-xl-4 col-lg-6 col-md-6" key={index}>
|
||||
<div className="contact-icon-item">
|
||||
<div className="icon">
|
||||
<i className={card.iconType}></i>
|
||||
</div>
|
||||
<div className="content">
|
||||
<p>{card.title}</p>
|
||||
<h6>
|
||||
{card.content.map((line, i) => (
|
||||
<span key={i}>
|
||||
{card.type === "email" ? (
|
||||
<a href={`mailto:${line}`}>{line}</a>
|
||||
) : card.type === "phone" ? (
|
||||
<a href={`tel:${line.replace(/\s/g, "")}`}>{line}</a>
|
||||
) : (
|
||||
line
|
||||
)}
|
||||
{i < card.content.length - 1 && <br />}
|
||||
</span>
|
||||
))}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Offices */}
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">Văn Phòng</h3>
|
||||
<div className="space-y-4">
|
||||
{contactData.offices.map((office, index) => (
|
||||
<div key={index} className="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-gray-900 mb-2">
|
||||
{office.name}
|
||||
</h4>
|
||||
<p className="text-gray-700 text-sm mb-1">
|
||||
📍 {office.address}
|
||||
</p>
|
||||
<p className="text-gray-700 text-sm mb-1">
|
||||
📞 {office.phone}
|
||||
</p>
|
||||
<p className="text-gray-700 text-sm">✉️ {office.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
Gửi Tin Nhắn
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{contactData.formFields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{field.label}
|
||||
{field.required && (
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{field.type === "textarea" ? (
|
||||
<textarea
|
||||
name={field.name}
|
||||
value={formData[field.name as keyof typeof formData]}
|
||||
onChange={handleInputChange}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
) : field.type === "select" ? (
|
||||
<select
|
||||
name={field.name}
|
||||
value={formData[field.name as keyof typeof formData]}
|
||||
onChange={handleInputChange}
|
||||
required={field.required}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Chọn dịch vụ</option>
|
||||
{field.options?.map((option, index) => (
|
||||
<option key={index} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type={field.type}
|
||||
name={field.name}
|
||||
value={formData[field.name as keyof typeof formData]}
|
||||
onChange={handleInputChange}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
>
|
||||
Gửi Tin Nhắn
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Section Start */}
|
||||
<section className="contact-section-3 section-padding fix pt-0">
|
||||
<div className="container">
|
||||
<div className="contact-from-wrapper">
|
||||
<h5 className="text-center">{form.heading}</h5>
|
||||
<p className="text-center mt-3 mb-5">"{form.description}"</p>
|
||||
|
||||
{/* Status Message */}
|
||||
{submitStatus.type && (
|
||||
<div
|
||||
className={`alert ${submitStatus.type === "success" ? "alert-success" : "alert-danger"
|
||||
} text-center mb-4`}
|
||||
>
|
||||
{submitStatus.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row g-4">
|
||||
<div className="col-xl-12">
|
||||
<form onSubmit={handleSubmit} className="contact-form-items">
|
||||
<div className="row g-4">
|
||||
{form.fields.map((field, index) => (
|
||||
<div className={field.colClass || "col-lg-12"} key={index}>
|
||||
<div className="form-clt">
|
||||
<span>{field.label}{field.required && " *"}</span>
|
||||
{field.type === "textarea" ? (
|
||||
<textarea
|
||||
name={field.name}
|
||||
value={formData[field.name as keyof typeof formData] || ""}
|
||||
onChange={handleChange}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
></textarea>
|
||||
) : (
|
||||
<input
|
||||
type={field.type}
|
||||
name={field.name}
|
||||
value={formData[field.name as keyof typeof formData] || ""}
|
||||
onChange={handleChange}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="col-lg-4">
|
||||
<button
|
||||
type="submit"
|
||||
className={form.submitButton.buttonClass}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "SENDING..." : form.submitButton.text}
|
||||
<i className={form.submitButton.icon}></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Map Section Start */}
|
||||
{map.embedUrl && (
|
||||
<div className="map-section section-padding pt-0">
|
||||
<div className="map-items">
|
||||
<div className="googpemap">
|
||||
<iframe
|
||||
src={map.embedUrl}
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user