forked from UKSOURCE/hailearning.edu.vn
feat: add country API integration and image fallback component
This commit is contained in:
@@ -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"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
40
app/components/ImageWithFallback.tsx
Normal file
40
app/components/ImageWithFallback.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user