Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/hailearning.edu.vn into fea/thanh-02022026-news

This commit is contained in:
Wini_Fy
2026-02-05 11:00:10 +07:00
17 changed files with 1587 additions and 1430 deletions

831
api/visa.ts Normal file
View File

@@ -0,0 +1,831 @@
// app/api/visa/route.ts
import { NextRequest, NextResponse } from "next/server";
// ==================== INTERFACES ====================
interface VisaItem {
title: string;
description: string;
}
interface VisaTypeCategory {
items: VisaItem[];
}
interface VisaProcessStep {
number: string;
title: string;
description: string;
}
interface VisaProcess {
title: string;
steps: VisaProcessStep[];
}
interface VisaCategory {
title: string;
steps: string[][];
}
interface VisaService {
title: string;
steps: VisaProcessStep[];
}
interface RelatedCountry {
id: number;
name: string;
icon: string;
}
interface ContactInfo {
sectionTitle: string;
helpText: string;
img: string;
phone: {
label: string;
value: string;
link: string;
};
email: {
label: string;
value: string;
link: string;
};
location: {
label: string;
address: string;
};
}
interface ActiveCountry {
id: number;
name: string;
title: string;
mainImage: string;
description: string;
additionalInfo: string;
tagline: string;
visaTypes: VisaTypeCategory[];
visaProcess: VisaProcess;
gallery: string[];
visaCategories: VisaCategory;
visaService: VisaService;
}
export interface DetailedView {
activeCountry: ActiveCountry;
relatedCountries: RelatedCountry[];
contactInfo: ContactInfo;
}
export interface VisaCountry {
id: number;
name: string;
slug: string;
icon: string;
services: string[];
detailedView?: DetailedView;
}
export interface VisaHero {
title: string;
summaryList: VisaCountry[];
}
export interface VisaData {
hero: VisaHero;
}
export interface VisaApiResponse {
success: boolean;
message?: string;
data?: VisaCountry | VisaCountry[] | VisaHero | null;
error?: string;
}
export interface VisaInquiry {
name: string;
email: string;
phone: string;
country: string;
visaType: string;
}
export interface VisaInquiryResponse {
success: boolean;
message: string;
inquiryId?: string;
data?: VisaInquiry;
}
// ==================== HELPER FUNCTIONS ====================
const getApiUrl = (): string => {
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
};
/**
* Fetch visa data từ remote API hoặc fallback
* @param slug - Optional country slug
* @returns Promise<VisaCountry | VisaCountry[]>
*/
export async function fetchVisaData(): Promise<any> {
const apiUrl = getApiUrl();
const url = `${apiUrl}`; // Đảm bảo apiUrl không thừa dấu / ở cuối
try {
const response = await fetch(`${url}/api/visa`, {
method: "GET",
headers: { "Content-Type": "application/json" },
cache: "no-store",
});
if (!response.ok) throw new Error(`API returned ${response.status}`);
const apiResponse = await response.json();
if (!apiResponse.success || !apiResponse.hero) {
throw new Error("Dữ liệu API không đúng cấu trúc");
}
// Hàm bổ trợ để nối URL chuẩn
const fixUrl = (path: string) => {
if (!path || path.startsWith("http")) return path;
return `${url}${path.startsWith("/") ? "" : "/"}${path}`;
};
// Duyệt qua danh sách summaryList để ghi đè toàn bộ ảnh
if (apiResponse.hero.summaryList) {
apiResponse.hero.summaryList = apiResponse.hero.summaryList.map(
(country: any) => {
// 1. Fix icon cấp gốc
const updatedCountry = {
...country,
icon: fixUrl(country.icon),
};
// 2. Fix sâu trong detailedView (nếu có)
if (updatedCountry.detailedView) {
const dv = updatedCountry.detailedView;
updatedCountry.detailedView = {
...dv,
// Fix ảnh trong activeCountry
activeCountry: dv.activeCountry
? {
...dv.activeCountry,
mainImage: fixUrl(dv.activeCountry.mainImage),
gallery: dv.activeCountry.gallery?.map(fixUrl),
}
: dv.activeCountry,
// Fix icon trong danh sách nước liên quan
relatedCountries: dv.relatedCountries?.map((rc: any) => ({
...rc,
icon: fixUrl(rc.icon),
})),
// Fix ảnh nền trong contactInfo
contactInfo: dv.contactInfo
? {
...dv.contactInfo,
img: fixUrl(dv.contactInfo.img),
}
: dv.contactInfo,
};
}
return updatedCountry;
},
);
}
return apiResponse;
} catch (error) {
console.error("❌ Error fetching:", error);
return getFallbackVisaData();
}
}
// ==================== FALLBACK DATA ====================
/**
* Fallback visa data - được dùng khi API không hoạt động
*/
function getFallbackVisaData(): VisaData {
return {
hero: {
title: "Visa Services ",
summaryList: [
{
id: 1,
name: "France",
slug: "france",
icon: "/assets/img/home-2/visa/03.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
detailedView: {
activeCountry: {
id: 1,
name: "France",
title: "COUNTRY FRANCE",
mainImage: "/assets/img/inner-page/country-details/details-1.jpg",
description:
"France is one of the most popular destinations for international students, offering world-class universities, diverse cultural experiences, and countless career opportunities...",
additionalInfo:
"Our consultancy provides complete guidance for study visas, work permits, and permanent residency pathways tailored to your goals.",
tagline:
"Over the last 35 Years we made an impact that is strong & we have long way to go.",
visaTypes: [
{
items: [
{
title: "Tourist Visa",
description:
"Broad term that can refer to various aspects of interconnectedness",
},
{
title: "Work Permit",
description:
"Broad term that can refer to various aspects of interconnectedness",
},
],
},
{
items: [
{
title: "Student",
description:
"Broad term that can refer to various aspects of interconnectedness",
},
{
title: "Family Visa",
description:
"Broad term that can refer to various aspects of interconnectedness",
},
],
},
],
visaProcess: {
title: "France Visa Process",
steps: [
{
number: "01",
title: "Consultation & Eligibility Check",
description:
"Our experts review your profile and visa requirements.",
},
{
number: "02",
title: "Application Preparation",
description:
"We help with document collection, form filling, and statement drafting.",
},
{
number: "03",
title: "Submission",
description:
"Visa application is submitted online with required fees.",
},
{
number: "04",
title: "Interview Guidance",
description:
"Get training and mock sessions for embassy interview.",
},
{
number: "05",
title: "Approval & Travel",
description:
"Once approved, we provide travel and pre-departure guidance.",
},
],
},
gallery: [
"/assets/img/inner-page/country-details/details-2.jpg",
"/assets/img/inner-page/country-details/details-3.png",
],
visaCategories: {
title: "Types of France Visas",
steps: [
[
"Student Visa (F1, M1, J1)",
"Work Visa (H1B, L1)",
"Tourist Visa (B1/B2)",
],
[
"Family/Spouse Visa (K1, IR1, F2A)",
"Green Card / Immigrant Visa",
],
],
},
visaService: {
title: "Our France Visa Service Options",
steps: [
{
number: "01",
title: "Consultation & Eligibility Check",
description:
"Our experts review your profile and visa requirements.",
},
{
number: "02",
title: "Application Preparation",
description:
"We help with document collection, form filling, and statement drafting.",
},
{
number: "03",
title: "Submission",
description:
"Visa application is submitted online with required fees.",
},
{
number: "04",
title: "Interview Guidance",
description:
"Get training and mock sessions for embassy interview.",
},
{
number: "05",
title: "Approval & Travel",
description:
"Once approved, we provide travel and pre-departure guidance.",
},
],
},
},
relatedCountries: [
{
id: 1,
name: "Canada",
icon: "/assets/img/inner-page/country-details/01.png",
},
{
id: 2,
name: "USA",
icon: "/assets/img/inner-page/country-details/02.png",
},
{
id: 3,
name: "UK",
icon: "/assets/img/inner-page/country-details/03.png",
},
{
id: 4,
name: "Germany",
icon: "/assets/img/inner-page/country-details/05.png",
},
{
id: 5,
name: "Spain",
icon: "/assets/img/inner-page/country-details/06.png",
},
],
contactInfo: {
sectionTitle: "Visa & Immigration",
helpText: "Need Help? Book Lab Visit",
img: "/assets/img/inner-page/country-details/bg.jpg",
phone: {
label: "Call Us",
value: "+33 1 23 45 67 89",
link: "tel:+33123456789",
},
email: {
label: "Mail Us",
value: "visa@france-consultant.com",
link: "mailto:visa@france-consultant.com",
},
location: {
label: "Location",
address: "Paris, Lyon, Marseille",
},
},
},
},
{
id: 2,
name: "UK",
slug: "uk",
icon: "/assets/img/home-2/visa/11.png",
services: [
"Student Visa & Admission",
"Work Visa Skilled Worker",
"Family Visa",
"Entrepreneur Visa",
],
detailedView: {
activeCountry: {
id: 2,
name: "United Kingdom",
title: "COUNTRY UK",
mainImage: "/assets/img/inner-page/country-details/details-1.jpg",
description:
"The UK is a world leader in education and innovation, offering exceptional universities and career opportunities for international professionals...",
additionalInfo:
"We provide comprehensive support for UK student visas, work visas, and family sponsorship applications.",
tagline: "35+ years of excellence in UK visa consulting.",
visaTypes: [
{
category: "Student & Work",
items: [
{
title: "Student Visa",
description:
"Study at UK universities with part-time work opportunities",
},
{
title: "Graduate Visa",
description:
"Work in the UK after completing your studies",
},
],
},
{
category: "Family & Other",
items: [
{
title: "Skilled Worker Visa",
description: "Work in a skilled occupation in the UK",
},
{
title: "Family Visa",
description: "Join your family in the UK",
},
],
},
],
visaProcess: {
title: "UK Visa Process",
steps: [
{
number: "01",
title: "Eligibility Assessment",
description:
"We evaluate your qualifications and circumstances.",
},
{
number: "02",
title: "Documentation",
description:
"Comprehensive document preparation and review.",
},
{
number: "03",
title: "Application Submission",
description: "Submit your visa application online.",
},
{
number: "04",
title: "Biometrics & Interview",
description: "Complete biometric appointment if required.",
},
{
number: "05",
title: "Visa Decision",
description: "Receive your visa and start your UK journey.",
},
],
},
gallery: [
"/assets/img/inner-page/country-details/details-2.jpg",
"/assets/img/inner-page/country-details/details-3.png",
],
visaCategories: {
title: "Types of UK Visas",
steps: [
[
"Student Visa (Tier 4)",
"Graduate Visa",
"Skilled Worker Visa",
],
["Family Visa", "Visitor Visa", "Entrepreneur Visa"],
],
},
visaService: {
title: "Our UK Visa Services",
steps: [
{
number: "01",
title: "Initial Consultation",
description: "Free assessment of your visa eligibility.",
},
{
number: "02",
title: "Document Preparation",
description: "Expert guidance on all required documents.",
},
{
number: "03",
title: "Application Submission",
description: "Professional submission and tracking.",
},
{
number: "04",
title: "Interview Coaching",
description: "Prepare for your visa interview.",
},
{
number: "05",
title: "Post-Visa Support",
description: "Guidance for accommodation and settlement.",
},
],
},
},
relatedCountries: [
{
id: 1,
name: "Canada",
icon: "/assets/img/inner-page/country-details/01.png",
},
{
id: 2,
name: "USA",
icon: "/assets/img/inner-page/country-details/02.png",
},
{
id: 3,
name: "Australia",
icon: "/assets/img/inner-page/country-details/03.png",
},
],
contactInfo: {
sectionTitle: "UK Visa & Immigration",
helpText: "Need Help? Book Consultation",
img: "/assets/img/inner-page/country-details/bg.jpg",
phone: {
label: "Call Us",
value: "+44 20 7946 0958",
link: "tel:+442079460958",
},
email: {
label: "Mail Us",
value: "visa@uk-consultant.com",
link: "mailto:visa@uk-consultant.com",
},
location: {
label: "Location",
address: "London, Manchester, Birmingham",
},
},
},
},
{
id: 3,
name: "Canada",
slug: "canada",
icon: "/assets/img/home-2/visa/02.png",
services: [
"Student Visa & Admission",
"Work Permit",
"Express Entry",
"Family Sponsorship",
],
detailedView: {
activeCountry: {
id: 3,
name: "Canada",
title: "COUNTRY CANADA",
mainImage: "/assets/img/inner-page/country-details/details-1.jpg",
description:
"Canada is known for its high quality of life, excellent education system, and welcoming immigration policies...",
additionalInfo:
"We specialize in Express Entry, study permits, and work permits for Canada.",
tagline: "Your pathway to a better life in Canada starts here.",
visaTypes: [
{
category: "Study & Work",
items: [
{
title: "Study Permit",
description: "Study at Canadian institutions",
},
{
title: "Work Permit",
description: "Temporary work in Canada",
},
],
},
{
category: "Immigration & Family",
items: [
{
title: "Express Entry",
description: "Fast-track permanent residency",
},
{
title: "Family Sponsorship",
description: "Bring your family to Canada",
},
],
},
],
visaProcess: {
title: "Canada Visa Process",
steps: [
{
number: "01",
title: "Initial Assessment",
description:
"We assess your eligibility for Canadian programs.",
},
{
number: "02",
title: "Documentation",
description: "Prepare comprehensive application packages.",
},
{
number: "03",
title: "Application",
description: "Submit to IRCC (Immigration Canada).",
},
{
number: "04",
title: "Medical & Security",
description: "Complete medical exams and security checks.",
},
{
number: "05",
title: "Approval",
description: "Receive your visa and prepare for arrival.",
},
],
},
gallery: [
"/assets/img/inner-page/country-details/details-2.jpg",
"/assets/img/inner-page/country-details/details-3.png",
],
visaCategories: {
title: "Types of Canadian Visas",
steps: [
[
"Study Permit",
"Work Permit (Open/Closed)",
"International Mobility Program",
],
[
"Express Entry (FSW, CEC, FST)",
"Provincial Nominee Program",
"Family Sponsorship",
],
],
},
visaService: {
title: "Our Canada Visa Services",
steps: [
{
number: "01",
title: "Free Consultation",
description: "Discuss your Canadian immigration goals.",
},
{
number: "02",
title: "Eligibility Check",
description: "Determine best pathway for you.",
},
{
number: "03",
title: "Application Support",
description: "Complete assistance with applications.",
},
{
number: "04",
title: "Preparation",
description: "Prepare for medical exams and interviews.",
},
{
number: "05",
title: "Settlement Support",
description: "Help with housing and job search.",
},
],
},
},
relatedCountries: [
{
id: 1,
name: "USA",
icon: "/assets/img/inner-page/country-details/01.png",
},
{
id: 2,
name: "Australia",
icon: "/assets/img/inner-page/country-details/02.png",
},
],
contactInfo: {
sectionTitle: "Canada Visa & Immigration",
helpText: "Ready to Immigrate to Canada?",
img: "/assets/img/inner-page/country-details/bg.jpg",
phone: {
label: "Call Us",
value: "+1 416 815 7755",
link: "tel:+14168157755",
},
email: {
label: "Mail Us",
value: "visa@canada-consultant.com",
link: "mailto:visa@canada-consultant.com",
},
location: {
label: "Location",
address: "Toronto, Vancouver, Montreal",
},
},
},
},
{
id: 4,
name: "Germany",
slug: "germany",
icon: "/assets/img/home-2/visa/12.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
{
id: 5,
name: "Spain",
slug: "spain",
icon: "/assets/img/home-2/visa/13.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
{
id: 6,
name: "South Korea",
slug: "south-korea",
icon: "/assets/img/home-2/visa/14.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
{
id: 7,
name: "Japan",
slug: "japan",
icon: "/assets/img/home-2/visa/15.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
{
id: 8,
name: "Croatia",
slug: "croatia",
icon: "/assets/img/home-2/visa/16.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
{
id: 9,
name: "England",
slug: "england",
icon: "/assets/img/home-2/visa/17.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
{
id: 10,
name: "Indonesia",
slug: "indonesia",
icon: "/assets/img/home-2/visa/18.png",
services: [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada",
],
},
],
},
};
}

View File

@@ -1,28 +1,61 @@
'use client'; 'use client';
import { useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import HeaderTop from './HeaderTop'; import HeaderTop from './HeaderTop';
import HeaderBottom from './HeaderBottom'; import HeaderBottom from './HeaderBottom';
import Offcanvas from './Offcanvas'; import Offcanvas from './Offcanvas';
import { headerMenuService } from '@/services/header-menu.service';
import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
const Header = () => { const Header = () => {
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false); const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const [menuItems, setMenuItems] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen); const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen);
const toggleSearch = () => setIsSearchOpen(!isSearchOpen); const toggleSearch = () => setIsSearchOpen(!isSearchOpen);
// Helper to adapt 'children' from API to 'submenu' for the existing components
const adaptMenu = useCallback((item: HeaderMenuType): any => ({
label: item.title,
href: item.url,
submenu: item.children && item.children.length > 0
? item.children.map((child: HeaderMenuType) => adaptMenu(child))
: undefined
}), []);
useEffect(() => {
const fetchMenu = async () => {
try {
setIsLoading(true);
const data = await headerMenuService.getHeaderMenu();
const mappedData = data.map(item => adaptMenu(item));
setMenuItems(mappedData);
} catch (error) {
console.error('Error fetching menu in Header:', error);
} finally {
setIsLoading(false);
}
};
fetchMenu();
}, [adaptMenu]);
return ( return (
<> <>
<HeaderTop /> <HeaderTop />
<HeaderBottom <HeaderBottom
onToggleOffcanvas={toggleOffcanvas} onToggleOffcanvas={toggleOffcanvas}
onToggleSearch={toggleSearch} onToggleSearch={toggleSearch}
menuItems={menuItems}
isLoading={isLoading}
/> />
<Offcanvas <Offcanvas
isOpen={isOffcanvasOpen} isOpen={isOffcanvasOpen}
onClose={() => setIsOffcanvasOpen(false)} onClose={() => setIsOffcanvasOpen(false)}
menuItems={menuItems}
/> />
{/* Search Popup */} {/* Search Popup */}

View File

@@ -1,52 +1,24 @@
'use client'; 'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import HeaderMenu from './HeaderMenu'; import HeaderMenu from './HeaderMenu';
import { headerMenuService } from '@/services/header-menu.service';
import headerData from './header.json'; import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
// Map the JSON data to satisfy the HeaderMenu props interface
interface JsonMenuItem {
label: string;
href: string;
children?: JsonMenuItem[];
}
interface MenuItem {
label: string;
href: string;
submenu?: MenuItem[];
megaMenuContent?: React.ReactNode;
}
// We need to recursively map 'children' to 'submenu'
const mapMenuItems = (items: JsonMenuItem[]): MenuItem[] => {
return items.map(item => {
const newItem: MenuItem = {
label: item.label,
href: item.href,
};
if (item.children && item.children.length > 0) {
newItem.submenu = mapMenuItems(item.children);
}
return newItem;
});
};
const menuItems: MenuItem[] = mapMenuItems(headerData.menu as JsonMenuItem[]);
interface HeaderBottomProps { interface HeaderBottomProps {
onToggleOffcanvas: () => void; onToggleOffcanvas: () => void;
onToggleSearch: () => void; onToggleSearch: () => void;
menuItems: any[];
isLoading: boolean;
} }
const HeaderBottom: React.FC<HeaderBottomProps> = ({ onToggleOffcanvas, onToggleSearch }) => { const HeaderBottom: React.FC<HeaderBottomProps> = ({
onToggleOffcanvas,
onToggleSearch,
menuItems,
isLoading
}) => {
return ( return (
<header id="header-sticky" className="header-1"> <header id="header-sticky" className="header-1">
<div className="container-fluid"> <div className="container-fluid">
@@ -59,7 +31,7 @@ const HeaderBottom: React.FC<HeaderBottomProps> = ({ onToggleOffcanvas, onToggle
</Link> </Link>
</div> </div>
<div className="mean__menu-wrapper"> <div className="mean__menu-wrapper">
<HeaderMenu menuItems={menuItems} /> {!isLoading && <HeaderMenu menuItems={menuItems as any} />}
</div> </div>
</div> </div>
<div className="header-right d-flex align-items-center mt-0"> <div className="header-right d-flex align-items-center mt-0">

View File

@@ -20,9 +20,10 @@ interface MenuItem {
interface OffcanvasProps { interface OffcanvasProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
menuItems: any[];
} }
const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose }) => { const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose, menuItems }) => {
// Explicitly casting headerData to the expected structure // Explicitly casting headerData to the expected structure
const data = headerData as { const data = headerData as {
top: { top: {
@@ -37,10 +38,10 @@ const Offcanvas: React.FC<OffcanvasProps> = ({ isOpen, onClose }) => {
phone: string; phone: string;
}; };
}; };
menu: MenuItem[];
}; };
const { offcanvas, top, menu } = data; const { offcanvas, top } = data;
const menu = menuItems;
return ( return (
<> <>

View File

@@ -108,7 +108,7 @@
"children": [ "children": [
{ {
"label": "Visa List", "label": "Visa List",
"href": "/visa-list" "href": "/visa"
}, },
{ {
"label": "Visa Details", "label": "Visa Details",

View File

@@ -42,19 +42,46 @@ export default function RootLayout({
{children} {children}
<Footer /> <Footer />
<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 src="/assets/js/swiper-bundle.min.js" strategy="afterInteractive" /> <Script
<Script src="/assets/js/jquery.meanmenu.min.js" strategy="afterInteractive" /> src="/assets/js/swiper-bundle.min.js"
<Script src="/assets/js/jquery.magnific-popup.min.js" strategy="afterInteractive" /> 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/wow.min.js" strategy="afterInteractive" />
<Script src="/assets/js/gsap.js" strategy="afterInteractive" /> <Script src="/assets/js/gsap.js" strategy="afterInteractive" />
<Script src="/assets/js/lenis.min.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/SplitText.min.js" strategy="afterInteractive" />
<Script src="/assets/js/main.js" strategy="afterInteractive" /> <Script src="/assets/js/main.js" strategy="afterInteractive" />
</body> </body>

View File

@@ -1,416 +0,0 @@
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import visaData from "../visa.json";
const ASSET_URL = process.env.NEXT_PUBLIC_API_URL || "";
interface CountryDetailsClientProps {
country: {
id: number;
name: string;
icon: string;
services: string[];
};
}
export default function CountryDetailsClient({
country,
}: CountryDetailsClientProps) {
const [showBackToTop, setShowBackToTop] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// Get detailed data from visa.json
const countryData = visaData.visaSystem.detailedView.activeCountry;
const relatedCountries =
visaData.visaSystem.detailedView.relatedCountries.map((c: any) => ({
...c,
icon: `${ASSET_URL}/${c.icon}`,
}));
const contactInfo = visaData.visaSystem.contactInfo;
useEffect(() => {
const handleScroll = () => setShowBackToTop(window.scrollY > 100);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const scrollToTop = () => window.scrollTo({ top: 0, behavior: "smooth" });
return (
<>
{/* Back to Top */}
<button
onClick={scrollToTop}
className={`fixed bottom-8 right-8 w-12 h-12 rounded-full bg-white shadow-lg flex items-center justify-center cursor-pointer transition-all z-40 ${
showBackToTop ? "opacity-100" : "opacity-0 pointer-events-none"
}`}
>
<svg
className="w-6 h-6 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 11l5-5m0 0l5 5m-5-5v12"
/>
</svg>
</button>
{/* Header Top */}
<div className="bg-gray-900 text-white py-3 hidden lg:block">
<div className="max-w-7xl mx-auto px-8 flex justify-between items-center text-sm">
<ul className="flex gap-8">
<li>
<a href="tel:+093783575222">+09 378 357 5222</a>
</li>
<li>69 Street, 5th Avenue LA, United States</li>
<li>
<a href="mailto:info@example.com">info@example.com</a>
</li>
</ul>
<select className="bg-gray-900 text-white border-0 outline-none cursor-pointer">
<option>English</option>
<option>Bangla</option>
<option>Hindi</option>
</select>
</div>
</div>
{/* Header */}
<header className="sticky top-0 z-20 bg-white shadow-md">
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-4 flex justify-between items-center">
<Link href="/" className="flex-shrink-0">
<img
src={`${ASSET_URL}/assets/img/logo/black-logo.svg`}
alt="logo"
className="h-10"
/>
</Link>
<nav className="hidden lg:flex gap-8">
<Link href="/" className="text-gray-700 hover:text-blue-600">
Home
</Link>
<Link href="/about" className="text-gray-700 hover:text-blue-600">
About Us
</Link>
<Link
href="/country-list"
className="text-gray-700 hover:text-blue-600"
>
VISA
</Link>
<Link href="/contact" className="text-gray-700 hover:text-blue-600">
Contact Us
</Link>
</nav>
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden text-2xl"
>
</button>
</div>
</header>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<div className="fixed inset-0 z-30 lg:hidden">
<div
className="absolute inset-0 bg-black/50"
onClick={() => setIsMobileMenuOpen(false)}
></div>
<div className="absolute left-0 top-0 w-80 h-full bg-white p-6 overflow-y-auto">
<button
onClick={() => setIsMobileMenuOpen(false)}
className="float-right text-2xl"
>
</button>
<nav className="space-y-3 mt-8">
<Link href="/" className="block py-2">
Home
</Link>
<Link href="/about" className="block py-2">
About Us
</Link>
<Link href="/country-list" className="block py-2">
Visa
</Link>
<Link href="/contact" className="block py-2">
Contact Us
</Link>
</nav>
</div>
</div>
)}
{/* Breadcrumb */}
<section
className="py-20 px-4 lg:px-8 bg-cover relative"
style={{
backgroundImage: `url('${ASSET_URL}/${countryData.mainImage}')`,
}}
>
<div className="absolute inset-0 bg-black/30"></div>
<div className="max-w-7xl mx-auto relative z-10 text-center text-white">
<h1 className="text-5xl font-bold mb-6">{countryData.title}</h1>
<ul className="flex justify-center gap-4">
<li>
<Link href="/">Home</Link>
</li>
<li></li>
<li>{countryData.name}</li>
</ul>
</div>
</section>
{/* Country Details */}
<section className="py-20 px-4 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2">
<img
src={`${ASSET_URL}/${countryData.mainImage}`}
alt={countryData.name}
className="w-full rounded-lg mb-8"
/>
<h2 className="text-4xl font-bold mb-4">{countryData.name}</h2>
<p className="text-gray-700 mb-4">{countryData.description}</p>
<p className="text-gray-700 mb-4">{countryData.additionalInfo}</p>
<h5 className="text-xl font-semibold mb-6">
{countryData.tagline}
</h5>
{/* Visa Types */}
<div className="grid grid-cols-2 gap-6 mb-8">
{countryData.visaTypes.map((typeGroup: any, idx: number) => (
<React.Fragment key={idx}>
{typeGroup.items.map((item: any, itemIdx: number) => (
<div
key={itemIdx}
className="p-6 border-l-4 border-blue-600"
>
<h5 className="font-bold mb-2">{item.title}</h5>
<p className="text-sm text-gray-600">
{item.description}
</p>
</div>
))}
</React.Fragment>
))}
</div>
{/* Visa Process */}
<h3 className="text-2xl font-bold mb-6">USA Visa Process</h3>
<ul className="space-y-4 mb-8">
{countryData.visaProcess.steps.map((process: any) => (
<li key={process.number} className="flex gap-4">
<span className="font-bold text-blue-600 flex-shrink-0">
{process.number}.
</span>
<span>
<strong>{process.title}</strong> {process.description}
</span>
</li>
))}
</ul>
{/* Gallery */}
<div className="grid grid-cols-2 gap-6 mb-8">
{countryData.gallery.map((image: string, idx: number) => (
<img
key={idx}
src={`${ASSET_URL}/${image}`}
alt={`${countryData.name} gallery`}
className="rounded-lg w-full"
/>
))}
</div>
{/* Visa Types List */}
<h3 className="text-2xl font-bold mb-6">Types of Visas</h3>
{countryData.visaCategories.steps.map(
(subGroup: string[], groupIdx: number) => (
<ul className="visa-list-2" key={groupIdx}>
{/* Map lần 2 để render từng chuỗi trong mảng con */}
{subGroup.map((category: string, idx: number) => (
<li key={idx}>
<i className="fa-solid fa-chevrons-right"></i>
{category}
</li>
))}
</ul>
),
)}
{/* Service Options */}
<h3 className="text-2xl font-bold mb-6">
Our {countryData.name} Visa Service Options
</h3>
<ul className="space-y-4">
{countryData.visaProcess.steps.map((process: any) => (
<li key={process.number} className="flex gap-4">
<span className="font-bold text-blue-600 flex-shrink-0">
{process.number}.
</span>
<span>
<strong>{process.title}</strong> {process.description}
</span>
</li>
))}
</ul>
</div>
{/* Sidebar */}
<div className="lg:col-span-1">
{/* Related Countries */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
{relatedCountries.map((c: any) => (
<div
key={c.id}
className="flex items-center justify-between py-4 border-b last:border-0 cursor-pointer hover:text-blue-600"
>
<div className="flex items-center gap-3">
<img src={c.icon} alt={c.name} className="w-10 h-10" />
<h5 className="font-medium">{c.name}</h5>
</div>
<span className="text-blue-600"></span>
</div>
))}
</div>
{/* Contact Box */}
<div
className="rounded-lg p-6 text-white relative"
style={{
backgroundImage: `url('${ASSET_URL}/assets/img/inner-page/country-details/bg.jpg')`,
backgroundSize: "cover",
}}
>
<div className="absolute inset-0 bg-black/60 rounded-lg"></div>
<div className="relative z-10">
<h3 className="text-2xl font-bold mb-2">
Visa & Immigration
</h3>
<p className="mb-6">Need Help? Book Lab Visit</p>
<div className="space-y-4">
<div className="flex gap-3">
<div className="w-10 h-10 rounded-full bg-blue-600 flex items-center justify-center flex-shrink-0">
📞
</div>
<div>
<span className="text-sm">Call Us:</span>
<p className="font-bold">
<a href={`tel:${contactInfo.phone}`}>
{contactInfo.phone}
</a>
</p>
</div>
</div>
<div className="flex gap-3">
<div className="w-10 h-10 rounded-full bg-blue-600 flex items-center justify-center flex-shrink-0">
</div>
<div>
<span className="text-sm">Mail Us:</span>
<p className="font-bold">
<a href={`mailto:${contactInfo.email}`}>
{contactInfo.email}
</a>
</p>
</div>
</div>
<div className="flex gap-3">
<div className="w-10 h-10 rounded-full bg-blue-600 flex items-center justify-center flex-shrink-0">
📍
</div>
<div>
<span className="text-sm">Location:</span>
<p className="font-bold">{contactInfo.location}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer
className="py-20 px-4 lg:px-8 bg-cover text-white relative"
style={{
backgroundImage: `url('${ASSET_URL}/assets/img/home-1/footer-bg.jpg')`,
}}
>
<div className="absolute inset-0 bg-black/60"></div>
<div className="max-w-7xl mx-auto relative z-10 text-center">
<h2 className="text-3xl font-bold mb-4">
<a href="tel:+16336547896">+163 3654 7896</a>
</h2>
<h2 className="text-xl mb-8">
69 Street, 5th Avenue LA, United States
</h2>
<Link href="/">
<img
src={`${ASSET_URL}/assets/img/logo/white-logo.svg`}
alt="logo"
className="h-10 mx-auto mb-8"
/>
</Link>
<ul className="flex flex-wrap justify-center gap-8 mb-8">
<li>
<a href="/" className="hover:text-blue-300">
Home
</a>
</li>
<li>
<a href="/about" className="hover:text-blue-300">
About Us
</a>
</li>
<li>
<a href="/country-list" className="hover:text-blue-300">
Visa
</a>
</li>
<li>
<a href="/blog" className="hover:text-blue-300">
Pages
</a>
</li>
<li>
<a href="/contact" className="hover:text-blue-300">
Contact Us
</a>
</li>
</ul>
<div className="flex justify-center gap-6">
<a href="#" className="hover:text-blue-300">
𝕏
</a>
<a href="#" className="hover:text-blue-300">
📷
</a>
<a href="#" className="hover:text-blue-300">
in
</a>
<a href="#" className="hover:text-blue-300">
</a>
</div>
</div>
</footer>
</>
);
}

View File

@@ -0,0 +1,233 @@
import React from "react";
import Breadcrumb from "../../components/Breadcrumb";
import { type VisaCountry } from "@/api/visa";
interface VisaDetailProps {
country: VisaCountry;
}
export default function VisaDetail({ country }: VisaDetailProps) {
const { name: rootName, detailedView } = country;
const {
activeCountry: countryData,
relatedCountries,
contactInfo,
} = country.detailedView;
return (
<>
{/* Breadcrumb-Wrapper Section Start */}
<Breadcrumb title={rootName} current={rootName} />
{/* Country-details Section Start */}
<section className="country-details-section section-padding fix">
<div className="container">
<div className="country-details-wrapper">
<div className="row g-4">
{/* Main Content */}
<div className="col-lg-8">
<div className="country-details-post">
<div className="details-image">
<img src={countryData.mainImage} alt="img" />
</div>
<div className="country-details-content">
<h2>{countryData.name}</h2>
<p>{countryData.description}</p>
<p className="mt-3">{countryData.additionalInfo}</p>
<h5>{countryData.tagline}</h5>
{/* Visa Types */}
<div className="tourist-visa-box">
{/* Render mảng đầu tiên (index 0) */}
{countryData.visaTypes?.[0] && (
<div className="tourist-box style-2">
{countryData.visaTypes[0].items.map(
(item: any, itemIdx: number) => (
<div key={itemIdx} className="tourist-content">
<h5>{item.title}</h5>
<p>{item.description}</p>
</div>
),
)}
</div>
)}
{/* Render mảng thứ hai (index 1) */}
{countryData.visaTypes?.[1] && (
<div className="tourist-box">
{countryData.visaTypes[1].items.map(
(item: any, itemIdx: number) => (
<div key={itemIdx} className="tourist-content">
<h5>{item.title}</h5>
<p>{item.description}</p>
</div>
),
)}
</div>
)}
</div>
{/* Visa Process */}
{countryData.visaProcess && (
<>
<h3 className="text">
{countryData.visaProcess.title}
</h3>
<ul className="list-item">
{countryData.visaProcess.steps.map(
(process: any, idx: number) => (
<li key={idx}>
<strong>
{process.number}. {process.title}
</strong>
<span>{process.description}</span>
</li>
),
)}
</ul>
</>
)}
{/* Gallery */}
{countryData.gallery && countryData.gallery.length > 0 && (
<div className="row g-4 mb-4">
{countryData.gallery.map(
(image: string, idx: number) => (
<div key={idx} className="col-lg-6">
<div className="thumb">
<img src={image} alt="gallery-img" />
</div>
</div>
),
)}
</div>
)}
{/* Visa Categories */}
{countryData.visaCategories && (
<>
<h3 className="text mb-3">
{countryData.visaCategories.title}
</h3>
{countryData.visaCategories.steps.map(
(subGroup: string[], groupIdx: number) => (
<ul className="visa-list-2" key={groupIdx}>
{subGroup.map((category: string, idx: number) => (
<li key={idx}>
<i className="fa-solid fa-chevrons-right"></i>
{category}
</li>
))}
</ul>
),
)}
</>
)}
{/* Service Options */}
{countryData.visaService && (
<>
<h3 className="text">
{countryData.visaService.title}
</h3>
<ul className="list-item">
{countryData.visaService.steps.map(
(process: any, idx: number) => (
<li key={idx}>
<strong>
{process.number}. {process.title} -
</strong>
<span> {process.description}</span>
</li>
),
)}
</ul>
</>
)}
</div>
</div>
</div>
{/* Sidebar */}
<div className="col-lg-4">
<div className="country-details-sideber">
{relatedCountries.map((relCountry: any, idx: number) => (
<div key={idx} className="icon-box-item">
<div className="left-item">
<div className="icon">
<img src={relCountry.icon} alt="img" />
</div>
<h5>{relCountry.name}</h5>
</div>
<i className="fa-solid fa-chevrons-right"></i>
</div>
))}
{/* Contact Box */}
{contactInfo && (
<div
className="visa-contact-box bg-cover"
style={{
backgroundImage: `url(${contactInfo.img})`,
padding: "30px",
borderRadius: "8px",
color: "white",
}}
>
<div className="content">
<h3>{contactInfo.sectionTitle}</h3>
<p>{contactInfo.helpText}</p>
{/* Phone */}
<div className="icon-item">
<div className="icon">
<i className="fa-solid fa-phone"></i>
</div>
<div className="cont">
<span>{contactInfo.phone.label}: </span>
<h6>
<a href={contactInfo.phone.link}>
{contactInfo.phone.value}
</a>
</h6>
</div>
</div>
{/* Email */}
<div className="icon-item">
<div className="icon">
<i className="fa-regular fa-envelope"></i>
</div>
<div className="cont">
<span>{contactInfo.email.label}: </span>
<h6>
<a href={contactInfo.email.link}>
{contactInfo.email.value}
</a>
</h6>
</div>
</div>
{/* Location */}
<div className="icon-item">
<div className="icon">
<i className="fa-regular fa-location-dot"></i>
</div>
<div className="cont">
<span>{contactInfo.location.label}: </span>
<h6>{contactInfo.location.address}</h6>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</section>
</>
);
}

View File

@@ -2,17 +2,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import React from "react"; import React from "react";
import visaData from "../visa.json"; import VisaDetail from "./VisaDetail";
import Breadcrumb from "../components/Breadcrumb"; import { fetchVisaData, type VisaCountry } from "@/api/visa";
const ASSET_URL = process.env.NEXT_PUBLIC_API_URL || "";
interface VisaCountryData {
id: number;
name: string;
icon: string;
services: string[];
}
interface CountryDetailsProps { interface CountryDetailsProps {
params: Promise<{ params: Promise<{
@@ -20,48 +11,39 @@ interface CountryDetailsProps {
}>; }>;
} }
// Helper function to map slugs to country names
const getCountryFromSlug = (slug: string): string => {
const slugToCountry: { [key: string]: string } = {
france: "France",
uk: "UK",
canada: "Canada",
germany: "Germany",
spain: "Spain",
"south-korea": "South Korea",
japan: "Japan",
croatia: "Croatia",
england: "England",
indonesia: "Indonesia",
"united-states-of-america": "United States of America",
};
return slugToCountry[slug] || slug;
};
export default function CountryDetailsPage({ params }: CountryDetailsProps) { export default function CountryDetailsPage({ params }: CountryDetailsProps) {
const [country, setCountry] = useState<VisaCountryData | null>(null); // 1. Quản lý trạng thái dữ liệu
const [country, setCountry] = useState<VisaCountry | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [slug, setSlug] = useState<string>("");
useEffect(() => { useEffect(() => {
// Unwrap the params Promise const initPage = async () => {
Promise.resolve(params).then((resolvedParams) => { try {
const currentSlug = resolvedParams.slug; setLoading(true);
setSlug(currentSlug);
const countryName = getCountryFromSlug(currentSlug); // 2. Giải nén params (Vì params là Promise trong Next.js mới)
const foundCountry = visaData.visaSystem.summaryList.find( const resolvedParams = await params;
(c) => c.name === countryName, const currentSlug = resolvedParams.slug;
// 3. Lấy dữ liệu từ API bên trong useEffect
const visaData = await fetchVisaData();
// 4. Tìm nước trong danh sách summaryList dựa trên slug từ URL
const found = visaData.hero.summaryList.find(
(item: VisaCountry) => item.slug === currentSlug,
); );
if (foundCountry) { if (found) {
setCountry(foundCountry); setCountry(found);
} }
} catch (error) {
console.error("Lỗi khi tải dữ liệu:", error);
} finally {
setLoading(false); setLoading(false);
}); }
}, [params]); };
initPage();
}, [params]);
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
@@ -70,215 +52,14 @@ export default function CountryDetailsPage({ params }: CountryDetailsProps) {
); );
} }
if (!country) { // Xử lý khi không tìm thấy dữ liệu
if (!country || !country.detailedView?.activeCountry) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<h1>Country not found</h1> <h1>Country details not found</h1>
</div> </div>
); );
} }
const breadcrumbData = visaData.visaSystem.breadcrumb; return <VisaDetail country={country} />;
const countryData = visaData.visaSystem.detailedView.activeCountry;
const relatedCountries = visaData.visaSystem.detailedView.relatedCountries;
const contactInfo = visaData.visaSystem.contactInfo;
return (
<>
{/* Breadcrumb-Wrapper Section Start */}
<Breadcrumb
title={breadcrumbData.list.title}
breadcrumbItems={[
{ label: "Home", href: "/" },
{ label: countryData.name },
]}
backgroundImage={`${ASSET_URL}/${breadcrumbData.list.image}`}
/>
{/* Country-details Section Start */}
<section className="country-details-section section-padding fix">
<div className="container">
<div className="country-details-wrapper">
<div className="row g-4">
{/* Main Content */}
<div className="col-lg-8">
<div className="country-details-post">
<div className="details-image">
<img
src={`${ASSET_URL}/${countryData.mainImage}`}
alt="img"
/>
</div>
<div className="country-details-content">
<h2>{countryData.name}</h2>
<p>{countryData.description}</p>
<p className="mt-3">{countryData.additionalInfo}</p>
<h5>{countryData.tagline}</h5>
{/* Visa Types */}
<div className="tourist-visa-box">
{/* Render mảng đầu tiên (index 0) */}
{countryData.visaTypes[0] && (
<div className="tourist-box style-2">
{countryData.visaTypes[0].items.map(
(item: any, itemIdx: number) => (
<div key={itemIdx} className="tourist-content">
<h5>{item.title}</h5>
<p>{item.description}</p>
</div>
),
)}
</div>
)}
{/* Render mảng thứ hai (index 1) */}
{countryData.visaTypes[1] && (
<div className="tourist-box">
{countryData.visaTypes[1].items.map(
(item: any, itemIdx: number) => (
<div key={itemIdx} className="tourist-content">
<h5>{item.title}</h5>
<p>{item.description}</p>
</div>
),
)}
</div>
)}
</div>
{/* Visa Process */}
<h3 className="text">{countryData.visaProcess.title}</h3>
<ul className="list-item">
{countryData.visaProcess.steps.map(
(process: any, idx: number) => (
<li key={idx}>
{process.number}. {process.title}
<span>{process.description}</span>
</li>
),
)}
</ul>
{/* Gallery */}
<div className="row g-4 mb-4">
{countryData.gallery.map((image: string, idx: number) => (
<div key={idx} className="col-lg-6">
<div className="thumb">
<img src={`${ASSET_URL}/${image}`} alt="img" />
</div>
</div>
))}
</div>
{/* Visa Categories */}
<h3 className="text mb-3">
{countryData.visaCategories.title}
</h3>
{countryData.visaCategories.steps.map(
(subGroup: string[], groupIdx: number) => (
<ul className="visa-list-2" key={groupIdx}>
{/* Map lần 2 để render từng chuỗi trong mảng con */}
{subGroup.map((category: string, idx: number) => (
<li key={idx}>
<i className="fa-solid fa-chevrons-right"></i>
{category}
</li>
))}
</ul>
),
)}
{/* Service Options */}
<h3 className="text">{countryData.visaService.title}</h3>
<ul className="list-item">
{countryData.visaService.steps.map(
(process: any, idx: number) => (
<li key={idx}>
{process.number}. {process.title}
<span>{process.description}</span>
</li>
),
)}
</ul>
</div>
</div>
</div>
{/* Sidebar */}
<div className="col-lg-4">
<div className="country-details-sideber">
{relatedCountries.map((relCountry: any, idx: number) => (
<div key={idx} className="icon-box-item">
<div className="left-item">
<div className="icon">
<img
src={`${ASSET_URL}/${relCountry.icon}`}
alt="img"
/>
</div>
<h5>{relCountry.name}</h5>
</div>
<i className="fa-solid fa-chevrons-right"></i>
</div>
))}
{/* Contact Box */}
<div
className="visa-contact-box bg-cover"
style={{
backgroundImage: `url(${ASSET_URL}/assets/img/inner-page/country-details/bg.jpg)`,
padding: "30px",
borderRadius: "8px",
color: "white",
}}
>
<div className="content">
<h3>{contactInfo.sectionTitle}</h3>
<p>{contactInfo.helpText}</p>
<div className="icon-item">
<div className="icon">
<i className="fa-solid fa-phone"></i>
</div>
<div className="cont">
<span>{contactInfo.phone.label}: </span>
<h6>
<a href={`tel:${contactInfo.phone.link}`}>
{contactInfo.phone.value}
</a>
</h6>
</div>
</div>
<div className="icon-item">
<div className="icon">
<i className="fa-regular fa-envelope"></i>
</div>
<div className="cont">
<span>{contactInfo.email.label}: </span>
<h6>
<a href={`mailto:${contactInfo.email.link}`}>
{contactInfo.email.value}
</a>
</h6>
</div>
</div>
<div className="icon-item">
<div className="icon">
<i className="fa-regular fa-location-dot"></i>
</div>
<div className="cont">
<span>{contactInfo.location.label}: </span>
<h6>{contactInfo.location.address}</h6>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</>
);
} }

View File

@@ -13,7 +13,7 @@ interface BreadcrumbProps {
export default function Breadcrumb({ export default function Breadcrumb({
title, title,
breadcrumbItems, breadcrumbItems,
backgroundImage = `${ASSET_URL}/assets/img/inner-page/breadcrumb.jpg`, backgroundImage = `${ASSET_URL}/img/inner-page/breadcrumb.jpg`,
showShape = true, showShape = true,
}: BreadcrumbProps) { }: BreadcrumbProps) {
return ( return (
@@ -25,7 +25,7 @@ export default function Breadcrumb({
> >
{showShape && ( {showShape && (
<div className="shape"> <div className="shape">
<img src={`${ASSET_URL}/assets/img/inner-page/shape.png`} alt="img" /> <img src={`${ASSET_URL}/img/inner-page/shape.png`} alt="img" />
</div> </div>
)} )}
<div className="container"> <div className="container">

View File

@@ -1,7 +1,4 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
const ASSET_URL = process.env.NEXT_PUBLIC_API_URL || "";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Visaway Immigration & Visa Consulting HTML Template", title: "Visaway Immigration & Visa Consulting HTML Template",
description: "Visaway Immigration & Visa Consulting HTML Template", description: "Visaway Immigration & Visa Consulting HTML Template",
@@ -20,392 +17,8 @@ export default function VisaLayout({
}) { }) {
return ( return (
<> <>
{/* Preloader Start */}
<div id="preloader" className="preloader" style={{ display: "none" }}>
<div className="animation-preloader">
<div className="spinner"></div>
<div className="txt-loading">
<span data-text-preloader="V" className="letters-loading">
{" "}
V{" "}
</span>
<span data-text-preloader="I" className="letters-loading">
{" "}
I{" "}
</span>
<span data-text-preloader="S" className="letters-loading">
{" "}
S{" "}
</span>
<span data-text-preloader="A" className="letters-loading">
{" "}
A{" "}
</span>
<span data-text-preloader="W" className="letters-loading">
{" "}
W{" "}
</span>
<span data-text-preloader="A" className="letters-loading">
{" "}
A{" "}
</span>
<span data-text-preloader="Y" className="letters-loading">
{" "}
Y{" "}
</span>
</div>
<p className="text-center">Loading</p>
</div>
<div className="loader">
<div className="row">
<div className="col-3 loader-section section-left">
<div className="bg"></div>
</div>
<div className="col-3 loader-section section-left">
<div className="bg"></div>
</div>
<div className="col-3 loader-section section-right">
<div className="bg"></div>
</div>
<div className="col-3 loader-section section-right">
<div className="bg"></div>
</div>
</div>
</div>
</div>
{/* GT Back To Top Start */}
<button id="back-top" className="back-to-top show">
<i className="fa-regular fa-arrow-up"></i>
</button>
{/* GT MouseCursor Start */}
<div className="mouseCursor cursor-outer"></div>
<div className="mouseCursor cursor-inner"></div>
{/* Header-Top-Section Start */}
<div className="header-top-section">
<div className="container-fluid">
<div className="header-top-wrapper">
<div className="header-left">
<ul className="list">
<li className="style-2">
<span>Help Line</span>
<i className="fa-solid fa-phone"></i>
<a href="tel:+093783575222">+09 378 357 5222</a>
</li>
<li>
<i className="fa-solid fa-location-dot"></i>
69 Street, 5th AvenueLA, United States
</li>
<li>
<i className="fa-solid fa-envelope"></i>
<a href="mailto:info@example.com">info@example.com</a>
</li>
</ul>
</div>
<div className="header-right">
<div className="flag-wrap">
<div className="flag">
<i className="fa-solid fa-globe"></i>
</div>
<div className="nice-select" tabIndex={0}>
<span className="current"> English </span>
<ul className="list">
<li data-value="1" className="option selected focus">
English
</li>
<li data-value="1" className="option">
Bangla
</li>
<li data-value="1" className="option">
Hindi
</li>
</ul>
</div>
</div>
<div className="social-item">
<a href="#">
<i className="fa-brands fa-linkedin"></i>
</a>
<a href="#">
<i className="fa-brands fa-twitter"></i>
</a>
<a href="#">
<i className="fa-brands fa-instagram"></i>
</a>
<a href="#">
<i className="fa-brands fa-youtube"></i>
</a>
</div>
</div>
</div>
</div>
</div>
{/* Offcanvas Area Start */}
<div className="fix-area">
<div className="offcanvas__info">
<div className="offcanvas__wrapper">
<div className="offcanvas__content">
<div className="offcanvas__top mb-5 d-flex justify-content-between align-items-center">
<div className="offcanvas__logo">
<a href="/">
<img
src={`${ASSET_URL}/assets/img/logo/black-logo.svg`}
alt="logo-img"
/>
</a>
</div>
<div className="offcanvas__close">
<button>
<i className="fas fa-times"></i>
</button>
</div>
</div>
<p className="text d-none d-xl-block">
Nullam dignissim, ante scelerisque the is euismod fermentum odio
sem semper the is erat, a feugiat leo urna eget eros. Duis
Aenean a imperdiet risus.
</p>
<div className="mobile-menu fix mb-3"></div>
<div className="offcanvas__contact d-xl-block">
<h4 className="d-xl-block">Contact Info</h4>
<ul className="d-xl-block">
<li className="d-flex align-items-center">
<div className="offcanvas__contact-icon">
<i className="fal fa-map-marker-alt"></i>
</div>
<div className="offcanvas__contact-text">
<a target="_blank" href="#">
Main Street, Melbourne, Australia
</a>
</div>
</li>
<li className="d-flex align-items-center">
<div className="offcanvas__contact-icon mr-15">
<i className="fal fa-envelope"></i>
</div>
<div className="offcanvas__contact-text">
<a href="mailto:info@example.com">
<span>info@example.com</span>
</a>
</div>
</li>
<li className="d-flex align-items-center">
<div className="offcanvas__contact-icon mr-15">
<i className="fal fa-clock"></i>
</div>
<div className="offcanvas__contact-text">
<a target="_blank" href="#">
Mod-friday, 09am -05pm
</a>
</div>
</li>
<li className="d-flex align-items-center">
<div className="offcanvas__contact-icon mr-15">
<i className="far fa-phone"></i>
</div>
<div className="offcanvas__contact-text">
<a href="tel:+11002345909">+11002345909</a>
</div>
</li>
</ul>
<div className="social-icon d-flex align-items-center">
<a href="#">
<i className="fab fa-facebook-f"></i>
</a>
<a href="#">
<i className="fab fa-twitter"></i>
</a>
<a href="#">
<i className="fab fa-youtube"></i>
</a>
<a href="#">
<i className="fab fa-linkedin-in"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="offcanvas__overlay"></div>
{/* Header Section Start */}
{/* <header id="header-sticky" className="header-1">
<div className="container-fluid">
<div className="mega-menu-wrapper">
<div className="header-main">
<div className="header-left">
<div className="logo">
<a href="/" className="header-logo-2">
<img
src={`${ASSET_URL}/assets/img/logo/black-logo.svg`}
alt="logo-img"
/>
</a>
</div>
<div className="mean__menu-wrapper">
<div className="main-menu">
<nav id="mobile-menu">
<ul>
<li className="has-dropdown active menu-thumb">
<a href="/"> Home </a>
</li>
<li>
<a href="/about">About Us</a>
</li>
<li className="has-dropdown">
<a href="#"> Pages </a>
<ul className="submenu">
<li>
<a href="/services">Services</a>
</li>
<li>
<a href="/visa">Country List</a>
</li>
<li>
<a href="/pricing">Our Pricing</a>
</li>
<li>
<a href="/appointment">Appointment</a>
</li>
</ul>
</li>
<li>
<a href="/visa"> VISA </a>
</li>
<li>
<a href="/blog"> Blog </a>
</li>
<li>
<a href="/contact">Contact Us</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div className="header-right d-flex align-items-center mt-0">
<div className="header-call-item">
<a href="#" className="main-header__search search-toggler">
<i className="fa-regular fa-magnifying-glass"></i>
</a>
<a href="/contact" className="theme-btn">
Apply now
<i className="fa-solid fa-arrow-right"></i>
</a>
<div className="header__hamburger my-auto">
<div className="sidebar__toggle">
<i className="fa-solid fa-bars-staggered"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</header> */}
{/* Search Area Start */}
<div className="search-popup">
<div className="search-popup__overlay search-toggler"></div>
<div className="search-popup__content">
<form
role="search"
method="get"
className="search-popup__form"
action="#"
>
<input
type="text"
id="search"
name="search"
placeholder="Search Here..."
/>
<button
type="submit"
aria-label="search submit"
className="search-btn"
>
<span>
<i className="fa-regular fa-magnifying-glass"></i>
</span>
</button>
</form>
</div>
</div>
{/* Main Content */} {/* Main Content */}
{children} {children}
{/* Footer Section Start */}
{/* <footer
className="footer-section fix bg-cover"
style={{
backgroundImage: `url(${ASSET_URL}/assets/img/home-1/footer-bg.jpg)`,
}}
>
<div className="container">
<div className="footer-wrapper">
<div className="row">
<div className="col-xl-12">
<div className="footer-item">
<h2>
<a href="tel:+16336547896">+163 3654 7896</a>
</h2>
<h2 className="text">
69 Street, 5th AvenueLA, United States
</h2>
<div className="footer-list-item">
<a href="/">
<img
src={`${ASSET_URL}/assets/img/logo/white-logo.svg`}
alt="img"
/>
</a>
<ul className="footer-list">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/about">About Us</a>
</li>
<li>
<a href="/visa">Visa</a>
</li>
<li>
<a href="/blog">Pages</a>
</li>
<li>
<a href="/blog">Article</a>
</li>
<li>
<a href="/contact">Contact Us</a>
</li>
</ul>
<div className="social-icon">
<a href="#">
<i className="fa-brands fa-twitter"></i>
</a>
<a href="#">
<i className="fa-brands fa-instagram"></i>
</a>
<a href="#">
<i className="fa-brands fa-linkedin"></i>
</a>
<a href="#">
<i className="fa-brands fa-youtube"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</footer> */}
</> </>
); );
} }

View File

@@ -1,40 +1,52 @@
"use client"; // app/visa/page.tsx
import visaData from "./visa.json"; import Breadcrumb from "../components/Breadcrumb";
import Breadcrumb from "./components/Breadcrumb"; import { fetchVisaData, type VisaCountry } from "@/api/visa";
const ASSET_URL = process.env.NEXT_PUBLIC_API_URL || ""; export default async function VisaListPage() {
// Fetch all visa countries từ API
interface VisaCountry { let visaCountries: any[] = [];
id: number; let visaHero: any = {};
name: string; const visaTitle = "Visa Services";
icon: string; const breadcrumbCurrent = "Visa Services";
services: string[]; let error: string | null = null;
try {
const visaData = await fetchVisaData();
visaCountries = visaData.hero.summaryList.map((country: VisaCountry) => ({
...country,
icon: country.icon,
}));
visaHero = visaData.hero;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error("Error fetching visa countries:", errorMessage);
error = errorMessage;
} }
const visaCountries: VisaCountry[] = visaData.visaSystem.summaryList.map( // Fallback message nếu có error
(country) => ({ if (error && visaCountries.length === 0) {
...country, return (
icon: `${ASSET_URL}/${country.icon}`, <>
}), <Breadcrumb title={visaTitle} current={breadcrumbCurrent} />
<section className="visa-provide-section section-padding section-bg-1 fix">
<div className="container">
<div className="row">
<div className="col-12 text-center">
<p className="text-danger">
Error loading visa services: {error}
</p>
</div>
</div>
</div>
</section>
</>
); );
}
export default function VisaListPage() {
const getSlug = (countryName: string): string => {
return countryName.toLowerCase().replace(/\s+/g, "-");
};
return ( return (
<> <>
{/* Breadcrumb-Wrapper Section Start */} {/* Breadcrumb-Wrapper Section Start */}
<Breadcrumb <Breadcrumb title={visaHero.title} current={visaHero.title} />
title={visaData.visaSystem.breadcrumb.list.title}
breadcrumbItems={[
{ label: "Home", href: "/" },
{ label: visaData.visaSystem.breadcrumb.list.title },
]}
backgroundImage={`${ASSET_URL}/${visaData.visaSystem.breadcrumb.list.image}`}
/>
{/* Service Section Start */} {/* Service Section Start */}
<section className="visa-provide-section section-padding section-bg-1 fix"> <section className="visa-provide-section section-padding section-bg-1 fix">
@@ -46,45 +58,58 @@ export default function VisaListPage() {
<div className="visa-top-item"> <div className="visa-top-item">
<div className="visa-left"> <div className="visa-left">
<div className="icon"> <div className="icon">
<img src={country.icon} alt="img" /> <img
src={country.icon}
alt={country.name}
loading="lazy"
/>
</div> </div>
<div className="content"> <div className="content">
<p>Visa Service</p> <p>Visa Service</p>
<h3> <h3>
<a href={`/visa/${getSlug(country.name)}`}> <a href={`/visa/${country.slug}`}>{country.name}</a>
{country.name}
</a>
</h3> </h3>
</div> </div>
</div> </div>
<a
href={`/visa/${getSlug(country.name)}`} {/* Read More Button */}
className="theme-btn" <a href={`/visa/${country.slug}`} className="theme-btn">
>
Read More Read More
<i className="fa-solid fa-arrow-right"></i> <i className="fa-solid fa-arrow-right"></i>
</a> </a>
</div> </div>
{/* Services List */}
<div className="visa-list-item"> <div className="visa-list-item">
{/* First Column */}
<ul className="list"> <ul className="list">
{country.services[0] && (
<li> <li>
<i className="fa-regular fa-arrow-right"></i> <i className="fa-regular fa-arrow-right"></i>
{country.services[0]} {country.services[0]}
</li> </li>
)}
{country.services[1] && (
<li> <li>
<i className="fa-regular fa-arrow-right"></i> <i className="fa-regular fa-arrow-right"></i>
{country.services[1]} {country.services[1]}
</li> </li>
)}
</ul> </ul>
{/* Second Column */}
<ul className="list"> <ul className="list">
{country.services[2] && (
<li> <li>
<i className="fa-regular fa-arrow-right"></i> <i className="fa-regular fa-arrow-right"></i>
{country.services[2]} {country.services[2]}
</li> </li>
)}
{country.services[3] && (
<li> <li>
<i className="fa-regular fa-arrow-right"></i> <i className="fa-regular fa-arrow-right"></i>
{country.services[3]} {country.services[3]}
</li> </li>
)}
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -1,122 +1,17 @@
{ {
"visaSystem": { "hero": {
"breadcrumb": { "title": "Visa",
"list": {
"title": "Country List",
"image": "assets/img/inner-page/breadcrumb.jpg"
}
},
"summaryList": [ "summaryList": [
{ {
"id": 1, "id": 1,
"name": "France", "name": "France",
"slug": "france",
"icon": "assets/img/home-2/visa/03.png", "icon": "assets/img/home-2/visa/03.png",
"services": [ "services": [
"Student Visa & Admission", "Student Visa & Admission",
"Work Visa H1B", "Work Visa H1B",
"Work permit for Canada", "Work permit for Canada",
"Student Visa for Canada" "Student Visa for Canada"
]
},
{
"id": 2,
"name": "UK",
"icon": "assets/img/home-2/visa/11.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 3,
"name": "Canada",
"icon": "assets/img/home-2/visa/02.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 4,
"name": "Germany",
"icon": "assets/img/home-2/visa/12.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 5,
"name": "Spain",
"icon": "assets/img/home-2/visa/13.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 6,
"name": "South Korea",
"icon": "assets/img/home-2/visa/14.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 7,
"name": "Japan",
"icon": "assets/img/home-2/visa/15.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 8,
"name": "Croatia",
"icon": "assets/img/home-2/visa/16.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 9,
"name": "England",
"icon": "assets/img/home-2/visa/17.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 10,
"name": "Indonesia",
"icon": "assets/img/home-2/visa/18.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
}
], ],
"detailedView": { "detailedView": {
"activeCountry": { "activeCountry": {
@@ -197,7 +92,10 @@
"Work Visa (H1B, L1)", "Work Visa (H1B, L1)",
"Tourist Visa (B1/B2)" "Tourist Visa (B1/B2)"
], ],
["Family/Spouse Visa (K1, IR1, F2A)", "Green Card / Immigrant Visa"] [
"Family/Spouse Visa (K1, IR1, F2A)",
"Green Card / Immigrant Visa"
]
] ]
}, },
"visaService": { "visaService": {
@@ -267,11 +165,11 @@
"name": "Japan", "name": "Japan",
"icon": "assets/img/inner-page/country-details/08.png" "icon": "assets/img/inner-page/country-details/08.png"
} }
] ],
},
"contactInfo": { "contactInfo": {
"sectionTitle": "Visa & Immigration", "sectionTitle": "Visa & Immigration",
"helpText": "Need Help? Book Lab Visit", "helpText": "Need Help? Book Lab Visit",
"img": "assets/img/inner-page/country-details/contact-bg.jpg",
"phone": { "phone": {
"label": "Call Us", "label": "Call Us",
"value": "+009 438 222 9540", "value": "+009 438 222 9540",
@@ -288,4 +186,115 @@
} }
} }
} }
},
{
"id": 2,
"name": "UK",
"slug": "uk",
"icon": "assets/img/home-2/visa/11.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 3,
"name": "Canada",
"slug": "canada",
"icon": "assets/img/home-2/visa/02.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 4,
"name": "Germany",
"slug": "germany",
"icon": "assets/img/home-2/visa/12.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 5,
"name": "Spain",
"slug": "spain",
"icon": "assets/img/home-2/visa/13.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 6,
"name": "South Korea",
"slug": "south-korea",
"icon": "assets/img/home-2/visa/14.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 7,
"name": "Japan",
"slug": "japan",
"icon": "assets/img/home-2/visa/15.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 8,
"name": "Croatia",
"slug": "croatia",
"icon": "assets/img/home-2/visa/16.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 9,
"name": "England",
"slug": "england",
"icon": "assets/img/home-2/visa/17.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
},
{
"id": 10,
"name": "Indonesia",
"slug": "indonesia",
"icon": "assets/img/home-2/visa/18.png",
"services": [
"Student Visa & Admission",
"Work Visa H1B",
"Work permit for Canada",
"Student Visa for Canada"
]
}
]
}
} }

20
lib/axios.ts Normal file
View File

@@ -0,0 +1,20 @@
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Response interceptor for basic error handling
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@@ -9777,3 +9777,4 @@ html.lenis body {
a { a {
color: var(--white); color: var(--white);
} /*# sourceMappingURL=main.css.map */ } /*# sourceMappingURL=main.css.map */

View File

@@ -0,0 +1,20 @@
import axiosInstance from '../lib/axios';
import { HeaderMenu } from '../types/header-menu';
export const headerMenuService = {
/**
* Fetch active header menu tree from API
*/
async getHeaderMenu(): Promise<HeaderMenu[]> {
try {
const response = await axiosInstance.get<{ success: boolean; data: HeaderMenu[] }>('/api/header-menu');
if (response.data.success) {
return response.data.data;
}
return [];
} catch (error) {
console.error('Failed to fetch header menu:', error);
return []; // Fallback to empty menu
}
}
};

7
types/header-menu.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface HeaderMenu {
id: string;
title: string;
url: string;
type?: 'internal' | 'external';
children?: HeaderMenu[];
}