feat: Add service details page with API integration

This commit is contained in:
nguyenvanbao
2026-02-03 10:18:53 +07:00
parent 8d105dda9c
commit 9f67fd44ef
8 changed files with 1136 additions and 71 deletions

View File

@@ -0,0 +1,31 @@
interface BreadcrumbProps {
title: string;
current: string;
}
export default function Breadcrumb({ title, current }: BreadcrumbProps) {
return (
<section
className="breadcrumb-wrapper fix bg-cover"
style={{ backgroundImage: "url(/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">{title}</h1>
<ul className="breadcrumb-list">
<li>
<a href="/">Home</a>
</li>
<li>
<i className="fa-solid fa-chevron-right"></i>
</li>
<li>{current}</li>
</ul>
</div>
</div>
</section>
);
}

View File

@@ -1 +1,8 @@
@import "tailwindcss";
@import "tailwindcss";
.collapse {
visibility: visible !important;
}
.collapse.show {
visibility: visible;
}

View File

@@ -36,19 +36,46 @@ export default function RootLayout({
<main className="min-h-screen">{children}</main>
<Footer />
{/* Nhúng các script JS giống news.html */}
<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" strategy="afterInteractive" />
<Script src="/assets/js/jquery.meanmenu.min.js" strategy="afterInteractive" />
<Script src="/assets/js/jquery.magnific-popup.min.js" strategy="afterInteractive" />
<Script
src="/assets/js/swiper-bundle.min.js"
strategy="afterInteractive"
/>
<Script
src="/assets/js/jquery.meanmenu.min.js"
strategy="afterInteractive"
/>
<Script
src="/assets/js/jquery.magnific-popup.min.js"
strategy="afterInteractive"
/>
<Script src="/assets/js/wow.min.js" strategy="afterInteractive" />
<Script src="/assets/js/gsap.js" strategy="afterInteractive" />
<Script src="/assets/js/lenis.min.js" strategy="afterInteractive" />
<Script src="/assets/js/ScrollTrigger.min.js" strategy="afterInteractive" />
<Script
src="/assets/js/ScrollTrigger.min.js"
strategy="afterInteractive"
/>
<Script src="/assets/js/SplitText.min.js" strategy="afterInteractive" />
<Script src="/assets/js/main.js" strategy="afterInteractive" />
</body>

View File

@@ -0,0 +1,56 @@
export default function NotFound() {
return (
<>
<section
className="breadcrumb-wrapper fix bg-cover"
style={{
backgroundImage: "url(/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">Service Not Found</h1>
<ul className="breadcrumb-list">
<li>
<a href="/">Home</a>
</li>
<li>
<i className="fa-solid fa-chevron-right"></i>
</li>
<li>
<a href="/services">Services</a>
</li>
<li>
<i className="fa-solid fa-chevron-right"></i>
</li>
<li>
<a href="#">Not Found</a>
</li>
</ul>
</div>
</div>
</section>
<section className="section-padding">
<div className="container">
<div className="row justify-content-center">
<div className="col-lg-8 text-center">
<h2>Service Not Found</h2>
<p className="mb-4">
The service you're looking for could not be found. Please check
the URL or return to our services page.
</p>
<a href="/services" className="theme-btn">
Back to Services
<i className="fa-solid fa-arrow-right"></i>
</a>
</div>
</div>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,170 @@
import { Metadata } from "next";
import { fetchServiceBySlug } from "../../../../api/servicesApi";
import { notFound } from "next/navigation";
import { imageUrl } from "../../../utils/image";
import Breadcrumb from "../../../components/Breadcrumb";
interface ServiceDetailsPageProps {
params: Promise<{
slug: string;
}>;
}
export async function generateMetadata({
params,
}: ServiceDetailsPageProps): Promise<Metadata> {
try {
const { slug } = await params;
const data = await fetchServiceBySlug(slug);
if (!data || !data.serviceDetails) {
return {
title: "Service Not Found",
description: "The requested service could not be found.",
};
}
return {
title: `${data.serviceDetails.content.title} - Visaway Immigration Services`,
description: data.serviceDetails.content.description,
};
} catch (error) {
return {
title: "Service Not Found",
description: "The requested service could not be found.",
};
}
}
export default async function ServiceDetailsPage({
params,
}: ServiceDetailsPageProps) {
try {
const { slug } = await params;
console.log("Service details page - slug:", slug);
const data = await fetchServiceBySlug(slug);
if (!data || !data.serviceDetails) {
notFound();
}
const { serviceDetails } = data;
const { content, keyFeatures, faq } = serviceDetails;
return (
<>
{/* Breadcrumb Section */}
<Breadcrumb title="Services Details" current="Service Details" />
{/* Service-details Section Start */}
<section className="service-details-section section-padding fix">
<div className="container">
<div className="service-details-wrapper">
<div className="row">
<div className="col-xl-12">
<div className="service-details-post">
<h2>{content.title}</h2>
<p className="mt-2">{content.description}</p>
<div className="details-image">
<img src={imageUrl(content.mainImage)} alt="img" />
</div>
<h3 className="text">{content.overviewTitle}</h3>
<p className="mt-3 mb-3">{content.overviewDescription}</p>
<p className="mb-4">{content.additionalDescription}</p>
<div className="row g-4">
<div className="col-lg-6">
<div className="service-left-content">
<h3>{keyFeatures.title}</h3>
<ul className="list-item">
{keyFeatures.items.map(
(feature: any, index: number) => (
<li key={index}>
<i className="fa-solid fa-chevrons-right"></i>
<span>{feature.title} -</span>
{feature.description}
</li>
),
)}
</ul>
</div>
</div>
<div className="col-lg-6">
<div className="thumb">
<img
src={imageUrl(keyFeatures.sideImage)}
alt="img"
/>
</div>
</div>
</div>
<div className="row mt-4 mt-xl-0 g-4">
<div className="col-lg-6">
<div className="thumb">
<img src={imageUrl(faq.sideImage)} alt="img" />
</div>
</div>
<div className="col-lg-6">
<div className="faq-items">
<h3 className="mb-3">{faq.title}</h3>
<div className="accordion" id="accordionExample">
{faq.items.map((faqItem: any, index: number) => {
const isExpanded = faqItem.isExpanded;
return (
<div
key={`faq-${index}`}
className="accordion-item wow fadeInUp"
data-wow-delay={`.${(index + 1) * 2}s`}
>
<h5
className="accordion-header"
id={`heading${index}`}
>
<button
className={`accordion-button ${!isExpanded ? "collapsed" : ""}`}
type="button"
data-bs-toggle="collapse"
data-bs-target={`#collapse${index}`}
aria-expanded={
isExpanded ? "true" : "false"
}
aria-controls={`collapse${index}`}
>
{faqItem.question}
</button>
</h5>
<div
id={`collapse${index}`}
className={`accordion-collapse collapse ${isExpanded ? "show" : ""}`}
aria-labelledby={`heading${index}`}
data-bs-parent="#accordionExample"
>
<div className="accordion-body">
<p>{faqItem.answer}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</>
);
} catch (error) {
console.error("Error loading service details:", error);
const { slug } = await params;
console.log("Slug that failed:", slug);
console.log("Falling back to not found page");
notFound();
}
}

View File

@@ -1,73 +1,218 @@
import servicesData from "./services.json";
import { Metadata } from "next";
import { fetchServicePageData } from "../../api/servicesApi";
import { imageUrl } from "../utils/image";
import Breadcrumb from "../components/Breadcrumb";
export const metadata: Metadata = {
title: "Services - Visaway Immigration & Visa Consulting",
description: "Immigration & Visa Consulting Services",
};
export default async function ServicesPage() {
const data = await fetchServicePageData();
const { services, destinations, visas, reviews } = data;
export default function ServicesPage() {
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">
{servicesData.title}
</h1>
<p className="text-xl text-gray-600">{servicesData.subtitle}</p>
</div>
<>
{/* Breadcrumb Section */}
<Breadcrumb title="Services" current="Services" />
{/* Services Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-2 gap-8">
{servicesData.services.map((service) => (
<div
key={service.id}
className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition-shadow"
>
{/* Service Header */}
<div className="flex items-center mb-4">
<span className="text-4xl mr-4">{service.icon}</span>
<div>
<h3 className="text-xl font-semibold text-gray-900">
{service.name}
</h3>
<p className="text-blue-600 font-medium">{service.price}</p>
{/* Service Section Start */}
<section className="service-section section-padding fix section-bg-1">
<div className="container">
<div className="section-title text-center">
<span className="sub-title-2 wow fadeInUp">
{services.title.subTitle}
</span>
<h2 className="split-text-right split-text-in-right">
{services.title.mainTitle}
</h2>
</div>
<div className="row">
<div className="col-xl-12">
{services.items.map((service: any) => (
<div
key={service.id}
className={`service-main-item-3 ${service.layout === "right" ? "style-2" : ""} fade-up-anim`}
>
{service.layout === "right" && (
<div className="service-button">
<a
href={`/services/details/${service.slug}`}
className="theme-btn"
>
read more
<i className="fa-solid fa-arrow-right"></i>
</a>
</div>
)}
<div className="service-left">
{service.layout === "left" && (
<div className="service-image">
<img src={imageUrl(service.image)} alt="img" />
</div>
)}
<div className="content">
<h3>
<a href={`/services/details/${service.slug}`}>
{service.name}
</a>
</h3>
<p>{service.description}</p>
</div>
{service.layout === "right" && (
<div className="service-image">
<img src={imageUrl(service.image)} alt="img" />
</div>
)}
</div>
{service.layout === "left" && (
<div className="service-button">
<a
href={`/services/details/${service.slug}`}
className="theme-btn"
>
read more
<i className="fa-solid fa-arrow-right"></i>
</a>
</div>
)}
</div>
))}
</div>
</div>
</div>
</section>
{/* Destination-Offer Section Start */}
<section className="destination-offer-section section-padding fix">
<div className="bg-image">
<img src={imageUrl(destinations.backgroundImage)} alt="img" />
</div>
<div className="container">
<div className="section-title">
<span className="sub-title-2 theme wow fadeInUp">
{destinations.title.subTitle}
</span>
<h2 className="split-text-right split-text-in-right text-white">
{destinations.title.mainTitle}
</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) => (
<div key={country.id} className="col destination-offer-item">
<div className="choose-us-image">
<img src={imageUrl(country.image)} alt="img" />
</div>
<div className="choose-us-content">
<div className="icon-item">
<div className="icon">
<img src={imageUrl(country.icon)} alt="img" />
</div>
<h5>
<a href={country.link}>{country.name}</a>
</h5>
</div>
<p>{country.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* Description */}
<p className="text-gray-700 mb-6">{service.description}</p>
{/* Features */}
<div className="mb-6">
<h4 className="font-semibold text-gray-900 mb-3">Tính năng:</h4>
<ul className="space-y-2">
{service.features.map((feature, index) => (
<li key={index} className="flex items-center text-gray-700">
<span className="text-green-500 mr-2"></span>
{feature}
</li>
))}
</ul>
{/* Service-Visa Section Start */}
<section className="service-visa-section fix">
<div className="container">
<div className="service-visa-wrapper">
{visas.items.map((visa: any, index: number) => (
<div
key={visa.id}
className={`service-visa-items ${index > 0 ? "style-2" : ""}`}
>
<div className="top-item">
<h4 className="number">{visa.number}</h4>
<h3>
<a href={visa.buttonLink}>{visa.name}</a>
</h3>
</div>
<p>{visa.description}</p>
<a href={visa.buttonLink} className="service-button">
{visa.buttonText}
</a>
</div>
))}
</div>
</div>
</section>
{/* CTA Button */}
<button className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors">
Tìm Hiểu Thêm
</button>
{/* Testimonial Section3 Start */}
<section className="testimonial-section section-padding fix">
<div className="container">
<div className="section-title-area">
<div className="section-title mb-0">
<span className="sub-title-2 wow fadeInUp">
{reviews.title.subTitle}
</span>
<h2 className="split-text-right split-text-in-right">
{reviews.title.mainTitle}
</h2>
</div>
))}
<a href="#" className="theme-btn">
View All Review
<i className="fa-solid fa-arrow-right"></i>
</a>
</div>
<div className="testimonial-wrapper-3">
<div className="row g-4 align-items-center">
<div className="col-lg-4">
<div className="testimonial-thumb">
<img src={imageUrl(reviews.thumb)} alt="img" />
</div>
</div>
<div className="col-lg-8">
<div className="testimonial-content">
<div className="swiper testimonial-slider-3">
<div className="swiper-wrapper">
{reviews.items.map((testimonial: any) => (
<div key={testimonial.id} className="swiper-slide">
<div className="content">
<div className="star">
{[...Array(testimonial.rating)].map(
(_: any, i: number) => (
<i key={i} className="fa-solid fa-star"></i>
),
)}
</div>
<h3>"{testimonial.content}"</h3>
<div className="info-item">
<div className="icon">
<i className={testimonial.icon}></i>
</div>
<div className="content">
<h5>{testimonial.author.name}</h5>
<span>{testimonial.author.type}</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
<div className="array-buttons-3">
<button className="array-prev">
<i className="fa-solid fa-arrow-left"></i>
</button>
<button className="array-next">
<i className="fa-solid fa-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Contact CTA */}
<div className="text-center mt-12 bg-gray-50 rounded-lg p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Cần Vấn Thêm?
</h2>
<p className="text-gray-700 mb-6">
Liên hệ với chúng tôi đ đưc vấn miễn phí nhận báo giá chi
tiết
</p>
<button className="bg-green-600 text-white px-8 py-3 rounded-lg hover:bg-green-700 transition-colors">
Liên Hệ Ngay
</button>
</div>
</div>
</div>
</section>
</>
);
}

5
app/utils/image.ts Normal file
View File

@@ -0,0 +1,5 @@
export const imageUrl = (path?: string) => {
if (!path) return "";
if (path.startsWith("http")) return path;
return `${process.env.NEXT_PUBLIC_API_URL}/${path}`;
};