Merge pull request 'feat: Refactor home page components' (#21) from fea/thanh-02022026-news into main

Reviewed-on: UKSOURCE/hailearning.edu.vn#21
This commit is contained in:
2026-02-05 14:21:34 +00:00
6 changed files with 149 additions and 64 deletions

View File

@@ -46,11 +46,11 @@ const FAQSection = ({ data }: FAQSectionProps) => {
<div key={index} className="accordion-item wow fadeInUp" data-wow-delay={`.${(index + 1) * 2}s`}> <div key={index} className="accordion-item wow fadeInUp" data-wow-delay={`.${(index + 1) * 2}s`}>
<h5 className="accordion-header" id={`heading${index}`}> <h5 className="accordion-header" id={`heading${index}`}>
<button <button
className={`accordion-button ${index !== 1 ? 'collapsed' : ''}`} className="accordion-button collapsed"
type="button" type="button"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target={`#collapse${index}`} data-bs-target={`#collapse${index}`}
aria-expanded={index === 1 ? 'true' : 'false'} aria-expanded="false"
aria-controls={`collapse${index}`} aria-controls={`collapse${index}`}
> >
{item.question} {item.question}
@@ -58,7 +58,7 @@ const FAQSection = ({ data }: FAQSectionProps) => {
</h5> </h5>
<div <div
id={`collapse${index}`} id={`collapse${index}`}
className={`accordion-collapse collapse ${index === 1 ? 'show' : ''}`} className="accordion-collapse collapse"
aria-labelledby={`heading${index}`} aria-labelledby={`heading${index}`}
data-bs-parent="#accordionExample" data-bs-parent="#accordionExample"
> >

View File

@@ -1,25 +1,60 @@
import { getCmsImageUrl } from '@/utils/image'; import { getCmsImageUrl } from '@/utils/image';
import Link from 'next/link'; import Link from 'next/link';
interface HeroSlide {
title: string;
subtitle: string;
description: string;
primaryButton: {
label: string;
href: string;
};
secondaryButton: {
label: string;
href: string;
};
heroImage?: string;
videoUrl: string;
}
interface HeroSectionProps { interface HeroSectionProps {
data: { data: {
title: string;
subtitle: string;
description: string;
primaryButton: {
label: string;
href: string;
};
secondaryButton: {
label: string;
href: string;
};
backgroundImage: string; backgroundImage: string;
videoUrl: string; // Optional multi-slide support from CMS
slides?: HeroSlide[];
// Legacy single-slide fields (fallback)
title?: string;
subtitle?: string;
description?: string;
primaryButton?: {
label: string;
href: string;
};
secondaryButton?: {
label: string;
href: string;
};
heroImage?: string;
videoUrl?: string;
}; };
} }
const HeroSection = ({ data }: HeroSectionProps) => { const HeroSection = ({ data }: HeroSectionProps) => {
const slides: HeroSlide[] =
(data.slides && data.slides.length > 0)
? data.slides
: [{
title: data.title || '',
subtitle: data.subtitle || '',
description: data.description || '',
primaryButton: data.primaryButton || { label: '', href: '#' },
secondaryButton: data.secondaryButton || { label: '', href: '#' },
heroImage: data.heroImage,
videoUrl: data.videoUrl || '',
}];
const firstSlide = slides[0];
return ( return (
<section className="hero-section hero-1 fix bg-cover" style={{ backgroundImage: `url('${getCmsImageUrl(data.backgroundImage)}')` }}> <section className="hero-section hero-1 fix bg-cover" style={{ backgroundImage: `url('${getCmsImageUrl(data.backgroundImage)}')` }}>
<div className="left-shape"> <div className="left-shape">
@@ -49,46 +84,51 @@ const HeroSection = ({ data }: HeroSectionProps) => {
<div className="col-lg-6"> <div className="col-lg-6">
<div className="swiper hero-slider"> <div className="swiper hero-slider">
<div className="swiper-wrapper"> <div className="swiper-wrapper">
<div className="swiper-slide"> {slides.map((slide, index) => (
<div className="hero-content"> <div className="swiper-slide" key={index}>
<h6>{data.subtitle}</h6> <div className="hero-content">
<h1> <h6>{slide.subtitle}</h6>
{data.title} <h1>
<a href={data.videoUrl} className="video-btn video-popup"> {slide.title}
<i className="fa-solid fa-play"></i> {slide.videoUrl && (
</a> <a href={slide.videoUrl} className="video-btn video-popup">
</h1> <i className="fa-solid fa-play"></i>
<p> </a>
{data.description} )}
</p> </h1>
<div className="hero-button"> <p>
<Link href={data.primaryButton.href} className="theme-btn"> {slide.description}
{data.primaryButton.label} </p>
<i className="fa-solid fa-arrow-right"></i> <div className="hero-button">
</Link> {slide.primaryButton?.href && (
<Link href={data.secondaryButton.href} className="theme-btn style-2"> <Link href={slide.primaryButton.href} className="theme-btn">
{data.secondaryButton.label} {slide.primaryButton.label}
<i className="fa-solid fa-arrow-right"></i> <i className="fa-solid fa-arrow-right"></i>
</Link> </Link>
)}
{slide.secondaryButton?.href && (
<Link href={slide.secondaryButton.href} className="theme-btn style-2">
{slide.secondaryButton.label}
<i className="fa-solid fa-arrow-right"></i>
</Link>
)}
</div>
</div> </div>
</div> </div>
</div> ))}
</div> </div>
</div> </div>
</div> </div>
<div className="col-lg-6"> <div className="col-lg-6">
<div className="swiper image-slider"> <div className="swiper image-slider">
<div className="swiper-wrapper"> <div className="swiper-wrapper">
<div className="swiper-slide"> {slides.map((slide, index) => (
<div className="hero-image"> <div className="swiper-slide" key={index}>
<img src="/assets/img/home-1/hero/man.png" alt="img" /> <div className="hero-image">
<img src={slide.heroImage || firstSlide.heroImage || "/assets/img/home-1/hero/man.png"} alt="img" />
</div>
</div> </div>
</div> ))}
<div className="swiper-slide">
<div className="hero-image">
<img src="/assets/img/home-1/hero/man.png" alt="img" />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,8 +3,11 @@ import Link from 'next/link';
interface WhyChooseUsProps { interface WhyChooseUsProps {
data: { data: {
heading: string; heading: string;
highlightWord?: string;
subheading: string; subheading: string;
description: string; description: string;
mainImage?: string;
secondaryImage?: string;
items: { items: {
icon: string; icon: string;
title: string; title: string;
@@ -19,6 +22,25 @@ interface WhyChooseUsProps {
} }
const WhyChooseUs = ({ data }: WhyChooseUsProps) => { const WhyChooseUs = ({ data }: WhyChooseUsProps) => {
const highlight = data.highlightWord?.trim();
let headingContent: React.ReactNode = data.heading;
if (highlight) {
const index = data.heading.indexOf(highlight);
if (index !== -1) {
const before = data.heading.slice(0, index);
const after = data.heading.slice(index + highlight.length);
headingContent = (
<>
{before}
<span>{highlight}</span>
{after}
</>
);
}
}
return ( return (
<section className="about-section section-padding fix pb-0"> <section className="about-section section-padding fix pb-0">
<div className="top-shape"> <div className="top-shape">
@@ -29,9 +51,9 @@ const WhyChooseUs = ({ data }: WhyChooseUsProps) => {
<div className="row g-4"> <div className="row g-4">
<div className="col-lg-6"> <div className="col-lg-6">
<div className="about-image"> <div className="about-image">
<img src="/assets/img/home-1/about/about-1.jpg" alt="img" className="wow img-custom-anim-left" /> <img src={data.mainImage || "/assets/img/home-1/about/about-1.jpg"} alt="img" className="wow img-custom-anim-left" />
<div className="about-image-2"> <div className="about-image-2">
<img src="/assets/img/home-1/about/about-02.jpg" alt="img" className="wow img-custom-anim-right" /> <img src={data.secondaryImage || "/assets/img/home-1/about/about-02.jpg"} alt="img" className="wow img-custom-anim-right" />
</div> </div>
<div className="bg-shape"> <div className="bg-shape">
<img src="/assets/img/home-1/about/Vector.png" alt="img" /> <img src="/assets/img/home-1/about/Vector.png" alt="img" />
@@ -49,7 +71,7 @@ const WhyChooseUs = ({ data }: WhyChooseUsProps) => {
<div className="section-title mb-0"> <div className="section-title mb-0">
<span className="sub-title wow fadeInUp">{data.subheading}</span> <span className="sub-title wow fadeInUp">{data.subheading}</span>
<h2 className="split-text-right split-text-in-right"> <h2 className="split-text-right split-text-in-right">
{data.heading.split(' Dreams ')[0]} <span>Dreams</span> {data.heading.split(' Dreams ')[1]} {headingContent}
</h2> </h2>
</div> </div>
<p className="text wow fadeInUp" data-wow-delay=".3s"> <p className="text wow fadeInUp" data-wow-delay=".3s">

View File

@@ -22,7 +22,13 @@ const FooterBottom = () => {
loadFooterData(); loadFooterData();
}, []); }, []);
const { bottom } = data; // Ensure we always have a valid `bottom` object, even if API shape changes
const bottom = data?.bottom || footerData.bottom;
// If bottom is still missing, avoid rendering to prevent runtime errors
if (!bottom) {
return null;
}
return ( return (
<div className="footer-bottom"> <div className="footer-bottom">

View File

@@ -22,10 +22,19 @@ const FooterTop = () => {
loadFooterData(); loadFooterData();
}, []); }, []);
const { top } = data; // Ensure we always have a valid `top` object, even if API shape changes
const top = data?.top || footerData.top;
// If for some reason `top` is still missing, avoid rendering to prevent runtime errors
if (!top) {
return null;
}
return ( return (
<footer className="footer-section fix bg-cover" style={{ backgroundImage: `url('${top.bgImage}')` }}> <footer
className="footer-section fix bg-cover"
style={top.bgImage ? { backgroundImage: `url('${top.bgImage}')` } : undefined}
>
<div className="container"> <div className="container">
<div className="footer-wrapper"> <div className="footer-wrapper">
<div className="row"> <div className="row">

View File

@@ -1,22 +1,30 @@
{ {
"hero": { "hero": {
"title": "From Application to Visa We've Got You Covered",
"subtitle": "Global Education Simplified",
"description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.",
"primaryButton": {
"label": "Apply now",
"href": "/contact"
},
"secondaryButton": {
"label": "Book Free Consultation",
"href": "/contact"
},
"backgroundImage": "/assets/img/home-1/hero/bg.jpg", "backgroundImage": "/assets/img/home-1/hero/bg.jpg",
"videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I" "slides": [
{
"title": "From Application to Visa We've Got You Covered",
"subtitle": "Global Education Simplified",
"description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.",
"primaryButton": {
"label": "Apply now",
"href": "/contact"
},
"secondaryButton": {
"label": "Book Free Consultation",
"href": "/contact"
},
"heroImage": "/assets/img/home-1/hero/man.png",
"videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I"
}
]
}, },
"whyChooseUs": { "whyChooseUs": {
"heading": "Turning Study Abroad Dreams Into Reality", "heading": "Turning Study Abroad Dreams Into Reality",
"highlightWord": "Dreams",
"mainImage": "/assets/img/home-1/about/about-1.jpg",
"secondaryImage": "/assets/img/home-1/about/about-02.jpg",
"subheading": "About Our Consultancy", "subheading": "About Our Consultancy",
"description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.", "description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.",
"items": [ "items": [