Merge pull request 'feat: refactor service API schema and update layout structure' (#8) from fix/bao-03022026-Page-Service-Service-Detail into main

Reviewed-on: UKSOURCE/hailearning.edu.vn#8
This commit is contained in:
2026-02-04 02:23:52 +00:00
6 changed files with 89 additions and 77 deletions

View File

@@ -8,7 +8,6 @@
======================= */
export interface ServiceItem {
id: string;
slug: string;
name: string;
description: string;
@@ -58,7 +57,6 @@ export interface DestinationSection {
subTitle: string;
mainTitle: string;
};
items: CountryItem[];
}
export interface VisaItem {
@@ -90,11 +88,6 @@ export interface ReviewSection {
subTitle: string;
mainTitle: string;
};
viewAllButton: {
text: string;
icon: string;
link: string;
};
thumb: string;
items: ClientReviewItem[];
}
@@ -251,7 +244,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
},
items: [
{
id: "immigration-appeal",
slug: "immigration-appeal",
name: "Immigration Appeal & Legal Support",
description:
@@ -311,7 +303,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
},
},
{
id: "scholarship-guidance",
slug: "scholarship-guidance",
name: "Scholarship & Study Grant Guidance",
description:
@@ -373,7 +364,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
},
},
{
id: "permanent-residency",
slug: "permanent-residency",
name: "Permanent Residency (PR) Services",
description:
@@ -435,7 +425,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
},
},
{
id: "citizenship-naturalization",
slug: "citizenship-naturalization",
name: "Citizenship & Naturalization Guidance",
description:
@@ -505,52 +494,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
subTitle: "Countries we offer",
mainTitle: "Choose Your Immigration Destination",
},
items: [
{
id: "canada",
name: "Canada",
description:
"Canada provides quality education, rich and global opportunities",
image: "/assets/img/home-3/choose-us/01.jpg",
icon: "/assets/img/home-3/choose-us/icon-1.png",
link: "/countries/canada",
},
{
id: "south-korea",
name: "South Korea",
description:
"South Korea offers advanced technology and cultural experiences",
image: "/assets/img/home-3/choose-us/02.jpg",
icon: "/assets/img/home-3/choose-us/icon-2.png",
link: "/countries/south-korea",
},
{
id: "france",
name: "France",
description: "France offers unique cultural experiences and education",
image: "/assets/img/home-3/choose-us/03.jpg",
icon: "/assets/img/home-3/choose-us/icon-3.png",
link: "/countries/france",
},
{
id: "uk",
name: "UK",
description:
"UK provides world-class education and career opportunities",
image: "/assets/img/home-3/choose-us/04.jpg",
icon: "/assets/img/home-3/choose-us/icon-2.png",
link: "/countries/uk",
},
{
id: "germany",
name: "Germany",
description:
"Germany offers excellent education and work opportunities",
image: "/assets/img/home-3/choose-us/05.jpg",
icon: "/assets/img/home-3/choose-us/icon-3.png",
link: "/countries/germany",
},
],
},
visas: {
@@ -590,11 +533,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
subTitle: "What Our Clients Say",
mainTitle: "Immigration Success Stories",
},
viewAllButton: {
text: "View All Review",
icon: "fa-solid fa-arrow-right",
link: "/contact",
},
thumb: "/assets/img/home-3/test-thumb.jpg",
items: [
{

View File

@@ -1,7 +1,10 @@
import type { Metadata } from "next";
import "./globals.css";
<<<<<<< HEAD
=======
>>>>>>> 64845c60b32397ad46cc4109974ab616fe186836
// import Header from "./components/Header";
// import Footer from "./components/Footer";
import Loader from "./components/Loader";
@@ -35,7 +38,7 @@ export default function RootLayout({
<link rel="stylesheet" href="/assets/css/magnific-popup.css" />
{/* MeanMenu.css */}
<link rel="stylesheet" href="/assets/css/meanmenu.css" />
{/* Odometer.css */}
{/* Odometer.css */}
<link rel="stylesheet" href="/assets/css/odometer.css" />
{/* Swiper Bundle.css */}
<link rel="stylesheet" href="/assets/css/swiper-bundle.min.css" />
@@ -44,7 +47,7 @@ export default function RootLayout({
{/* Main.css */}
<link rel="stylesheet" href="/assets/css/main.css" />
</head>
<body className="smooth-scroll-yes">
<body className="smooth-scroll-yes">
<Loader />
<BackToTop />
<MouseCursor />
@@ -55,11 +58,26 @@ export default function RootLayout({
{/* <Footer /> */}
{/* Scripts */}
<Script src="/assets/js/jquery-3.7.1.min.js" strategy="beforeInteractive" />
<Script src="/assets/js/viewport.jquery.js" strategy="afterInteractive" />
<Script src="/assets/js/bootstrap.bundle.min.js" strategy="afterInteractive" />
<Script src="/assets/js/jquery.nice-select.min.js" strategy="afterInteractive" />
<Script src="/assets/js/jquery.waypoints.js" strategy="afterInteractive" />
<Script
src="/assets/js/jquery-3.7.1.min.js"
strategy="beforeInteractive"
/>
<Script
src="/assets/js/viewport.jquery.js"
strategy="afterInteractive"
/>
<Script
src="/assets/js/bootstrap.bundle.min.js"
strategy="afterInteractive"
/>
<Script
src="/assets/js/jquery.nice-select.min.js"
strategy="afterInteractive"
/>
<Script
src="/assets/js/jquery.waypoints.js"
strategy="afterInteractive"
/>
<Script src="/assets/js/odometer.min.js" strategy="afterInteractive" />
<Script
src="/assets/js/swiper-bundle.min.js"

View File

@@ -16,7 +16,6 @@ export async function generateMetadata({
try {
const { slug } = await params;
const data = await fetchServiceBySlug(slug);
if (!data || !data.serviceDetails) {
return {
title: "Service Not Found",
@@ -112,6 +111,10 @@ export default async function ServiceDetailsPage({
<div className="accordion" id="accordionExample">
{faq.items.map((faqItem: any, index: number) => {
const isExpanded = faqItem.isExpanded;
const questionNumber = String(index + 1).padStart(
2,
"0",
);
return (
<div
key={`faq-${index}`}
@@ -132,7 +135,7 @@ export default async function ServiceDetailsPage({
}
aria-controls={`collapse${index}`}
>
{faqItem.question}
{questionNumber}. {faqItem.question}
</button>
</h5>
<div

View File

@@ -11,6 +11,51 @@ export const metadata: Metadata = {
export default async function ServicesPage() {
const data = await fetchServicePageData();
const { services, destinations, visas, reviews } = data;
const country = [
{
id: "canada",
name: "Canada",
description:
"Canada provides quality education, rich culture and global opportunities",
image: "img/home-3/choose-us/01.jpg",
icon: "img/home-3/choose-us/icon-1.png",
link: "country-details.html",
},
{
id: "south-korea",
name: "South Korea",
description:
"South Korea offers advanced technology and cultural experiences",
image: "img/home-3/choose-us/02.jpg",
icon: "img/home-3/choose-us/icon-2.png",
link: "country-details.html",
},
{
id: "france",
name: "France",
description:
"France offers rich cultural heritage and educational excellence",
image: "img/home-3/choose-us/03.jpg",
icon: "img/home-3/choose-us/icon-3.png",
link: "country-details.html",
},
{
id: "uk",
name: "UK",
description: "UK provides world-class education and career opportunities",
image: "img/home-3/choose-us/04.jpg",
icon: "img/home-3/choose-us/icon-2.png",
link: "country-details.html",
},
{
id: "germany",
name: "Germany",
description: "Germany offers excellent education and strong economy",
image: "img/home-3/choose-us/05.jpg",
icon: "img/home-3/choose-us/icon-3.png",
link: "country-details.html",
},
];
return (
<>
@@ -99,7 +144,7 @@ export default async function ServicesPage() {
</h2>
</div>
<div className="destination-offer-wrapper-3 fade-up-anim row g-4 g-xl-4 row-cols-xl-5 row-cols-lg-4 row-cols-md-2 row-cols-1">
{destinations.items.map((country: any) => (
{country.map((country: any) => (
<div key={country.id} className="col destination-offer-item">
<div className="choose-us-image">
<img src={imageUrl(country.image)} alt="img" />
@@ -122,7 +167,7 @@ export default async function ServicesPage() {
</section>
{/* Service-Visa Section Start */}
<section className="service-visa-section fix">
{/* <section className="service-visa-section fix">
<div className="container">
<div className="service-visa-wrapper">
{visas.items.map((visa: any, index: number) => (
@@ -144,7 +189,7 @@ export default async function ServicesPage() {
))}
</div>
</div>
</section>
</section> */}
{/* Testimonial Section3 Start */}
<section className="testimonial-section section-padding fix">

View File

@@ -1,5 +1,13 @@
export const imageUrl = (path?: string) => {
if (!path) return "";
// Không có ảnh → ảnh mặc định
if (!path) return "/_images/default.jpg";
// Đã là full URL
if (path.startsWith("http")) return path;
return `${process.env.NEXT_PUBLIC_API_URL}/${path}`;
const base = (
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"
).replace(/\/$/, "");
return `${base}/${path.replace(/^\//, "")}`;
};

BIN
public/_images/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB