forked from UKSOURCE/hailearning.edu.vn
merge code
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
PORT=3000
|
||||||
|
NEXT_PUBLIC_API_URL=https://www.hailearning.edu.vn/
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,7 +31,7 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
35
api/aboutApi.ts
Normal file
35
api/aboutApi.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* About API Functions
|
||||||
|
* Fetch about us data from external API
|
||||||
|
*/
|
||||||
|
|
||||||
|
const getApiUrl = (): string => {
|
||||||
|
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
|
||||||
|
};
|
||||||
|
|
||||||
|
import { AboutData } from "../app/about/types";
|
||||||
|
|
||||||
|
export const aboutApi = {
|
||||||
|
// Get about us data
|
||||||
|
getAbout: async (): Promise<AboutData | null> => {
|
||||||
|
try {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
const response = await fetch(`${apiUrl}/api/about`, {
|
||||||
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`HTTP error! status: ${response.status}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching about data:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@@ -53,9 +53,7 @@ export const fetchBlogList = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// Next.js: cache và revalidate (disabled)
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// next: { revalidate: 60 }, // Revalidate mỗi 60 giây
|
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,8 +89,7 @@ export const fetchBlogDetail = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// No cache for blog detail (disabled caching)
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,8 +128,7 @@ export const fetchFeaturedBlogs = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 60 },
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,8 +164,7 @@ export const fetchRecentBlogs = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 60 },
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,8 +197,7 @@ export const fetchCategories = async (): Promise<CategoryListResponse> => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 300 }, // Categories ít thay đổi, cache lâu hơn
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,8 +233,7 @@ export const fetchCategoryDetail = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 300 },
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -276,8 +269,7 @@ export const fetchTags = async (): Promise<TagListResponse> => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 300 },
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -313,8 +305,7 @@ export const fetchPopularTags = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 300 },
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -350,8 +341,7 @@ export const fetchTagDetail = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
// next: { revalidate: 300 },
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
// no-cache
|
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ export const fetchServicePageData = async (): Promise<ServicePageData> => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Services API response status:", response.status);
|
console.log("Services API response status:", response.status);
|
||||||
@@ -179,6 +181,8 @@ export const fetchServiceBySlug = async (slug: string): Promise<any> => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
// Không cache - luôn fetch dữ liệu mới nhất
|
||||||
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Response status:", response.status);
|
console.log("Response status:", response.status);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface VisaItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface VisaTypeCategory {
|
interface VisaTypeCategory {
|
||||||
|
title?: string;
|
||||||
items: VisaItem[];
|
items: VisaItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +434,7 @@ function getFallbackVisaData(): VisaData {
|
|||||||
tagline: "35+ years of excellence in UK visa consulting.",
|
tagline: "35+ years of excellence in UK visa consulting.",
|
||||||
visaTypes: [
|
visaTypes: [
|
||||||
{
|
{
|
||||||
category: "Student & Work",
|
title: "Student & Work",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Student Visa",
|
title: "Student Visa",
|
||||||
@@ -448,7 +449,7 @@ function getFallbackVisaData(): VisaData {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "Family & Other",
|
title: "Family & Other",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Skilled Worker Visa",
|
title: "Skilled Worker Visa",
|
||||||
@@ -601,7 +602,7 @@ function getFallbackVisaData(): VisaData {
|
|||||||
tagline: "Your pathway to a better life in Canada starts here.",
|
tagline: "Your pathway to a better life in Canada starts here.",
|
||||||
visaTypes: [
|
visaTypes: [
|
||||||
{
|
{
|
||||||
category: "Study & Work",
|
title: "Study & Work",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Study Permit",
|
title: "Study Permit",
|
||||||
@@ -614,7 +615,7 @@ function getFallbackVisaData(): VisaData {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "Immigration & Family",
|
title: "Immigration & Family",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Express Entry",
|
title: "Express Entry",
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
{
|
|
||||||
"hero": {
|
|
||||||
"title": "About Us",
|
|
||||||
"subtitle": "Global Education Simplified",
|
|
||||||
"breadcrumb": ["Home", "About Us"],
|
|
||||||
"backgroundImage": "/assets/img/inner-page/breadcrumb.jpg"
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"subheading": "Company Intro",
|
|
||||||
"heading": "Building Pathways to Your Immigration Success",
|
|
||||||
"description": "We provide expert guidance, personalized solutions, and transparent processes to help you achieve your immigration goals. Our dedicated team ensures a smooth journey, building pathways to your international success.",
|
|
||||||
"image": "/assets/img/inner-page/intro.jpg"
|
|
||||||
},
|
|
||||||
"mission": {
|
|
||||||
"subheading": "About Our Consultancy",
|
|
||||||
"heading": "Turning Study Abroad Dreams Into Reality",
|
|
||||||
"description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.",
|
|
||||||
"images": {
|
|
||||||
"main": "/assets/img/home-1/about/about-1.jpg",
|
|
||||||
"secondary": "/assets/img/home-1/about/about-02.jpg",
|
|
||||||
"bgShape": "/assets/img/home-1/about/Vector.png",
|
|
||||||
"planeShape": "/assets/img/home-1/about/plane.png",
|
|
||||||
"topShape": "/assets/img/home-1/about/shape.png",
|
|
||||||
"globeShape": "/assets/img/home-1/about/globe.png"
|
|
||||||
},
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"icon": "/assets/img/home-1/icon/01.svg",
|
|
||||||
"label": "Global Reach",
|
|
||||||
"description": "Expanding Opportunities Worldwide"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "/assets/img/home-1/icon/01.svg",
|
|
||||||
"label": "Global Reach",
|
|
||||||
"description": "Expanding Opportunities Worldwide"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"features": [
|
|
||||||
"Fastest Visa form processing with skilled immigration agents",
|
|
||||||
"Partnership with International Educational Institutions"
|
|
||||||
],
|
|
||||||
"ctaButton": {
|
|
||||||
"label": "Get Started",
|
|
||||||
"href": "/about"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"features": {
|
|
||||||
"backgroundImage": "/assets/img/home-2/feature/bg-shape.png",
|
|
||||||
"subheading": "Your Travel Made Easy",
|
|
||||||
"heading": "Smooth Visa Journey Guaranteed",
|
|
||||||
"description": "We provide expert guidance for every visa application, ensuring smooth processing, personalized support, and reliable assistance",
|
|
||||||
"image": "/assets/img/home-2/feature/02.png",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"icon": "/assets/img/home-2/icon/01.png",
|
|
||||||
"title": "Expert Consultants",
|
|
||||||
"description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "/assets/img/home-2/icon/01.png",
|
|
||||||
"title": "Personalized Support",
|
|
||||||
"description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "/assets/img/home-2/icon/01.png",
|
|
||||||
"title": "Transparent Process",
|
|
||||||
"description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ctaButton": {
|
|
||||||
"label": "Get Started Today",
|
|
||||||
"href": "/contact"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"news": {
|
|
||||||
"subheading": "Visa Tips & Guides",
|
|
||||||
"heading": "Latest Insights & Updates",
|
|
||||||
"ctaButton": {
|
|
||||||
"label": "view all articles",
|
|
||||||
"href": "/blog"
|
|
||||||
},
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "Step-by-Step Guide to Applying for a Student Visa",
|
|
||||||
"category": "Student Visa",
|
|
||||||
"date": "20 August ,2025",
|
|
||||||
"comments": 8,
|
|
||||||
"author": {
|
|
||||||
"name": "Sohel",
|
|
||||||
"avatar": "/assets/img/home-1/news/client.png"
|
|
||||||
},
|
|
||||||
"link": "/blog/step-by-step-guide-student-visa",
|
|
||||||
"thumbnail": "/assets/img/home-1/news/news-1.jpg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Tips to Prepare Financial Documents for Visa Approval",
|
|
||||||
"category": "IELTS / TOEFL",
|
|
||||||
"date": "20 August ,2025",
|
|
||||||
"comments": 8,
|
|
||||||
"author": {
|
|
||||||
"name": "Sohel",
|
|
||||||
"avatar": "/assets/img/home-1/news/client.png"
|
|
||||||
},
|
|
||||||
"link": "/blog/financial-documents-visa-approval",
|
|
||||||
"thumbnail": "/assets/img/home-1/news/news-2.jpg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Post-Arrival Guide What Every Student Should Know",
|
|
||||||
"category": "Study Abroad",
|
|
||||||
"date": "20 August ,2025",
|
|
||||||
"comments": 8,
|
|
||||||
"author": {
|
|
||||||
"name": "Sohel",
|
|
||||||
"avatar": "/assets/img/home-1/news/client.png"
|
|
||||||
},
|
|
||||||
"link": "/blog/post-arrival-guide-students",
|
|
||||||
"thumbnail": "/assets/img/home-1/news/news-3.jpg"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
import { AboutHero, AboutIntro, AboutMission, AboutFeatures, AboutNews } from "../components/about";
|
import { AboutHero, AboutIntro, AboutMission, AboutFeatures, AboutNews } from "../components/about";
|
||||||
import aboutData from "./about.json";
|
import { aboutApi } from "../../api/aboutApi";
|
||||||
|
|
||||||
|
// Force dynamic rendering - không cache
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export default async function AboutPage() {
|
||||||
|
const data = await aboutApi.getAbout();
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AboutPage() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AboutHero data={aboutData.hero} />
|
<AboutHero data={data.hero} />
|
||||||
<AboutIntro data={aboutData.intro} />
|
<AboutIntro data={data.intro} />
|
||||||
<AboutMission data={aboutData.mission} />
|
<AboutMission data={data.mission} />
|
||||||
<AboutFeatures data={aboutData.features} />
|
<AboutFeatures data={data.features} />
|
||||||
<AboutNews data={aboutData.news} />
|
<AboutNews data={data.news} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export interface AboutData {
|
export interface AboutData {
|
||||||
hero: {
|
hero: {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
|
||||||
breadcrumb: string[];
|
breadcrumb: string[];
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { fetchBlogList, fetchBlogDetail } from "@/api/blogsApi";
|
|||||||
import Sidebar from "@/app/blog/components/Sidebar";
|
import Sidebar from "@/app/blog/components/Sidebar";
|
||||||
import { getCmsImageUrl } from "@/utils";
|
import { getCmsImageUrl } from "@/utils";
|
||||||
|
|
||||||
|
// Force dynamic rendering - không cache
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
// Generate static params for all blog posts
|
// Generate static params for all blog posts
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,47 +3,72 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Loader() {
|
export default function Loader() {
|
||||||
const [show, setShow] = useState(true);
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => setShow(false), 700);
|
const timer = setTimeout(() => setShow(false), 1100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="preloader" className="preloader">
|
<div id="preloader" className="preloader">
|
||||||
<div className="animation-preloader">
|
<div className="animation-preloader">
|
||||||
<div className="spinner"></div>
|
<div className="spinner"></div>
|
||||||
<div className="txt-loading">
|
<div className="txt-loading">
|
||||||
<span data-text-preloader="V" className="letters-loading">V</span>
|
<span data-text-preloader="H" className="letters-loading">
|
||||||
<span data-text-preloader="I" className="letters-loading">I</span>
|
H
|
||||||
<span data-text-preloader="S" className="letters-loading">S</span>
|
</span>
|
||||||
<span data-text-preloader="A" className="letters-loading">A</span>
|
<span data-text-preloader="A" className="letters-loading">
|
||||||
<span data-text-preloader="W" className="letters-loading">W</span>
|
A
|
||||||
<span data-text-preloader="A" className="letters-loading">A</span>
|
</span>
|
||||||
<span data-text-preloader="Y" className="letters-loading">Y</span>
|
<span data-text-preloader="I" className="letters-loading">
|
||||||
</div>
|
I
|
||||||
<p className="text-center">Loading</p>
|
</span>
|
||||||
</div>
|
<span data-text-preloader="L" className="letters-loading">
|
||||||
<div className="loader">
|
L
|
||||||
<div className="row">
|
</span>
|
||||||
<div className="col-3 loader-section section-left">
|
<span data-text-preloader="E" className="letters-loading">
|
||||||
<div className="bg"></div>
|
E
|
||||||
</div>
|
</span>
|
||||||
<div className="col-3 loader-section section-left">
|
<span data-text-preloader="A" className="letters-loading">
|
||||||
<div className="bg"></div>
|
A
|
||||||
</div>
|
</span>
|
||||||
<div className="col-3 loader-section section-right">
|
<span data-text-preloader="R" className="letters-loading">
|
||||||
<div className="bg"></div>
|
R
|
||||||
</div>
|
</span>
|
||||||
<div className="col-3 loader-section section-right">
|
<span data-text-preloader="N" className="letters-loading">
|
||||||
<div className="bg"></div>
|
N
|
||||||
</div>
|
</span>
|
||||||
</div>
|
<span data-text-preloader="I" className="letters-loading">
|
||||||
</div>
|
I
|
||||||
|
</span>
|
||||||
|
<span data-text-preloader="N" className="letters-loading">
|
||||||
|
N
|
||||||
|
</span>
|
||||||
|
<span data-text-preloader="G" className="letters-loading">
|
||||||
|
G
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,20 +14,22 @@ const AboutHero = ({ data }: AboutHeroProps) => {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="page-heading">
|
<div className="page-heading">
|
||||||
<h1 className="breadcrumb-title">{data.title}</h1>
|
<h1 className="breadcrumb-title">{data.title}</h1>
|
||||||
<ul className="breadcrumb-list">
|
{Array.isArray(data.breadcrumb) && (
|
||||||
{data.breadcrumb.map((item, index) => (
|
<ul className="breadcrumb-list">
|
||||||
<li key={index}>
|
{data.breadcrumb.map((item, index) => (
|
||||||
{index === data.breadcrumb.length - 1 ? (
|
<li key={index}>
|
||||||
item
|
{index === data.breadcrumb.length - 1 ? (
|
||||||
) : (
|
item
|
||||||
<>
|
) : (
|
||||||
<Link href="/">{item}</Link>
|
<>
|
||||||
<i className="fa-solid fa-chevron-right ms-2 me-2"></i>
|
<Link href="/">{item}</Link>
|
||||||
</>
|
<i className="fa-solid fa-chevron-right ms-2 me-2"></i>
|
||||||
)}
|
</>
|
||||||
</li>
|
)}
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const Partners = ({ data }: PartnersProps) => {
|
|||||||
{(data.visaConsultancy?.items || []).map((partner, index) => (
|
{(data.visaConsultancy?.items || []).map((partner, index) => (
|
||||||
<div key={index} className="col-xl-3 col-lg-4 col-md-6">
|
<div key={index} className="col-xl-3 col-lg-4 col-md-6">
|
||||||
<div className="visa-consultency-item">
|
<div className="visa-consultency-item">
|
||||||
<div className="image">
|
<div className="image d-flex justify-content-center">
|
||||||
<img src={getCmsImageUrl(partner.icon)} alt={partner.name} />
|
<img src={getCmsImageUrl(partner.icon)} alt={partner.name} />
|
||||||
</div>
|
</div>
|
||||||
<h3>{partner.name}</h3>
|
<h3>{partner.name}</h3>
|
||||||
@@ -48,7 +48,7 @@ const Partners = ({ data }: PartnersProps) => {
|
|||||||
<div className="swiper-wrapper">
|
<div className="swiper-wrapper">
|
||||||
{(data.brands?.items || []).map((brand, index) => (
|
{(data.brands?.items || []).map((brand, index) => (
|
||||||
<div key={index} className="swiper-slide">
|
<div key={index} className="swiper-slide">
|
||||||
<div className="brand-image text-center">
|
<div className="brand-image d-flex justify-content-center">
|
||||||
<img src={getCmsImageUrl(brand.logo)} alt="brand-logo" />
|
<img src={getCmsImageUrl(brand.logo)} alt="brand-logo" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import FooterTop from './FooterTop';
|
import FooterTop from './FooterTop';
|
||||||
import FooterBottom from './FooterBottom';
|
import FooterBottom from './FooterBottom';
|
||||||
|
import { footerApi, FooterData } from "../../../../api/footerApi";
|
||||||
|
import footerData from "./footer.json";
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
|
const [data, setData] = useState<FooterData>(footerData as FooterData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadFooterData = async () => {
|
||||||
|
try {
|
||||||
|
const apiData = await footerApi.getFooter();
|
||||||
|
setData(apiData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load footer data from API, using static data:", error);
|
||||||
|
// Keep using static data as fallback
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFooterData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FooterTop />
|
<FooterTop data={data} />
|
||||||
<FooterBottom />
|
<FooterBottom data={data} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { FooterData } from "../../../../api/footerApi";
|
||||||
import { footerApi, FooterData } from "../../../../api/footerApi";
|
|
||||||
import footerData from "./footer.json";
|
import footerData from "./footer.json";
|
||||||
|
|
||||||
const FooterBottom = () => {
|
interface FooterBottomProps {
|
||||||
const [data, setData] = useState<FooterData>(footerData as FooterData);
|
data: FooterData;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const FooterBottom = ({ data }: FooterBottomProps) => {
|
||||||
const loadFooterData = async () => {
|
const effectiveData = data || footerData;
|
||||||
try {
|
|
||||||
const apiData = await footerApi.getFooter();
|
|
||||||
setData(apiData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load footer data from API, using static data:", error);
|
|
||||||
// Keep using static data as fallback
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadFooterData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Ensure we always have a valid `bottom` object, even if API shape changes
|
// Ensure we always have a valid `bottom` object, even if API shape changes
|
||||||
const bottom = data?.bottom || footerData.bottom;
|
const bottom = effectiveData?.bottom || footerData.bottom;
|
||||||
|
|
||||||
// If bottom is still missing, avoid rendering to prevent runtime errors
|
// If bottom is still missing, avoid rendering to prevent runtime errors
|
||||||
if (!bottom) {
|
if (!bottom) {
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { FooterData } from "../../../../api/footerApi";
|
||||||
import { footerApi, FooterData } from "../../../../api/footerApi";
|
|
||||||
import footerData from "./footer.json";
|
import footerData from "./footer.json";
|
||||||
|
|
||||||
const FooterTop = () => {
|
interface FooterTopProps {
|
||||||
const [data, setData] = useState<FooterData>(footerData as FooterData);
|
data: FooterData;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const FooterTop = ({ data }: FooterTopProps) => {
|
||||||
const loadFooterData = async () => {
|
// Use passed data, fallback to static json if needed
|
||||||
try {
|
const effectiveData = data || footerData;
|
||||||
const apiData = await footerApi.getFooter();
|
|
||||||
setData(apiData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load footer data from API, using static data:", error);
|
|
||||||
// Keep using static data as fallback
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadFooterData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Ensure we always have a valid `top` object, even if API shape changes
|
// Ensure we always have a valid `top` object, even if API shape changes
|
||||||
const top = data?.top || footerData.top;
|
const top = effectiveData?.top || footerData.top;
|
||||||
|
|
||||||
// If for some reason `top` is still missing, avoid rendering to prevent runtime errors
|
// If for some reason `top` is still missing, avoid rendering to prevent runtime errors
|
||||||
if (!top) {
|
if (!top) {
|
||||||
|
|||||||
@@ -1,74 +1,97 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } 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 MobileMenu from "./MobileMenu";
|
||||||
import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
|
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 [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [menuItems, setMenuItems] = useState<any[]>([]);
|
const [menuItems, setMenuItems] = useState<any[]>([]);
|
||||||
|
const [headerData, setHeaderData] = useState<any>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen);
|
const toggleOffcanvas = () => setIsOffcanvasOpen(!isOffcanvasOpen);
|
||||||
|
const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
const toggleSearch = () => setIsSearchOpen(!isSearchOpen);
|
const toggleSearch = () => setIsSearchOpen(!isSearchOpen);
|
||||||
|
|
||||||
// Helper to adapt 'children' from API to 'submenu' for the existing components
|
// Helper to adapt 'children' from API to 'submenu' for the existing components
|
||||||
const adaptMenu = useCallback((item: HeaderMenuType): any => ({
|
const adaptMenu = useCallback(
|
||||||
label: item.title,
|
(item: HeaderMenuType): any => ({
|
||||||
href: item.url,
|
label: item.title,
|
||||||
submenu: item.children && item.children.length > 0
|
href: item.url,
|
||||||
? item.children.map((child: HeaderMenuType) => adaptMenu(child))
|
submenu:
|
||||||
: undefined
|
item.children && item.children.length > 0
|
||||||
}), []);
|
? item.children.map((child: HeaderMenuType) => adaptMenu(child))
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMenu = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await headerMenuService.getHeaderMenu();
|
|
||||||
const mappedData = data.map(item => adaptMenu(item));
|
// Fetch Menu
|
||||||
setMenuItems(mappedData);
|
const menuPromise = headerMenuService.getHeaderMenu();
|
||||||
|
|
||||||
|
// Fetch Header Data (Logo, Topbar)
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
|
||||||
|
const headerPromise = fetch(`${apiUrl}/api/header`).then(res => res.json());
|
||||||
|
|
||||||
|
const [menuData, headerResult] = await Promise.all([menuPromise, headerPromise]);
|
||||||
|
|
||||||
|
// Process Menu
|
||||||
|
const mappedMenu = menuData.map((item) => adaptMenu(item));
|
||||||
|
setMenuItems(mappedMenu);
|
||||||
|
|
||||||
|
// Process Header Data
|
||||||
|
if (headerResult.success && headerResult.data) {
|
||||||
|
setHeaderData(headerResult.data);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching menu in Header:', error);
|
console.error("Error fetching header data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMenu();
|
fetchData();
|
||||||
}, [adaptMenu]);
|
}, [adaptMenu]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderTop />
|
<HeaderTop data={headerData?.top} />
|
||||||
<HeaderBottom
|
<HeaderBottom
|
||||||
onToggleOffcanvas={toggleOffcanvas}
|
onToggleOffcanvas={toggleOffcanvas}
|
||||||
|
onToggleMobileMenu={toggleMobileMenu}
|
||||||
onToggleSearch={toggleSearch}
|
onToggleSearch={toggleSearch}
|
||||||
menuItems={menuItems}
|
menuItems={menuItems}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
logo={headerData?.logo}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Offcanvas
|
<Offcanvas isOpen={isOffcanvasOpen} onClose={() => setIsOffcanvasOpen(false)} menuItems={menuItems} />
|
||||||
isOpen={isOffcanvasOpen}
|
|
||||||
onClose={() => setIsOffcanvasOpen(false)}
|
<MobileMenu isOpen={isMobileMenuOpen} onClose={() => setIsMobileMenuOpen(false)} menuItems={menuItems} />
|
||||||
menuItems={menuItems}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Search Popup */}
|
{/* Search Popup */}
|
||||||
<div className={`search-popup ${isSearchOpen ? 'active' : ''}`}>
|
<div className={`search-popup ${isSearchOpen ? "active" : ""}`}>
|
||||||
<div
|
<div className="search-popup__overlay search-toggler" onClick={() => setIsSearchOpen(false)}></div>
|
||||||
className="search-popup__overlay search-toggler"
|
|
||||||
onClick={() => setIsSearchOpen(false)}
|
|
||||||
></div>
|
|
||||||
<div className="search-popup__content">
|
<div className="search-popup__content">
|
||||||
<form role="search" method="get" className="search-popup__form" action="#">
|
<form role="search" method="get" className="search-popup__form" action="#">
|
||||||
<input type="text" id="search" name="search" placeholder="Search Here..." />
|
<input type="text" id="search" name="search" placeholder="Search Here..." />
|
||||||
<button type="submit" aria-label="search submit" className="search-btn">
|
<button type="submit" aria-label="search submit" className="search-btn">
|
||||||
<span><i className="fa-regular fa-magnifying-glass"></i></span>
|
<span>
|
||||||
|
<i className="fa-regular fa-magnifying-glass"></i>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
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 { headerMenuService } from "@/services/header-menu.service";
|
||||||
import { HeaderMenu as HeaderMenuType } from '@/types/header-menu';
|
import { HeaderMenu as HeaderMenuType } from "@/types/header-menu";
|
||||||
|
|
||||||
interface HeaderBottomProps {
|
interface HeaderBottomProps {
|
||||||
onToggleOffcanvas: () => void;
|
onToggleOffcanvas: () => void;
|
||||||
|
onToggleMobileMenu: () => void;
|
||||||
onToggleSearch: () => void;
|
onToggleSearch: () => void;
|
||||||
menuItems: any[];
|
menuItems: any[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
logo: { light: string; dark: string; alt: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderBottom: React.FC<HeaderBottomProps> = ({
|
const HeaderBottom: React.FC<HeaderBottomProps> = ({
|
||||||
onToggleOffcanvas,
|
onToggleOffcanvas,
|
||||||
|
onToggleMobileMenu,
|
||||||
onToggleSearch,
|
onToggleSearch,
|
||||||
menuItems,
|
menuItems,
|
||||||
isLoading
|
isLoading,
|
||||||
|
logo,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Helper function to resolve logo URL
|
||||||
|
const getLogoUrl = (path: string | undefined) => {
|
||||||
|
if (!path) return "/assets/img/logo/black-logo.svg";
|
||||||
|
if (path.startsWith("http")) return path;
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
|
||||||
|
return `${apiUrl}${path}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoSrc = getLogoUrl(logo?.light);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header id="header-sticky" className="header-1">
|
<header id="header-sticky" className="header-1">
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
@@ -27,7 +41,11 @@ const HeaderBottom: React.FC<HeaderBottomProps> = ({
|
|||||||
<div className="header-left">
|
<div className="header-left">
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<Link href="/" className="header-logo-2">
|
<Link href="/" className="header-logo-2">
|
||||||
<img src="/assets/img/logo/black-logo.svg" alt="logo-img" />
|
<img
|
||||||
|
src={logoSrc}
|
||||||
|
alt={logo?.alt || "logo-img"}
|
||||||
|
style={{ maxHeight: "4rem" }}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mean__menu-wrapper">
|
<div className="mean__menu-wrapper">
|
||||||
@@ -36,21 +54,44 @@ const HeaderBottom: React.FC<HeaderBottomProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="header-right d-flex align-items-center mt-0">
|
<div className="header-right d-flex align-items-center mt-0">
|
||||||
<div className="header-call-item">
|
<div className="header-call-item">
|
||||||
|
{/* Mobile Search Icon - visible on mobile only */}
|
||||||
<button
|
<button
|
||||||
onClick={onToggleSearch}
|
onClick={onToggleSearch}
|
||||||
className="main-header__search search-toggler"
|
className="main-header__search search-toggler mobile-search-icon"
|
||||||
>
|
>
|
||||||
<i className="fa-regular fa-magnifying-glass"></i>
|
<i className="fa-regular fa-magnifying-glass"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Desktop Search Icon - hidden on mobile */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleSearch}
|
||||||
|
className="main-header__search search-toggler desktop-search-icon"
|
||||||
|
>
|
||||||
|
<i className="fa-regular fa-magnifying-glass"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Link href="/contact" className="theme-btn">
|
<Link href="/contact" className="theme-btn">
|
||||||
Apply now
|
Apply now
|
||||||
<i className="fa-solid fa-arrow-right"></i>
|
<i className="fa-solid fa-arrow-right"></i>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="header__hamburger my-auto">
|
|
||||||
|
{/* Mobile Hamburger - visible on mobile only */}
|
||||||
|
<div className="header__hamburger my-auto mobile-hamburger">
|
||||||
|
<div
|
||||||
|
className="sidebar__toggle"
|
||||||
|
onClick={onToggleMobileMenu}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-bars"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Hamburger - hidden on mobile */}
|
||||||
|
<div className="header__hamburger my-auto desktop-hamburger">
|
||||||
<div
|
<div
|
||||||
className="sidebar__toggle"
|
className="sidebar__toggle"
|
||||||
onClick={onToggleOffcanvas}
|
onClick={onToggleOffcanvas}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-bars-staggered"></i>
|
<i className="fa-solid fa-bars-staggered"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,39 +18,15 @@ interface HeaderData {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderTop = () => {
|
const HeaderTop: React.FC<{ data: HeaderData['top'] }> = ({ data }) => {
|
||||||
const [data, setData] = useState<HeaderData>(headerData);
|
// Use passed data or fallback to local JSON if data is null (though parent should handle fetching)
|
||||||
const [loading, setLoading] = useState(true);
|
// If data is null (initial load), we can use headerData fallback or render nothing/skeleton
|
||||||
|
|
||||||
useEffect(() => {
|
const displayData = data || headerData.top;
|
||||||
const fetchHeaderData = async () => {
|
const { phone, email, location, socialLinks, languages } = displayData;
|
||||||
try {
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
|
|
||||||
const response = await fetch(`${apiUrl}/api/header`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data && result.data.top) {
|
|
||||||
setData({
|
|
||||||
top: result.data.top,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to fetch header data from API, using fallback:", error);
|
|
||||||
// Use fallback data (already set as initial state)
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchHeaderData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { phone, email, location, socialLinks, languages } = data.top;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header-top-section">
|
<div className="header-top-section">
|
||||||
|
|||||||
112
app/components/layout/Header/MobileMenu.tsx
Normal file
112
app/components/layout/Header/MobileMenu.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
submenu?: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MobileMenuProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
menuItems: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, menuItems }) => {
|
||||||
|
const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({});
|
||||||
|
|
||||||
|
// Disable body scroll when menu is open
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const toggleSubmenu = (path: string) => {
|
||||||
|
setExpandedItems((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[path]: !prev[path],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLinkClick = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursive component for rendering menu items with nested submenus
|
||||||
|
const renderMenuItem = (item: MenuItem, index: number, parentPath: string = "", level: number = 0) => {
|
||||||
|
const currentPath = parentPath ? `${parentPath}-${index}` : `${index}`;
|
||||||
|
const hasSubmenu = item.submenu && item.submenu.length > 0;
|
||||||
|
const isExpanded = expandedItems[currentPath];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={currentPath} className={`mobile-menu-item mobile-menu-level-${level}`}>
|
||||||
|
<div className="mobile-menu-item-wrapper">
|
||||||
|
{hasSubmenu ? (
|
||||||
|
<>
|
||||||
|
<span className="mobile-menu-link">{item.label}</span>
|
||||||
|
<button
|
||||||
|
className="mobile-menu-toggle"
|
||||||
|
onClick={() => toggleSubmenu(currentPath)}
|
||||||
|
aria-label={`Toggle ${item.label} submenu`}
|
||||||
|
>
|
||||||
|
<i className={`fas ${isExpanded ? "fa-minus" : "fa-plus"}`}></i>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link href={item.href} className="mobile-menu-link" onClick={handleLinkClick}>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submenu with accordion animation - supports nested submenus */}
|
||||||
|
{hasSubmenu && (
|
||||||
|
<ul
|
||||||
|
className={`mobile-submenu mobile-submenu-level-${level + 1} ${isExpanded ? "mobile-submenu-open" : ""}`}
|
||||||
|
>
|
||||||
|
{item.submenu!.map((subItem, subIndex) =>
|
||||||
|
renderMenuItem(subItem, subIndex, currentPath, level + 1),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile Menu Slide Panel */}
|
||||||
|
<div className={`mobile-slide-menu ${isOpen ? "mobile-menu-open" : ""}`}>
|
||||||
|
<div className="mobile-menu-header">
|
||||||
|
<div className="mobile-menu-logo">
|
||||||
|
<Link href="/" onClick={handleLinkClick}>
|
||||||
|
<img src="/assets/img/logo/black-logo.svg" alt="logo" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<button className="mobile-menu-close" onClick={onClose} aria-label="Close menu">
|
||||||
|
<i className="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="mobile-menu-nav">
|
||||||
|
<ul className="mobile-menu-list">{menuItems.map((item, index) => renderMenuItem(item, index))}</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overlay */}
|
||||||
|
<div className={`mobile-menu-overlay ${isOpen ? "mobile-overlay-open" : ""}`} onClick={onClose}></div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileMenu;
|
||||||
271
app/components/layout/Header/header-responsive.css
Normal file
271
app/components/layout/Header/header-responsive.css
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
/* ========================================
|
||||||
|
HEADER RESPONSIVE FIXES
|
||||||
|
Additional responsive adjustments for header
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
MOBILE & TABLET (max-width: 991px)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
/* Ensure header is always visible and sticky */
|
||||||
|
#header-sticky {
|
||||||
|
position: sticky !important;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add shadow when scrolled */
|
||||||
|
#header-sticky.sticky {
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header container adjustments */
|
||||||
|
.header-1 .container-fluid {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header main layout */
|
||||||
|
.header-1 .header-main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo section */
|
||||||
|
.header-1 .header-left {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-left .logo {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-left .logo img {
|
||||||
|
max-height: 40px;
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right section with icons */
|
||||||
|
.header-1 .header-right {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .header-call-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search icon mobile styling */
|
||||||
|
.header-1 .header-right .main-header__search {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid var(--theme);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .main-header__search i {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--theme);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .main-header__search:hover {
|
||||||
|
background-color: var(--theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .main-header__search:hover i {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger icon mobile styling */
|
||||||
|
.header-1 .header__hamburger {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header__hamburger .sidebar__toggle {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--header);
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header__hamburger .sidebar__toggle:hover {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure icons don't stick to edges */
|
||||||
|
.header-1 .header-right .header-call-item > * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
SMALL MOBILE (max-width: 480px)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header-1 .container-fluid {
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-main {
|
||||||
|
padding: 10px 0;
|
||||||
|
min-height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-left .logo img {
|
||||||
|
max-height: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .header-call-item {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .main-header__search {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header-right .main-header__search i {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .header__hamburger .sidebar__toggle {
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
DESKTOP (min-width: 992px)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
/* Ensure desktop layout is not affected */
|
||||||
|
.header-1 .header-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop menu visible */
|
||||||
|
.header-1 .mean__menu-wrapper {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop button visible */
|
||||||
|
/* .header-1 .header-right .theme-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Ensure proper desktop search icon */
|
||||||
|
.header-1 .header-right .main-header__search {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
PREVENT LAYOUT SHIFT ON LOAD
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.header-1 {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1.sticky {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
Z-INDEX MANAGEMENT
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.header-1 {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-slide-menu {
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
z-index: 99998;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas__info {
|
||||||
|
z-index: 9999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas__overlay {
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
ACCESSIBILITY IMPROVEMENTS
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
/* Focus states for mobile */
|
||||||
|
.mobile-search-icon:focus,
|
||||||
|
.mobile-hamburger .sidebar__toggle:focus {
|
||||||
|
outline: 2px solid var(--theme);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch target sizes */
|
||||||
|
.mobile-search-icon,
|
||||||
|
.mobile-hamburger .sidebar__toggle {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
HIDE OFFCANVAS ON MOBILE (Prevent Menu Overlap)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
/* Hide the old offcanvas menu on mobile - only use new mobile menu */
|
||||||
|
.offcanvas__info,
|
||||||
|
.offcanvas__overlay {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
/* Hide mobile menu on desktop - only use offcanvas */
|
||||||
|
.mobile-slide-menu,
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
495
app/components/layout/Header/mobile-menu.css
Normal file
495
app/components/layout/Header/mobile-menu.css
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
/* ========================================
|
||||||
|
MOBILE HEADER & MENU STYLES
|
||||||
|
Only applies to mobile & tablet (max-width: 991px)
|
||||||
|
Desktop layout remains unchanged
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
1. MOBILE HEADER ADJUSTMENTS
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
/* Make header sticky on mobile */
|
||||||
|
.header-1 {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
background-color: var(--white);
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide header top section on mobile */
|
||||||
|
.header-top-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile header adjustments */
|
||||||
|
.header-1 .header-main {
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo adjustments for mobile */
|
||||||
|
.header-1 .header-left .logo img {
|
||||||
|
max-height: 40px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide desktop menu on mobile */
|
||||||
|
.header-1 .mean__menu-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile header right section */
|
||||||
|
.header-1 .header-right .header-call-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide "Apply now" button on mobile */
|
||||||
|
/* .header-1 .header-right .theme-btn {
|
||||||
|
display: none;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Mobile search icon styling */
|
||||||
|
.mobile-search-icon {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid var(--theme);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--theme);
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-icon:hover {
|
||||||
|
background-color: var(--theme);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide desktop search icon on mobile */
|
||||||
|
.desktop-search-icon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile hamburger icon styling */
|
||||||
|
.mobile-hamburger {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hamburger .sidebar__toggle {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--header);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide desktop hamburger on mobile */
|
||||||
|
.desktop-hamburger {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper spacing */
|
||||||
|
.header-1 .container-fluid {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop: Show desktop icons, hide mobile icons */
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.mobile-search-icon,
|
||||||
|
.mobile-hamburger {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-search-icon,
|
||||||
|
.desktop-hamburger {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
2. MOBILE SLIDE MENU
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Mobile menu container */
|
||||||
|
.mobile-slide-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--white);
|
||||||
|
z-index: 99999;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-slide-menu.mobile-menu-open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile menu header */
|
||||||
|
.mobile-menu-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-logo img {
|
||||||
|
max-height: 35px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close button */
|
||||||
|
.mobile-menu-close {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--theme);
|
||||||
|
border: none;
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-close:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
3. MOBILE MENU NAVIGATION
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.mobile-menu-nav {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-item {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-item-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-link {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--header);
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-link:hover {
|
||||||
|
color: var(--theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plus/Minus toggle button */
|
||||||
|
.mobile-menu-toggle {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--theme);
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-toggle:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-toggle i {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
4. MOBILE SUBMENU (ACCORDION)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.mobile-submenu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity 0.3s ease,
|
||||||
|
padding 0.3s ease;
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu.mobile-submenu-open {
|
||||||
|
max-height: 1000px;
|
||||||
|
opacity: 1;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-link {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 20px 12px 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--header);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-link::before {
|
||||||
|
content: "→";
|
||||||
|
position: absolute;
|
||||||
|
left: 25px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-link:hover {
|
||||||
|
color: var(--theme);
|
||||||
|
padding-left: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-link:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
5. MOBILE MENU OVERLAY
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 99998;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
visibility 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-overlay.mobile-overlay-open {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
6. RESPONSIVE ADJUSTMENTS
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Tablet adjustments */
|
||||||
|
@media (max-width: 991px) and (min-width: 768px) {
|
||||||
|
.mobile-slide-menu {
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-link {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small mobile devices */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.mobile-slide-menu {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-1 .container-fluid {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-close {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
7. PREVENT BODY SCROLL WHEN MENU OPEN
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
body.mobile-menu-active {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
8. SMOOTH ANIMATIONS
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mobile-slide-menu,
|
||||||
|
.mobile-submenu,
|
||||||
|
.mobile-menu-overlay,
|
||||||
|
.mobile-menu-toggle i {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
9. DESKTOP - ENSURE NO MOBILE STYLES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.mobile-slide-menu,
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
NESTED SUBMENU SUPPORT (Multi-level)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Increase max-height for nested submenus */
|
||||||
|
.mobile-submenu.mobile-submenu-open {
|
||||||
|
max-height: 2000px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Level 1 submenu (first level) */
|
||||||
|
.mobile-submenu-level-1 {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-level-1 .mobile-menu-item-wrapper {
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-level-1 .mobile-menu-link {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Level 2 submenu (nested inside level 1) */
|
||||||
|
.mobile-submenu-level-2 {
|
||||||
|
background-color: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-level-2 .mobile-menu-item-wrapper {
|
||||||
|
padding-left: 60px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-level-2 .mobile-menu-link {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Level 3 submenu (nested inside level 2) */
|
||||||
|
.mobile-submenu-level-3 {
|
||||||
|
background-color: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-level-3 .mobile-menu-item-wrapper {
|
||||||
|
padding-left: 80px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-submenu-level-3 .mobile-menu-link {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust toggle button size for nested items */
|
||||||
|
.mobile-submenu .mobile-menu-toggle {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border for nested items */
|
||||||
|
.mobile-menu-level-1 {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-level-2,
|
||||||
|
.mobile-menu-level-3 {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust padding for nested menu items */
|
||||||
|
.mobile-menu-level-1 .mobile-menu-item-wrapper,
|
||||||
|
.mobile-menu-level-2 .mobile-menu-item-wrapper,
|
||||||
|
.mobile-menu-level-3 .mobile-menu-item-wrapper {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
PREVENT MENU OVERLAP - HIDE OLD OFFCANVAS ON MOBILE
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
/* Completely hide offcanvas menu on mobile */
|
||||||
|
.offcanvas__info,
|
||||||
|
.offcanvas__overlay,
|
||||||
|
.fix-area {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
z-index: -1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
/* Completely hide mobile menu on desktop */
|
||||||
|
.mobile-slide-menu,
|
||||||
|
.mobile-menu-overlay {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
z-index: -1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,8 +1,15 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Mobile Menu Styles */
|
||||||
|
@import "./components/layout/Header/mobile-menu.css";
|
||||||
|
|
||||||
.collapse {
|
.collapse {
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse.show {
|
.collapse.show {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Header Responsive Styles */
|
||||||
|
@import "./components/layout/Header/header-responsive.css";
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import MouseCursor from "./components/MouseCursor";
|
|||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Visaway – Immigration & Visa Consulting HTML Template",
|
title: "H.A.I Learning",
|
||||||
description: "Visaway – Immigration & Visa Consulting HTML Template",
|
description: "H.A.I Learning",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
55
app/not-found.tsx
Normal file
55
app/not-found.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
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">Page Not Found</h1>
|
||||||
|
<ul className="breadcrumb-list">
|
||||||
|
<li>
|
||||||
|
<Link href="/">Home</Link>
|
||||||
|
</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">
|
||||||
|
<div className="error-content">
|
||||||
|
<h2 className="mb-3">404 - Page Not Found</h2>
|
||||||
|
<p className="mb-4">
|
||||||
|
The page you're looking for could not be found. It may have been moved or deleted. Please check the URL or return to the home page.
|
||||||
|
</p>
|
||||||
|
<div className="button-group">
|
||||||
|
<Link href="/" className="theme-btn">
|
||||||
|
Back to Home
|
||||||
|
<i className="fa-solid fa-arrow-right"></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ import localHomeData from './home.json';
|
|||||||
import { getCmsImageUrl } from '@/utils/image';
|
import { getCmsImageUrl } from '@/utils/image';
|
||||||
import { fetchHomeData } from '@/api';
|
import { fetchHomeData } from '@/api';
|
||||||
|
|
||||||
|
// Force dynamic rendering - không cache
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
// Fetch home data (blog aggregation is now handled by the backend)
|
// Fetch home data (blog aggregation is now handled by the backend)
|
||||||
const apiHomeData = await fetchHomeData();
|
const apiHomeData = await fetchHomeData();
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import Breadcrumb from "../components/Breadcrumb";
|
|||||||
import ImageWithFallback from "../components/ImageWithFallback";
|
import ImageWithFallback from "../components/ImageWithFallback";
|
||||||
import "./services.css";
|
import "./services.css";
|
||||||
|
|
||||||
|
// Force dynamic rendering - không cache
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function ServicesPage() {
|
export default async function ServicesPage() {
|
||||||
const data = await fetchServicePageData();
|
const data = await fetchServicePageData();
|
||||||
const allCountries = await fetchCountries();
|
const allCountries = await fetchCountries();
|
||||||
|
|||||||
@@ -7,24 +7,33 @@ interface VisaDetailProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function VisaDetail({ country }: VisaDetailProps) {
|
export default function VisaDetail({ country }: VisaDetailProps) {
|
||||||
|
// 1. Kiểm tra an toàn (Null Check)
|
||||||
|
// Vì detailedView có kiểu là 'DetailedView | undefined', ta phải chắc chắn nó tồn tại
|
||||||
|
if (!country || !country.detailedView) {
|
||||||
|
return (
|
||||||
|
<div className="section-padding text-center">
|
||||||
|
<h3>Đang tải dữ liệu...</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { name: rootName, detailedView } = country;
|
const { name: rootName, detailedView } = country;
|
||||||
|
|
||||||
|
// 2. Bóc tách dữ liệu sau khi đã kiểm tra detailedView tồn tại
|
||||||
const {
|
const {
|
||||||
activeCountry: countryData,
|
activeCountry: countryData,
|
||||||
relatedCountries,
|
relatedCountries,
|
||||||
contactInfo,
|
contactInfo,
|
||||||
} = country.detailedView;
|
} = detailedView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Breadcrumb-Wrapper Section Start */}
|
|
||||||
<Breadcrumb title={rootName} current={rootName} />
|
<Breadcrumb title={rootName} current={rootName} />
|
||||||
|
|
||||||
{/* Country-details Section Start */}
|
|
||||||
<section className="country-details-section section-padding fix">
|
<section className="country-details-section section-padding fix">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="country-details-wrapper">
|
<div className="country-details-wrapper">
|
||||||
<div className="row g-4">
|
<div className="row g-4">
|
||||||
{/* Main Content */}
|
|
||||||
<div className="col-lg-8">
|
<div className="col-lg-8">
|
||||||
<div className="country-details-post">
|
<div className="country-details-post">
|
||||||
<div className="details-image">
|
<div className="details-image">
|
||||||
@@ -38,33 +47,19 @@ export default function VisaDetail({ country }: VisaDetailProps) {
|
|||||||
|
|
||||||
{/* Visa Types */}
|
{/* Visa Types */}
|
||||||
<div className="tourist-visa-box">
|
<div className="tourist-visa-box">
|
||||||
{/* Render mảng đầu tiên (index 0) */}
|
{countryData.visaTypes?.map((type: any, idx: number) => (
|
||||||
{countryData.visaTypes?.[0] && (
|
<div
|
||||||
<div className="tourist-box style-2">
|
key={idx}
|
||||||
{countryData.visaTypes[0].items.map(
|
className={`tourist-box ${idx === 0 ? "style-2" : ""}`}
|
||||||
(item: any, itemIdx: number) => (
|
>
|
||||||
<div key={itemIdx} className="tourist-content">
|
{type.items.map((item: any, itemIdx: number) => (
|
||||||
<h5>{item.title}</h5>
|
<div key={itemIdx} className="tourist-content">
|
||||||
<p>{item.description}</p>
|
<h5>{item.title}</h5>
|
||||||
</div>
|
<p>{item.description}</p>
|
||||||
),
|
</div>
|
||||||
)}
|
))}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Visa Process */}
|
{/* Visa Process */}
|
||||||
@@ -102,49 +97,6 @@ export default function VisaDetail({ country }: VisaDetailProps) {
|
|||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +104,7 @@ export default function VisaDetail({ country }: VisaDetailProps) {
|
|||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="col-lg-4">
|
<div className="col-lg-4">
|
||||||
<div className="country-details-sideber">
|
<div className="country-details-sideber">
|
||||||
{relatedCountries.map((relCountry: any, idx: number) => (
|
{relatedCountries?.map((relCountry: any, idx: number) => (
|
||||||
<div key={idx} className="icon-box-item">
|
<div key={idx} className="icon-box-item">
|
||||||
<div className="left-item">
|
<div className="left-item">
|
||||||
<div className="icon">
|
<div className="icon">
|
||||||
@@ -178,8 +130,6 @@ export default function VisaDetail({ country }: VisaDetailProps) {
|
|||||||
<div className="content">
|
<div className="content">
|
||||||
<h3>{contactInfo.sectionTitle}</h3>
|
<h3>{contactInfo.sectionTitle}</h3>
|
||||||
<p>{contactInfo.helpText}</p>
|
<p>{contactInfo.helpText}</p>
|
||||||
|
|
||||||
{/* Phone */}
|
|
||||||
<div className="icon-item">
|
<div className="icon-item">
|
||||||
<div className="icon">
|
<div className="icon">
|
||||||
<i className="fa-solid fa-phone"></i>
|
<i className="fa-solid fa-phone"></i>
|
||||||
@@ -187,13 +137,15 @@ export default function VisaDetail({ country }: VisaDetailProps) {
|
|||||||
<div className="cont">
|
<div className="cont">
|
||||||
<span>{contactInfo.phone.label}: </span>
|
<span>{contactInfo.phone.label}: </span>
|
||||||
<h6>
|
<h6>
|
||||||
<a href={contactInfo.phone.link}>
|
<a
|
||||||
|
href={contactInfo.phone.link}
|
||||||
|
style={{ color: "white" }}
|
||||||
|
>
|
||||||
{contactInfo.phone.value}
|
{contactInfo.phone.value}
|
||||||
</a>
|
</a>
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<div className="icon-item">
|
<div className="icon-item">
|
||||||
<div className="icon">
|
<div className="icon">
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import Breadcrumb from "../components/Breadcrumb";
|
import Breadcrumb from "../components/Breadcrumb";
|
||||||
import { fetchVisaData, type VisaCountry } from "@/api/visa";
|
import { fetchVisaData, type VisaCountry } from "@/api/visa";
|
||||||
|
|
||||||
|
// Force dynamic rendering - không cache
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function VisaListPage() {
|
export default async function VisaListPage() {
|
||||||
// Fetch all visa countries từ API
|
// Fetch all visa countries từ API
|
||||||
let visaCountries: any[] = [];
|
let visaCountries: any[] = [];
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
export default function LevelSlugPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { level, slug } = router.query;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 20 }}>
|
|
||||||
<h1>Lesson page</h1>
|
|
||||||
<p>Level: {level}</p>
|
|
||||||
<p>Slug: {slug}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
export default function LevelPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { level } = router.query;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 20 }}>
|
|
||||||
<h1>Level page</h1>
|
|
||||||
<p>Level: {level}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { AppProps } from "next/app";
|
|
||||||
import "../app/globals.css";
|
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
|
||||||
return <Component {...pageProps} />;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Html, Head, Main, NextScript } from "next/document";
|
|
||||||
|
|
||||||
export default function Document() {
|
|
||||||
return (
|
|
||||||
<Html lang="en">
|
|
||||||
<Head />
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -45,8 +45,8 @@ Version: 1.0.0
|
|||||||
--body: #fff;
|
--body: #fff;
|
||||||
--black: #000;
|
--black: #000;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
--theme: #e13833;
|
--theme: #BF3432;
|
||||||
--theme-2: #0048b4;
|
--theme-2: #0a2347;
|
||||||
--header: #151a26;
|
--header: #151a26;
|
||||||
--text: #535761;
|
--text: #535761;
|
||||||
--text-2: #0b4e3d;
|
--text-2: #0b4e3d;
|
||||||
@@ -7264,43 +7264,67 @@ html.lenis body {
|
|||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(2):before {
|
.letters-loading:nth-child(2):before {
|
||||||
animation-delay: 0.2s;
|
animation-delay: 0.1s;
|
||||||
}
|
}
|
||||||
.preloader
|
.preloader
|
||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(3):before {
|
.letters-loading:nth-child(3):before {
|
||||||
animation-delay: 0.4s;
|
animation-delay: 0.2s;
|
||||||
}
|
}
|
||||||
.preloader
|
.preloader
|
||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(4):before {
|
.letters-loading:nth-child(4):before {
|
||||||
animation-delay: 0.6s;
|
animation-delay: 0.3s;
|
||||||
}
|
}
|
||||||
.preloader
|
.preloader
|
||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(5):before {
|
.letters-loading:nth-child(5):before {
|
||||||
animation-delay: 0.8s;
|
animation-delay: 0.4s;
|
||||||
}
|
}
|
||||||
.preloader
|
.preloader
|
||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(6):before {
|
.letters-loading:nth-child(6):before {
|
||||||
animation-delay: 1s;
|
animation-delay: 0.5s;
|
||||||
}
|
}
|
||||||
.preloader
|
.preloader
|
||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(7):before {
|
.letters-loading:nth-child(7):before {
|
||||||
animation-delay: 1.2s;
|
animation-delay: 0.6s;
|
||||||
}
|
}
|
||||||
.preloader
|
.preloader
|
||||||
.animation-preloader
|
.animation-preloader
|
||||||
.txt-loading
|
.txt-loading
|
||||||
.letters-loading:nth-child(8):before {
|
.letters-loading:nth-child(8):before {
|
||||||
animation-delay: 1.4s;
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
|
.preloader
|
||||||
|
.animation-preloader
|
||||||
|
.txt-loading
|
||||||
|
.letters-loading:nth-child(8):before {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
.preloader
|
||||||
|
.animation-preloader
|
||||||
|
.txt-loading
|
||||||
|
.letters-loading:nth-child(9):before {
|
||||||
|
animation-delay: 0.9s;
|
||||||
|
}
|
||||||
|
.preloader
|
||||||
|
.animation-preloader
|
||||||
|
.txt-loading
|
||||||
|
.letters-loading:nth-child(10):before {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
.preloader
|
||||||
|
.animation-preloader
|
||||||
|
.txt-loading
|
||||||
|
.letters-loading:nth-child(11):before {
|
||||||
|
animation-delay: 1.1s;
|
||||||
}
|
}
|
||||||
.preloader .animation-preloader .txt-loading .letters-loading::before {
|
.preloader .animation-preloader .txt-loading .letters-loading::before {
|
||||||
animation: letters-loading 4s infinite;
|
animation: letters-loading 4s infinite;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 135 KiB |
BIN
public/assets/img/logo/Logo_HaiLearning.jpg
Normal file
BIN
public/assets/img/logo/Logo_HaiLearning.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Reference in New Issue
Block a user