diff --git a/api/footerApi.ts b/api/footerApi.ts new file mode 100644 index 0000000..83a393b --- /dev/null +++ b/api/footerApi.ts @@ -0,0 +1,62 @@ +/** + * Footer API Functions + * Fetch footer data from external API + */ + +const getApiUrl = (): string => { + return process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; +}; + +export interface FooterData { + top: { + bgImage: string; + phone: { + display: string; + href: string; + }; + address: string; + logo: { + src: string; + alt: string; + href: string; + }; + menuLinks: Array<{ + label: string; + href: string; + }>; + socialLinks: Array<{ + icon: string; + href: string; + }>; + }; + bottom: { + copyright: { + text: string; + brand: string; + rights: string; + }; + menuLinks: Array<{ + label: string; + href: string; + }>; + }; +} + +export const footerApi = { + // Get footer data + getFooter: async (): Promise => { + try { + const apiUrl = getApiUrl(); + const response = await fetch(`${apiUrl}/api/footer`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Error fetching footer data:", error); + // Fallback to static data if API fails + const fallbackData = await import("../app/components/layout/Footer/footer.json"); + return fallbackData.default as FooterData; + } + }, +}; diff --git a/api/index.ts b/api/index.ts index 34cd0fc..9a7b242 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,5 +1,6 @@ /** * Export all API functions */ -export * from './blogsApi'; -export * from './homeApi'; +export * from "./blogsApi"; +export * from "./footerApi"; +export * from "./servicesApi"; diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 4216b3f..31ab061 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -20,65 +20,67 @@ export async function generateStaticParams() { } interface BlogDetailsPageProps { - params: Promise<{ - slug: string; - }> | { - slug: string; - }; + params: + | Promise<{ + slug: string; + }> + | { + slug: string; + }; } // SEO metadata cho từng bài blog (Open Graph / thumbnail khi share) export async function generateMetadata({ - params, + params, }: { - params: - | Promise<{ - slug: string; - }> - | { - slug: string; - }; + params: + | Promise<{ + slug: string; + }> + | { + slug: string; + }; }): Promise { - const resolvedParams = params instanceof Promise ? await params : params; - const slug = resolvedParams.slug; + const resolvedParams = params instanceof Promise ? await params : params; + const slug = resolvedParams.slug; - try { - const blogResponse = await fetchBlogDetail(slug); - const post = blogResponse.data; + try { + const blogResponse = await fetchBlogDetail(slug); + const post = blogResponse.data; - const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"; - const url = `${siteUrl}/blog/${post.slug}`; - const imageUrl = post.featuredImage - ? getCmsImageUrl(post.featuredImage) - : `${siteUrl}/assets/img/inner-page/news-details/details-1.jpg`; + const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"; + const url = `${siteUrl}/blog/${post.slug}`; + const imageUrl = post.featuredImage + ? getCmsImageUrl(post.featuredImage) + : `${siteUrl}/assets/img/inner-page/news-details/details-1.jpg`; - return { - title: post.title, - description: post.excerpt, - openGraph: { - title: post.title, - description: post.excerpt, - url, - type: "article", - images: [ - { - url: imageUrl, - alt: post.title, - }, - ], - }, - twitter: { - card: "summary_large_image", - title: post.title, - description: post.excerpt, - images: [imageUrl], - }, - }; - } catch { - return { - title: "Blog Details", - }; - } + return { + title: post.title, + description: post.excerpt, + openGraph: { + title: post.title, + description: post.excerpt, + url, + type: "article", + images: [ + { + url: imageUrl, + alt: post.title, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: post.title, + description: post.excerpt, + images: [imageUrl], + }, + }; + } catch { + return { + title: "Blog Details", + }; + } } export default async function BlogDetailsPage({ params }: BlogDetailsPageProps) { @@ -100,18 +102,18 @@ export default async function BlogDetailsPage({ params }: BlogDetailsPageProps)
-

Post not found

-

The blog post you are looking for does not exist.

- - Back to Blog - +

Post not found

+

The blog post you are looking for does not exist.

+ + Back to Blog +
{/* Sidebar on the right */} +
- ); diff --git a/app/blog/category/[slug]/page.tsx b/app/blog/category/[slug]/page.tsx index 4eebb6e..b2532c0 100644 --- a/app/blog/category/[slug]/page.tsx +++ b/app/blog/category/[slug]/page.tsx @@ -4,22 +4,21 @@ import Sidebar from "@/app/blog/components/Sidebar"; import { fetchBlogsByCategory, fetchCategoryDetail } from "@/api/blogsApi"; interface CategoryPageProps { - params: - | Promise<{ - slug: string; - }> - | { - slug: string; - }; - searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string }; + params: + | Promise<{ + slug: string; + }> + | { + slug: string; + }; + searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string }; } export default async function CategoryPage({ params, searchParams }: CategoryPageProps) { // Handle both Promise and direct object const resolvedParams = params instanceof Promise ? await params : params; const slug = resolvedParams.slug; - const resolvedSearchParams = - searchParams instanceof Promise ? await searchParams : searchParams; + const resolvedSearchParams = searchParams instanceof Promise ? await searchParams : searchParams; const searchQuery = resolvedSearchParams?.search?.toString() || ""; const pageParam = resolvedSearchParams?.page?.toString() || "1"; const currentPage = Number.parseInt(pageParam, 10) || 1; @@ -30,9 +29,9 @@ export default async function CategoryPage({ params, searchParams }: CategoryPag [categoryResponse, blogsResponse] = await Promise.all([ fetchCategoryDetail(slug), fetchBlogsByCategory(slug, { - page: currentPage, - limit: 3, - ...(searchQuery ? { search: searchQuery } : {}), + page: currentPage, + limit: 3, + ...(searchQuery ? { search: searchQuery } : {}), }), ]); } catch { @@ -64,12 +63,7 @@ export default async function CategoryPage({ params, searchParams }: CategoryPag return ( <> - + ); } diff --git a/app/blog/page.tsx b/app/blog/page.tsx index a5218f1..1c9e5af 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -3,31 +3,27 @@ import NewsSection from "./components/NewsSection"; import { fetchBlogList } from "@/api/blogsApi"; interface NewsPageProps { - searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string }; + searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string }; } export default async function NewsPage({ searchParams }: NewsPageProps) { - const resolvedSearchParams = - searchParams instanceof Promise ? await searchParams : searchParams; - const searchQuery = resolvedSearchParams?.search?.toString() || ""; - const pageParam = resolvedSearchParams?.page?.toString() || "1"; - const currentPage = Number.parseInt(pageParam, 10) || 1; + const resolvedSearchParams = searchParams instanceof Promise ? await searchParams : searchParams; + const searchQuery = resolvedSearchParams?.search?.toString() || ""; + const pageParam = resolvedSearchParams?.page?.toString() || "1"; + const currentPage = Number.parseInt(pageParam, 10) || 1; - // Fetch blog list from API - const blogResponse = await fetchBlogList({ - page: currentPage, - limit: 3, - ...(searchQuery ? { search: searchQuery } : {}), - }); - const { blogs, pagination } = blogResponse.data; + // Fetch blog list from API + const blogResponse = await fetchBlogList({ + page: currentPage, + limit: 3, + ...(searchQuery ? { search: searchQuery } : {}), + }); + const { blogs, pagination } = blogResponse.data; - return ( - <> - - - - ); -} \ No newline at end of file + return ( + <> + + + + ); +} diff --git a/app/blog/tag/[slug]/page.tsx b/app/blog/tag/[slug]/page.tsx index 58c0386..5090bcc 100644 --- a/app/blog/tag/[slug]/page.tsx +++ b/app/blog/tag/[slug]/page.tsx @@ -4,22 +4,21 @@ import Sidebar from "@/app/blog/components/Sidebar"; import { fetchBlogsByTag, fetchTagDetail } from "@/api/blogsApi"; interface TagPageProps { - params: - | Promise<{ - slug: string; - }> - | { - slug: string; - }; - searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string }; + params: + | Promise<{ + slug: string; + }> + | { + slug: string; + }; + searchParams?: Promise<{ search?: string; page?: string }> | { search?: string; page?: string }; } export default async function TagPage({ params, searchParams }: TagPageProps) { // Handle both Promise and direct object const resolvedParams = params instanceof Promise ? await params : params; const slug = resolvedParams.slug; - const resolvedSearchParams = - searchParams instanceof Promise ? await searchParams : searchParams; + const resolvedSearchParams = searchParams instanceof Promise ? await searchParams : searchParams; const searchQuery = resolvedSearchParams?.search?.toString() || ""; const pageParam = resolvedSearchParams?.page?.toString() || "1"; const currentPage = Number.parseInt(pageParam, 10) || 1; @@ -30,9 +29,9 @@ export default async function TagPage({ params, searchParams }: TagPageProps) { [tagResponse, blogsResponse] = await Promise.all([ fetchTagDetail(slug), fetchBlogsByTag(slug, { - page: currentPage, - limit: 3, - ...(searchQuery ? { search: searchQuery } : {}), + page: currentPage, + limit: 3, + ...(searchQuery ? { search: searchQuery } : {}), }), ]); } catch { diff --git a/app/components/layout/Footer/FooterBottom.tsx b/app/components/layout/Footer/FooterBottom.tsx index b708b2c..9dcd7a1 100644 --- a/app/components/layout/Footer/FooterBottom.tsx +++ b/app/components/layout/Footer/FooterBottom.tsx @@ -1,8 +1,28 @@ -import Link from 'next/link'; -import footerData from './footer.json'; +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { footerApi, FooterData } from "../../../../api/footerApi"; +import footerData from "./footer.json"; const FooterBottom = () => { - const { bottom } = footerData; + const [data, setData] = useState(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(); + }, []); + + const { bottom } = data; return (
diff --git a/app/components/layout/Footer/FooterTop.tsx b/app/components/layout/Footer/FooterTop.tsx index 52c388b..89263fd 100644 --- a/app/components/layout/Footer/FooterTop.tsx +++ b/app/components/layout/Footer/FooterTop.tsx @@ -1,8 +1,28 @@ -import Link from 'next/link'; -import footerData from './footer.json'; +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { footerApi, FooterData } from "../../../../api/footerApi"; +import footerData from "./footer.json"; const FooterTop = () => { - const { top } = footerData; + const [data, setData] = useState(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(); + }, []); + + const { top } = data; return (