forked from UKSOURCE/hailearning.edu.vn
feat: refactor service API schema and update layout structure
This commit is contained in:
@@ -8,7 +8,6 @@
|
|||||||
======================= */
|
======================= */
|
||||||
|
|
||||||
export interface ServiceItem {
|
export interface ServiceItem {
|
||||||
id: string;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -58,7 +57,6 @@ export interface DestinationSection {
|
|||||||
subTitle: string;
|
subTitle: string;
|
||||||
mainTitle: string;
|
mainTitle: string;
|
||||||
};
|
};
|
||||||
items: CountryItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisaItem {
|
export interface VisaItem {
|
||||||
@@ -90,11 +88,6 @@ export interface ReviewSection {
|
|||||||
subTitle: string;
|
subTitle: string;
|
||||||
mainTitle: string;
|
mainTitle: string;
|
||||||
};
|
};
|
||||||
viewAllButton: {
|
|
||||||
text: string;
|
|
||||||
icon: string;
|
|
||||||
link: string;
|
|
||||||
};
|
|
||||||
thumb: string;
|
thumb: string;
|
||||||
items: ClientReviewItem[];
|
items: ClientReviewItem[];
|
||||||
}
|
}
|
||||||
@@ -251,7 +244,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
|
|||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "immigration-appeal",
|
|
||||||
slug: "immigration-appeal",
|
slug: "immigration-appeal",
|
||||||
name: "Immigration Appeal & Legal Support",
|
name: "Immigration Appeal & Legal Support",
|
||||||
description:
|
description:
|
||||||
@@ -311,7 +303,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "scholarship-guidance",
|
|
||||||
slug: "scholarship-guidance",
|
slug: "scholarship-guidance",
|
||||||
name: "Scholarship & Study Grant Guidance",
|
name: "Scholarship & Study Grant Guidance",
|
||||||
description:
|
description:
|
||||||
@@ -373,7 +364,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "permanent-residency",
|
|
||||||
slug: "permanent-residency",
|
slug: "permanent-residency",
|
||||||
name: "Permanent Residency (PR) Services",
|
name: "Permanent Residency (PR) Services",
|
||||||
description:
|
description:
|
||||||
@@ -435,7 +425,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "citizenship-naturalization",
|
|
||||||
slug: "citizenship-naturalization",
|
slug: "citizenship-naturalization",
|
||||||
name: "Citizenship & Naturalization Guidance",
|
name: "Citizenship & Naturalization Guidance",
|
||||||
description:
|
description:
|
||||||
@@ -505,52 +494,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
|
|||||||
subTitle: "Countries we offer",
|
subTitle: "Countries we offer",
|
||||||
mainTitle: "Choose Your Immigration Destination",
|
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: {
|
visas: {
|
||||||
@@ -590,11 +533,6 @@ export const getFallbackServicePageData = (): ServicePageData => ({
|
|||||||
subTitle: "What Our Clients Say",
|
subTitle: "What Our Clients Say",
|
||||||
mainTitle: "Immigration Success Stories",
|
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",
|
thumb: "/assets/img/home-3/test-thumb.jpg",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
|
// import Header from "./components/Header";
|
||||||
import Header from "./components/Header";
|
// import Footer from "./components/Footer";
|
||||||
import Footer from "./components/Footer";
|
|
||||||
import Loader from "./components/Loader";
|
import Loader from "./components/Loader";
|
||||||
import BackToTop from "./components/BackToTop";
|
import BackToTop from "./components/BackToTop";
|
||||||
import MouseCursor from "./components/MouseCursor";
|
import MouseCursor from "./components/MouseCursor";
|
||||||
@@ -48,18 +47,33 @@ export default function RootLayout({
|
|||||||
<Loader />
|
<Loader />
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
<MouseCursor />
|
<MouseCursor />
|
||||||
<Header />
|
{/* <Header /> */}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<Footer />
|
{/* <Footer /> */}
|
||||||
|
|
||||||
{/* Scripts */}
|
{/* Scripts */}
|
||||||
<Script src="/assets/js/jquery-3.7.1.min.js" strategy="beforeInteractive" />
|
<Script
|
||||||
<Script src="/assets/js/viewport.jquery.js" strategy="afterInteractive" />
|
src="/assets/js/jquery-3.7.1.min.js"
|
||||||
<Script src="/assets/js/bootstrap.bundle.min.js" strategy="afterInteractive" />
|
strategy="beforeInteractive"
|
||||||
<Script src="/assets/js/jquery.nice-select.min.js" strategy="afterInteractive" />
|
/>
|
||||||
<Script src="/assets/js/jquery.waypoints.js" strategy="afterInteractive" />
|
<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/odometer.min.js" strategy="afterInteractive" />
|
||||||
<Script
|
<Script
|
||||||
src="/assets/js/swiper-bundle.min.js"
|
src="/assets/js/swiper-bundle.min.js"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export async function generateMetadata({
|
|||||||
try {
|
try {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const data = await fetchServiceBySlug(slug);
|
const data = await fetchServiceBySlug(slug);
|
||||||
|
|
||||||
if (!data || !data.serviceDetails) {
|
if (!data || !data.serviceDetails) {
|
||||||
return {
|
return {
|
||||||
title: "Service Not Found",
|
title: "Service Not Found",
|
||||||
@@ -112,6 +111,10 @@ export default async function ServiceDetailsPage({
|
|||||||
<div className="accordion" id="accordionExample">
|
<div className="accordion" id="accordionExample">
|
||||||
{faq.items.map((faqItem: any, index: number) => {
|
{faq.items.map((faqItem: any, index: number) => {
|
||||||
const isExpanded = faqItem.isExpanded;
|
const isExpanded = faqItem.isExpanded;
|
||||||
|
const questionNumber = String(index + 1).padStart(
|
||||||
|
2,
|
||||||
|
"0",
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`faq-${index}`}
|
key={`faq-${index}`}
|
||||||
@@ -132,7 +135,7 @@ export default async function ServiceDetailsPage({
|
|||||||
}
|
}
|
||||||
aria-controls={`collapse${index}`}
|
aria-controls={`collapse${index}`}
|
||||||
>
|
>
|
||||||
{faqItem.question}
|
{questionNumber}. {faqItem.question}
|
||||||
</button>
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -11,6 +11,51 @@ export const metadata: Metadata = {
|
|||||||
export default async function ServicesPage() {
|
export default async function ServicesPage() {
|
||||||
const data = await fetchServicePageData();
|
const data = await fetchServicePageData();
|
||||||
const { services, destinations, visas, reviews } = data;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,7 +144,7 @@ export default async function ServicesPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</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">
|
<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 key={country.id} className="col destination-offer-item">
|
||||||
<div className="choose-us-image">
|
<div className="choose-us-image">
|
||||||
<img src={imageUrl(country.image)} alt="img" />
|
<img src={imageUrl(country.image)} alt="img" />
|
||||||
@@ -122,7 +167,7 @@ export default async function ServicesPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Service-Visa Section Start */}
|
{/* Service-Visa Section Start */}
|
||||||
<section className="service-visa-section fix">
|
{/* <section className="service-visa-section fix">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="service-visa-wrapper">
|
<div className="service-visa-wrapper">
|
||||||
{visas.items.map((visa: any, index: number) => (
|
{visas.items.map((visa: any, index: number) => (
|
||||||
@@ -144,7 +189,7 @@ export default async function ServicesPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section> */}
|
||||||
|
|
||||||
{/* Testimonial Section3 Start */}
|
{/* Testimonial Section3 Start */}
|
||||||
<section className="testimonial-section section-padding fix">
|
<section className="testimonial-section section-padding fix">
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
export const imageUrl = (path?: string) => {
|
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;
|
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
BIN
public/_images/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Reference in New Issue
Block a user