forked from UKSOURCE/hailearning.edu.vn
Merge pull request 'fea/thanh-02022026-news' (#19) from fea/thanh-02022026-news into main
Reviewed-on: UKSOURCE/hailearning.edu.vn#19
This commit is contained in:
43
api/homeApi.ts
Normal file
43
api/homeApi.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Lấy API URL từ environment variable
|
||||||
|
*/
|
||||||
|
const getApiUrl = (): string => {
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
|
if (!apiUrl) {
|
||||||
|
console.warn('NEXT_PUBLIC_API_URL is not set. Using default http://localhost:3001');
|
||||||
|
return 'http://localhost:3001';
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch home page data từ API
|
||||||
|
* @returns Promise<any>
|
||||||
|
* @throws Error nếu fetch thất bại
|
||||||
|
*/
|
||||||
|
export const fetchHomeData = async (): Promise<any> => {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
const url = `${apiUrl}/api/home`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching home data:', error);
|
||||||
|
return null; // Trả về null để fallback sang dữ liệu local
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,9 +6,6 @@ import { fetchBlogList, fetchBlogDetail } from "@/api/blogsApi";
|
|||||||
import Sidebar from "@/app/blog/components/Sidebar";
|
import Sidebar from "@/app/blog/components/Sidebar";
|
||||||
import { getCmsImageUrl } from "@/utils";
|
import { getCmsImageUrl } from "@/utils";
|
||||||
|
|
||||||
// Force dynamic rendering to avoid build-time API calls
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
// Generate static params for all blog posts
|
// Generate static params for all blog posts
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import NewsSection from "@/app/blog/components/NewsSection";
|
|||||||
import Sidebar from "@/app/blog/components/Sidebar";
|
import Sidebar from "@/app/blog/components/Sidebar";
|
||||||
import { fetchBlogsByCategory, fetchCategoryDetail } from "@/api/blogsApi";
|
import { fetchBlogsByCategory, fetchCategoryDetail } from "@/api/blogsApi";
|
||||||
|
|
||||||
// Force dynamic rendering to avoid build-time API calls
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
interface CategoryPageProps {
|
interface CategoryPageProps {
|
||||||
params:
|
params:
|
||||||
| Promise<{
|
| Promise<{
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import Breadcrumb from "@/app/components/Breadcrumb";
|
|||||||
import NewsSection from "./components/NewsSection";
|
import NewsSection from "./components/NewsSection";
|
||||||
import { fetchBlogList } from "@/api/blogsApi";
|
import { fetchBlogList } from "@/api/blogsApi";
|
||||||
|
|
||||||
// Force dynamic rendering to avoid build-time API calls
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
interface NewsPageProps {
|
interface NewsPageProps {
|
||||||
searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string };
|
searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import NewsSection from "@/app/blog/components/NewsSection";
|
|||||||
import Sidebar from "@/app/blog/components/Sidebar";
|
import Sidebar from "@/app/blog/components/Sidebar";
|
||||||
import { fetchBlogsByTag, fetchTagDetail } from "@/api/blogsApi";
|
import { fetchBlogsByTag, fetchTagDetail } from "@/api/blogsApi";
|
||||||
|
|
||||||
// Force dynamic rendering to avoid build-time API calls
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
interface TagPageProps {
|
interface TagPageProps {
|
||||||
params:
|
params:
|
||||||
| Promise<{
|
| Promise<{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getCmsImageUrl } from '@/utils/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface BlogPreviewProps {
|
interface BlogPreviewProps {
|
||||||
@@ -46,40 +47,66 @@ const BlogPreview = ({ data }: BlogPreviewProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{data.items.map((item, index) => (
|
{data.items.map((item, index) => {
|
||||||
<div key={index} className="col-xl-4 col-lg-6 col-md-6 wow fadeInUp" data-wow-delay={`.${(index + 1) * 2 + 1}s`}>
|
const thumbUrl = getCmsImageUrl(item.thumbnail);
|
||||||
<div className="news-card-item">
|
return (
|
||||||
<div className="news-image">
|
<div key={index} className="col-xl-4 col-lg-6 col-md-6 wow fadeInUp" data-wow-delay={`.${(index + 1) * 2 + 1}s`}>
|
||||||
<img src={item.thumbnail} alt="img" />
|
<div className="news-card-item">
|
||||||
<span>{item.category}</span>
|
<div className="news-image">
|
||||||
<div className="news-layer-wrapper">
|
<img
|
||||||
<div className="news-layer-image" style={{ backgroundImage: `url('${item.thumbnail}')` }}></div>
|
src={thumbUrl}
|
||||||
<div className="news-layer-image" style={{ backgroundImage: `url('${item.thumbnail}')` }}></div>
|
alt="img"
|
||||||
<div className="news-layer-image" style={{ backgroundImage: `url('${item.thumbnail}')` }}></div>
|
style={{
|
||||||
<div className="news-layer-image" style={{ backgroundImage: `url('${item.thumbnail}')` }}></div>
|
width: '419px',
|
||||||
</div>
|
height: '312px',
|
||||||
</div>
|
objectFit: 'cover'
|
||||||
<div className="news-content">
|
}}
|
||||||
<div className="list">
|
/>
|
||||||
<span>Comment ({item.comments.toString().padStart(2, '0')})</span>
|
<span>{item.category}</span>
|
||||||
<span>_ {formatDate(item.date)}</span>
|
<div className="news-layer-wrapper">
|
||||||
</div>
|
<div className="news-layer-image" style={{ backgroundImage: `url('${thumbUrl}')`, width: '419px', height: '312px' }}></div>
|
||||||
<h3>
|
<div className="news-layer-image" style={{ backgroundImage: `url('${thumbUrl}')`, width: '419px', height: '312px' }}></div>
|
||||||
<Link href={item.link}>
|
<div className="news-layer-image" style={{ backgroundImage: `url('${thumbUrl}')`, width: '419px', height: '312px' }}></div>
|
||||||
{item.title}
|
<div className="news-layer-image" style={{ backgroundImage: `url('${thumbUrl}')`, width: '419px', height: '312px' }}></div>
|
||||||
</Link>
|
</div>
|
||||||
</h3>
|
</div>
|
||||||
<div className="news-bottom">
|
<div className="news-content">
|
||||||
<div className="info-item">
|
<div className="list">
|
||||||
<img src={item.author.avatar} alt="img" />
|
<span>Comment ({item.comments.toString().padStart(2, '0')})</span>
|
||||||
<span>By {item.author.name}</span>
|
<span>_ {formatDate(item.date)}</span>
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
<Link href={item.link}>
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<div className="news-bottom">
|
||||||
|
<div className="info-item">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
backgroundColor: '#d1d5db',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#374151',
|
||||||
|
marginRight: '10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.author.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<span>By {item.author.name}</span>
|
||||||
|
</div>
|
||||||
|
<Link href={item.link} className="link-btn">View Articles<i className="fa-solid fa-arrow-right"></i></Link>
|
||||||
</div>
|
</div>
|
||||||
<Link href={item.link} className="link-btn">View Articles<i className="fa-solid fa-arrow-right"></i></Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getCmsImageUrl } from '@/utils/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface HeroSectionProps {
|
interface HeroSectionProps {
|
||||||
@@ -20,7 +21,7 @@ interface HeroSectionProps {
|
|||||||
|
|
||||||
const HeroSection = ({ data }: HeroSectionProps) => {
|
const HeroSection = ({ data }: HeroSectionProps) => {
|
||||||
return (
|
return (
|
||||||
<section className="hero-section hero-1 fix bg-cover" style={{ backgroundImage: `url('${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">
|
||||||
<img src="/assets/img/home-1/hero/sape-2.png" alt="img" />
|
<img src="/assets/img/home-1/hero/sape-2.png" alt="img" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
import { getCmsImageUrl } from '@/utils/image';
|
||||||
|
|
||||||
interface PartnersProps {
|
interface PartnersProps {
|
||||||
data: {
|
data: {
|
||||||
heading: string;
|
visaConsultancy: {
|
||||||
items: {
|
items: {
|
||||||
name: string;
|
name: string;
|
||||||
logo: string;
|
icon: string;
|
||||||
year: string;
|
year: string;
|
||||||
}[];
|
}[];
|
||||||
|
};
|
||||||
|
brands: {
|
||||||
|
items: {
|
||||||
|
logo: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,11 +24,11 @@ const Partners = ({ data }: PartnersProps) => {
|
|||||||
<section className="visa-consultency-section section-padding fix">
|
<section className="visa-consultency-section section-padding fix">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row g-4">
|
<div className="row g-4">
|
||||||
{data.items.slice(0, 4).map((partner, index) => (
|
{(data.visaConsultancy?.items || []).map((partner, index) => (
|
||||||
<div key={index} className="col-xl-3 col-lg-4 col-md-6">
|
<div key={index} className="col-xl-3 col-lg-4 col-md-6">
|
||||||
<div className="visa-consultency-item">
|
<div className="visa-consultency-item">
|
||||||
<div className="image">
|
<div className="image">
|
||||||
<img src={partner.logo} alt="img" />
|
<img src={getCmsImageUrl(partner.icon)} alt={partner.name} />
|
||||||
</div>
|
</div>
|
||||||
<h3>{partner.name}</h3>
|
<h3>{partner.name}</h3>
|
||||||
<h6>{partner.year}</h6>
|
<h6>{partner.year}</h6>
|
||||||
@@ -38,10 +46,10 @@ const Partners = ({ data }: PartnersProps) => {
|
|||||||
<div className="brand-item">
|
<div className="brand-item">
|
||||||
<div className="swiper brand-slider">
|
<div className="swiper brand-slider">
|
||||||
<div className="swiper-wrapper">
|
<div className="swiper-wrapper">
|
||||||
{data.items.slice(4).map((partner, index) => (
|
{(data.brands?.items || []).map((brand, index) => (
|
||||||
<div key={index} className="swiper-slide">
|
<div key={index} className="swiper-slide">
|
||||||
<div className="brand-image text-center">
|
<div className="brand-image text-center">
|
||||||
<img src={partner.logo} alt={partner.name} />
|
<img src={getCmsImageUrl(brand.logo)} alt="brand-logo" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@
|
|||||||
"subheading": "Did You Know",
|
"subheading": "Did You Know",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"value": "1000",
|
"value": "1",
|
||||||
"suffix": "k+",
|
"suffix": "k+",
|
||||||
"label": "Students Guided",
|
"label": "Students Guided",
|
||||||
"description": "Successfully assisted over a thousand students worldwide."
|
"description": "Successfully assisted over a thousand students worldwide."
|
||||||
@@ -238,54 +238,39 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"partners": {
|
"partners": {
|
||||||
"heading": "Our Trusted Partners",
|
"visaConsultancy": {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "Best Visa Consultancy",
|
"name": "Best Visa Consultancy",
|
||||||
"logo": "/assets/img/home-1/feature/icon-1.png",
|
"icon": "/assets/img/home-1/feature/icon-1.png",
|
||||||
"year": "2025"
|
"year": "2025"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Visa Success Award",
|
"name": "Visa Success Award",
|
||||||
"logo": "/assets/img/home-1/feature/icon-2.png",
|
"icon": "/assets/img/home-1/feature/icon-2.png",
|
||||||
"year": "2025"
|
"year": "2025"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Innovation Award",
|
"name": "Innovation Award",
|
||||||
"logo": "/assets/img/home-1/feature/icon-3.png",
|
"icon": "/assets/img/home-1/feature/icon-3.png",
|
||||||
"year": "2025"
|
"year": "2025"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Global Education Partner",
|
"name": "Global Education Partner",
|
||||||
"logo": "/assets/img/home-1/feature/icon-4.png",
|
"icon": "/assets/img/home-1/feature/icon-4.png",
|
||||||
"year": "2025"
|
"year": "2025"
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
"name": "University Partner 1",
|
},
|
||||||
"logo": "/assets/img/home-1/brand/01.png",
|
"brands": {
|
||||||
"year": "2025"
|
"items": [
|
||||||
},
|
{ "logo": "/assets/img/home-1/brand/01.png" },
|
||||||
{
|
{ "logo": "/assets/img/home-1/brand/02.png" },
|
||||||
"name": "University Partner 2",
|
{ "logo": "/assets/img/home-1/brand/03.png" },
|
||||||
"logo": "/assets/img/home-1/brand/02.png",
|
{ "logo": "/assets/img/home-1/brand/04.png" },
|
||||||
"year": "2025"
|
{ "logo": "/assets/img/home-1/brand/05.png" }
|
||||||
},
|
]
|
||||||
{
|
}
|
||||||
"name": "University Partner 3",
|
|
||||||
"logo": "/assets/img/home-1/brand/03.png",
|
|
||||||
"year": "2025"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "University Partner 4",
|
|
||||||
"logo": "/assets/img/home-1/brand/04.png",
|
|
||||||
"year": "2025"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "University Partner 5",
|
|
||||||
"logo": "/assets/img/home-1/brand/05.png",
|
|
||||||
"year": "2025"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"blogPreview": {
|
"blogPreview": {
|
||||||
"heading": "Latest Insights & Updates",
|
"heading": "Latest Insights & Updates",
|
||||||
|
|||||||
45
app/page.tsx
45
app/page.tsx
@@ -8,21 +8,42 @@ import FAQSection from './components/home/FAQSection';
|
|||||||
import Achievements from './components/home/Achievements';
|
import Achievements from './components/home/Achievements';
|
||||||
import Partners from './components/home/Partners';
|
import Partners from './components/home/Partners';
|
||||||
import BlogPreview from './components/home/BlogPreview';
|
import BlogPreview from './components/home/BlogPreview';
|
||||||
import homeData from './home.json';
|
import localHomeData from './home.json';
|
||||||
|
import { fetchHomeData } from '@/api';
|
||||||
|
import { getCmsImageUrl } from '@/utils/image';
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
// Fetch home data (blog aggregation is now handled by the backend)
|
||||||
|
const apiHomeData = await fetchHomeData();
|
||||||
|
|
||||||
|
// Use API home data if available, otherwise fallback to local JSON
|
||||||
|
const data = JSON.parse(JSON.stringify(apiHomeData || localHomeData));
|
||||||
|
|
||||||
|
// Process Hero Image URL
|
||||||
|
if (data.hero?.backgroundImage) {
|
||||||
|
data.hero.backgroundImage = getCmsImageUrl(data.hero.backgroundImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process blog images from CMS (thumbnail normalization)
|
||||||
|
if (data.blogPreview?.items) {
|
||||||
|
data.blogPreview.items = data.blogPreview.items.map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
thumbnail: getCmsImageUrl(item.thumbnail) || '/assets/img/home-1/news/01.jpg'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeroSection data={homeData.hero} />
|
<HeroSection data={data.hero} />
|
||||||
<WhyChooseUs data={homeData.whyChooseUs} />
|
<WhyChooseUs data={data.whyChooseUs} />
|
||||||
<VisaSolutions data={homeData.visaSolutions} />
|
<VisaSolutions data={data.visaSolutions} />
|
||||||
<VisaCountries data={homeData.visaCountries} />
|
<VisaCountries data={data.visaCountries} />
|
||||||
<Testimonials data={homeData.testimonials} />
|
<Testimonials data={data.testimonials} />
|
||||||
<VideoGallery data={homeData.videoGallery} />
|
<VideoGallery data={data.videoGallery} />
|
||||||
<FAQSection data={homeData.faq} />
|
<FAQSection data={data.faq} />
|
||||||
<Achievements data={homeData.achievements} />
|
<Achievements data={data.achievements} />
|
||||||
<Partners data={homeData.partners} />
|
<Partners data={data.partners} />
|
||||||
<BlogPreview data={homeData.blogPreview} />
|
<BlogPreview data={data.blogPreview} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.13.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user