feat: add country API integration and image fallback component

This commit is contained in:
nguyenvanbao
2026-02-04 14:42:54 +07:00
parent ec9883c65a
commit f4529d1e90
3 changed files with 188 additions and 51 deletions

View File

@@ -100,6 +100,20 @@ export interface ServicePageData {
reviews: ReviewSection; reviews: ReviewSection;
} }
export interface Country {
id: number;
name: string;
slug: string;
mainImage: string;
icon: string;
services: string[];
}
export interface CountryApiResponse {
success: boolean;
data: Country[];
}
/* ======================= /* =======================
Utils Utils
======================= */ ======================= */
@@ -230,6 +244,43 @@ export const fetchServiceBySlug = async (slug: string): Promise<any> => {
} }
}; };
export const fetchCountries = async (): Promise<Country[]> => {
try {
const apiUrl = getApiUrl();
const endpoint = `${apiUrl}/api/visa/country`;
console.log("Fetching countries from endpoint:", endpoint);
const response = await fetch(endpoint, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
});
console.log("Countries API response status:", response.status);
if (!response.ok) {
console.error("Countries API failed, using fallback data");
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = (await response.json()) as CountryApiResponse;
if (!result.success) {
throw new Error("Countries API returned success=false");
}
console.log("Countries data received successfully");
return result.data;
} catch (error) {
console.error("Error fetching countries:", error);
console.log("Using fallback countries data");
return getFallbackCountries();
}
};
/* ======================= /* =======================
Fallback Data Fallback Data
======================= */ ======================= */
@@ -560,3 +611,70 @@ export const getFallbackServicePageData = (): ServicePageData => ({
], ],
}, },
}); });
export const getFallbackCountries = (): Country[] => [
{
id: 1,
name: "Canada",
slug: "canada",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-1.png",
services: ["Immigration Appeal", "Permanent Residency", "Study Visa"],
},
{
id: 2,
name: "Australia",
slug: "australia",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-2.png",
services: ["Work Visa", "Permanent Residency", "Student Visa"],
},
{
id: 3,
name: "United Kingdom",
slug: "united-kingdom",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-3.png",
services: ["Study Visa", "Work Visa", "Family Visa"],
},
{
id: 4,
name: "United States",
slug: "united-states",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-1.png",
services: ["Immigration Appeal", "Work Visa", "Student Visa"],
},
{
id: 5,
name: "Germany",
slug: "germany",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-2.png",
services: ["Study Visa", "Work Visa", "Permanent Residency"],
},
{
id: 6,
name: "France",
slug: "france",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-3.png",
services: ["Student Visa", "Work Visa", "Family Visa"],
},
{
id: 7,
name: "New Zealand",
slug: "new-zealand",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-1.png",
services: ["Work Visa", "Permanent Residency", "Study Visa"],
},
{
id: 8,
name: "Japan",
slug: "japan",
mainImage: "_images/default.jpg",
icon: "img/home-3/choose-us/icon-2.png",
services: ["Work Visa", "Student Visa", "Business Visa"],
},
];

View File

@@ -0,0 +1,40 @@
"use client";
import { useState } from "react";
import { imageUrl } from "../utils/image";
interface ImageWithFallbackProps {
src: string;
alt: string;
fallbackSrc?: string;
className?: string;
style?: React.CSSProperties;
}
export default function ImageWithFallback({
src,
alt,
fallbackSrc = "_images/default.jpg",
className,
style,
}: ImageWithFallbackProps) {
const [imgSrc, setImgSrc] = useState(imageUrl(src));
const [hasError, setHasError] = useState(false);
const handleError = () => {
if (!hasError) {
setHasError(true);
setImgSrc(imageUrl(fallbackSrc));
}
};
return (
<img
src={imgSrc}
alt={alt}
className={className}
style={style}
onError={handleError}
/>
);
}

View File

@@ -1,57 +1,18 @@
import { Metadata } from "next"; import { fetchServicePageData, fetchCountries } from "../../api/servicesApi";
import { fetchServicePageData } from "../../api/servicesApi";
import { imageUrl } from "../utils/image"; import { imageUrl } from "../utils/image";
import Breadcrumb from "../components/Breadcrumb"; import Breadcrumb from "../components/Breadcrumb";
import ImageWithFallback from "../components/ImageWithFallback";
import "./services.css"; import "./services.css";
export default async function ServicesPage() { export default async function ServicesPage() {
const data = await fetchServicePageData(); const data = await fetchServicePageData();
const allCountries = await fetchCountries();
const { services, destinations, visas, reviews } = data; const { services, destinations, visas, reviews } = data;
const country = [
{ // Pagination logic - show only first 5 countries
id: "canada", const COUNTRIES_PER_PAGE = 5;
name: "Canada", const displayedCountries = allCountries.slice(0, COUNTRIES_PER_PAGE);
description: const hasMoreCountries = allCountries.length > COUNTRIES_PER_PAGE;
"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 (
<> <>
@@ -140,10 +101,14 @@ 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">
{country.map((country: any) => ( {displayedCountries.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" /> <ImageWithFallback
src={country.mainImage}
alt="img"
fallbackSrc="_images/default.jpg"
/>
</div> </div>
<div className="choose-us-content"> <div className="choose-us-content">
<div className="icon-item"> <div className="icon-item">
@@ -151,14 +116,28 @@ export default async function ServicesPage() {
<img src={imageUrl(country.icon)} alt="img" /> <img src={imageUrl(country.icon)} alt="img" />
</div> </div>
<h5> <h5>
<a href={country.link}>{country.name}</a> <a href={`/visa/${country.slug}`}>{country.name}</a>
</h5> </h5>
</div> </div>
<p>{country.description}</p> <p>
{country.services && country.services.length > 0
? `Services: ${country.services.slice(0, 2).join(", ")}${country.services.length > 2 ? "..." : ""}`
: "Immigration services available"}
</p>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{/* Show "View More" button if there are more countries */}
{hasMoreCountries && (
<div className="text-center mt-4">
<a href="/visa" className="theme-btn">
View All Countries ({allCountries.length})
<i className="fa-solid fa-arrow-right"></i>
</a>
</div>
)}
</div> </div>
</section> </section>