Commit 85518ad1 authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

fix mockdata

parent be3a7d91
...@@ -7,9 +7,9 @@ import { Swiper as SwiperType } from "swiper/types"; ...@@ -7,9 +7,9 @@ import { Swiper as SwiperType } from "swiper/types";
import { useRef } from "react"; import { useRef } from "react";
import "swiper/css"; import "swiper/css";
import { useGetBanner } from "@/api/endpoints/banner"; import { getBanner } from "@/api/endpoints/banner";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { fetchCmsFileById, resolveCmsFileUrl } from "@/lib/api/files"; import { resolveCmsFileUrl } from "@/lib/api/files";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
type ApiEnvelope<T> = { type ApiEnvelope<T> = {
...@@ -22,20 +22,65 @@ type ApiEnvelope<T> = { ...@@ -22,20 +22,65 @@ type ApiEnvelope<T> = {
const getEnvelopeData = <T,>(payload?: ApiEnvelope<T>) => const getEnvelopeData = <T,>(payload?: ApiEnvelope<T>) =>
payload?.responseData ?? payload?.data?.responseData; payload?.responseData ?? payload?.data?.responseData;
function BannerSlideItem({ fileId, alt }: { fileId: string; alt: string }) { type BannerRow = {
id: string;
file_id?: string | null;
banner_name?: string | null;
image_url?: string | null;
display_order?: number | null;
};
/**
* Mock banner khi BE /api/banner lỗi hoặc không trả data.
* Dùng ảnh `thumbnail.png` đã có sẵn trong public/.
*/
const MOCK_BANNER_ROWS: BannerRow[] = [
{
id: "mock-banner-1",
banner_name: "VCCI-HCM kết nối doanh nghiệp Việt Nam – Hoa Kỳ 2026",
image_url: "/thumbnail.png",
display_order: 1,
},
{
id: "mock-banner-2",
banner_name: "Hỗ trợ doanh nghiệp SME tiếp cận vốn tín dụng",
image_url: "/thumbnail.png",
display_order: 2,
},
{
id: "mock-banner-3",
banner_name: "AI trong quản trị hiệp hội doanh nghiệp",
image_url: "/thumbnail.png",
display_order: 3,
},
];
function BannerSlideItem({
src,
alt,
fileId,
}: {
src: string;
alt: string;
fileId?: string | null;
}) {
const { data: file, isPending } = useQuery({ const { data: file, isPending } = useQuery({
queryKey: ["file", fileId], queryKey: ["file", fileId],
queryFn: () => fetchCmsFileById(fileId), queryFn: () => Promise.resolve({ path: src }),
enabled: !!fileId, enabled: !!fileId && !src,
}); });
if (isPending) { if (fileId && isPending && !src) {
return ( return (
<Skeleton className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px]" /> <Skeleton className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px]" />
); );
} }
const url = file ? resolveCmsFileUrl(file.path) : "/img-error.png"; const url = src
? src
: file
? resolveCmsFileUrl(file.path)
: "/img-error.png";
return ( return (
<ImageNext <ImageNext
...@@ -52,14 +97,33 @@ function BannerSlideItem({ fileId, alt }: { fileId: string; alt: string }) { ...@@ -52,14 +97,33 @@ function BannerSlideItem({ fileId, alt }: { fileId: string; alt: string }) {
const Banner = () => { const Banner = () => {
const swiperRef = useRef<SwiperType | null>(null); const swiperRef = useRef<SwiperType | null>(null);
const { data: bannerData } = useGetBanner({ const { data: bannerData, isPending, isError } = useQuery({
queryKey: ["home-banner"],
queryFn: () =>
getBanner({
filters: "status@=ACTIVE", filters: "status@=ACTIVE",
sortField: "display_order", sortField: "display_order",
sortOrder: "asc", sortOrder: "asc",
}),
staleTime: 60 * 1000,
}); });
const pageData = getEnvelopeData<{ rows?: any[] }>(bannerData); const pageData = bannerData
const rows = pageData?.rows ?? []; ? getEnvelopeData<{ rows?: BannerRow[] }>(bannerData as unknown as ApiEnvelope<{ rows?: BannerRow[] }>)
: undefined;
// BE trả rows rỗng HOẶC bị lỗi → fallback mock banner để UI không trống.
const rows: BannerRow[] =
pageData?.rows && pageData.rows.length > 0
? pageData.rows
: MOCK_BANNER_ROWS;
if (isPending && !rows.length) {
return (
<div className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] bg-slate-100 flex items-center justify-center">
<Skeleton className="w-full h-full" />
</div>
);
}
if (!rows || rows.length === 0) { if (!rows || rows.length === 0) {
return ( return (
...@@ -78,25 +142,18 @@ const Banner = () => { ...@@ -78,25 +142,18 @@ const Banner = () => {
onSwiper={(s) => (swiperRef.current = s)} onSwiper={(s) => (swiperRef.current = s)}
className="w-full overflow-hidden" className="w-full overflow-hidden"
> >
{rows.map((row: any) => ( {rows.map((row) => {
const src = row.image_url ?? (row.file_id ? undefined : "/thumbnail.png");
return (
<SwiperSlide key={row.id}> <SwiperSlide key={row.id}>
{row.file_id ? (
<BannerSlideItem <BannerSlideItem
fileId={row.file_id} src={src ?? ""}
alt={row.banner_name || "Banner"} alt={row.banner_name || "Banner"}
fileId={row.file_id ?? null}
/> />
) : (
<ImageNext
src="/img-error.png"
alt={row.banner_name || "Banner"}
width={2560}
height={720}
sizes="100vw"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
)}
</SwiperSlide> </SwiperSlide>
))} );
})}
</Swiper> </Swiper>
); );
}; };
......
...@@ -28,6 +28,11 @@ type FeaturedMembersResponse = { ...@@ -28,6 +28,11 @@ type FeaturedMembersResponse = {
}; };
}; };
type FeaturedMemberState =
| { status: "loading" }
| { status: "empty" }
| { status: "ready"; rows: FeaturedMember[] };
const resolveMemberImage = (avatar: string | null | undefined, index: number) => { const resolveMemberImage = (avatar: string | null | undefined, index: number) => {
if (avatar?.startsWith("http://") || avatar?.startsWith("https://")) { if (avatar?.startsWith("http://") || avatar?.startsWith("https://")) {
return avatar; return avatar;
...@@ -40,15 +45,11 @@ const resolveMemberImage = (avatar: string | null | undefined, index: number) => ...@@ -40,15 +45,11 @@ const resolveMemberImage = (avatar: string | null | undefined, index: number) =>
return memberImages[index % memberImages.length] ?? "/img-error.png"; return memberImages[index % memberImages.length] ?? "/img-error.png";
}; };
const fallbackMembers: FeaturedMember[] = Array.from({ length: 9 }, (_, index) => ({
id: `fallback-member-${index}`,
name: `Hội viên tiêu biểu ${index + 1}`,
avatar: memberImages[index % memberImages.length],
}));
function Members() { function Members() {
const { memberConnectionPosts, categoryLinks, categoryNames } = useHomePosts(); const { memberConnectionPosts, categoryLinks, categoryNames } = useHomePosts();
const [featuredMembers, setFeaturedMembers] = useState<FeaturedMember[]>([]); const [featuredMembers, setFeaturedMembers] = useState<FeaturedMemberState>({
status: "loading",
});
const featuredConnection = memberConnectionPosts[0]; const featuredConnection = memberConnectionPosts[0];
const sectionLink = const sectionLink =
categoryLinks.get(categoryNames.ketNoiHoiVien.toLowerCase()) ?? "/hoi-vien/ket-noi-hoi-vien"; categoryLinks.get(categoryNames.ketNoiHoiVien.toLowerCase()) ?? "/hoi-vien/ket-noi-hoi-vien";
...@@ -56,7 +57,8 @@ function Members() { ...@@ -56,7 +57,8 @@ function Members() {
featuredConnection?.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE; featuredConnection?.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE;
const connectionImageAlt = const connectionImageAlt =
featuredConnection?.thumbnail?.alt || featuredConnection?.title || "VCCI HCM"; featuredConnection?.thumbnail?.alt || featuredConnection?.title || "VCCI HCM";
const displayMembers = featuredMembers.length > 0 ? featuredMembers : fallbackMembers; const displayMembers =
featuredMembers.status === "ready" ? featuredMembers.rows.slice(0, 9) : [];
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
...@@ -75,10 +77,17 @@ function Members() { ...@@ -75,10 +77,17 @@ function Members() {
const rows = data.responseData?.rows ?? []; const rows = data.responseData?.rows ?? [];
if (isMounted) { if (isMounted) {
setFeaturedMembers(rows.slice(0, 9)); setFeaturedMembers(
rows.length > 0
? { status: "ready", rows }
: { status: "empty" },
);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (isMounted) {
setFeaturedMembers({ status: "empty" });
}
} }
}; };
...@@ -89,29 +98,24 @@ function Members() { ...@@ -89,29 +98,24 @@ function Members() {
}; };
}, []); }, []);
const renderMemberContent = () => {
if (featuredMembers.status === "loading") {
return ( return (
<section className="flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch"> <div className="rounded-[16px] bg-white/40 px-5 py-10 text-center text-sm text-[#1e2f5e]/70">
<aside className="flex-1 rounded-[22px] bg-[#f7b500] p-4 shadow-[0_18px_34px_rgba(247,181,0,0.18)] md:p-5"> Đang tải dữ liệu...
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="client-section-title uppercase text-[#20449a]">
Hội viên tiêu biểu
</h2>
<div className="mt-2.5 h-1 w-10 rounded-full bg-white" />
</div> </div>
);
}
<Link if (featuredMembers.status === "empty" || displayMembers.length === 0) {
href={FEATURED_MEMBER_MORE_URL} return (
target="_blank" <div className="rounded-[16px] bg-white/40 px-5 py-10 text-center text-sm text-[#1e2f5e]/70">
rel="noreferrer" Chưa có thông tin.
className="pt-1 text-sm font-semibold text-[#1e2f5e] transition-colors hover:text-[#20449a]"
>
Xem thêm
</Link>
</div> </div>
);
}
<div className="mt-4 border-t border-[#e7aa00] pt-5" /> return (
<Swiper <Swiper
modules={[Autoplay]} modules={[Autoplay]}
autoplay={{ delay: 4200, disableOnInteraction: false }} autoplay={{ delay: 4200, disableOnInteraction: false }}
...@@ -146,6 +150,33 @@ function Members() { ...@@ -146,6 +150,33 @@ function Members() {
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
);
};
return (
<section className="flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch">
<aside className="flex-1 rounded-[22px] bg-[#f7b500] p-4 shadow-[0_18px_34px_rgba(247,181,0,0.18)] md:p-5">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="client-section-title uppercase text-[#20449a]">
Hội viên tiêu biểu
</h2>
<div className="mt-2.5 h-1 w-10 rounded-full bg-white" />
</div>
<Link
href={FEATURED_MEMBER_MORE_URL}
target="_blank"
rel="noreferrer"
className="pt-1 text-sm font-semibold text-[#1e2f5e] transition-colors hover:text-[#20449a]"
>
Xem thêm
</Link>
</div>
<div className="mt-4 border-t border-[#e7aa00] pt-5" />
{renderMemberContent()}
</aside> </aside>
<aside className="w-full xl:w-[31%] xl:min-w-[320px]"> <aside className="w-full xl:w-[31%] xl:min-w-[320px]">
......
...@@ -39,12 +39,64 @@ const resolvePartnerImage = (avatar: string | null | undefined, index: number) = ...@@ -39,12 +39,64 @@ const resolvePartnerImage = (avatar: string | null | undefined, index: number) =
return partnerImages[index % partnerImages.length] ?? "/img-error.png"; return partnerImages[index % partnerImages.length] ?? "/img-error.png";
}; };
const fallbackPartners: Partner[] = Array.from({ length: 6 }, (_, index) => ({ const renderPartnerContent = (partners: Partner[]) => {
id: `fallback-partner-${index}`, if (partners.length > 0) {
name: `Đối tác ${index + 1}`, return (
avatar: partnerImages[index % partnerImages.length], <Swiper
website: null, modules={[Autoplay]}
})); autoplay={{ delay: 4200, disableOnInteraction: false }}
observer
observeParents
updateOnWindowResize
slidesPerView="auto"
spaceBetween={16}
className="w-full"
>
{partners.map((partner, index) => (
<SwiperSlide
key={partner.id}
className="!h-auto !w-full sm:!w-[calc(50%-8px)] xl:!w-[calc(33.333%-10.67px)]"
>
{partner.website ? (
<a
href={partner.website}
target="_blank"
rel="noreferrer"
className="block"
>
<div className="flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_24px_rgba(31,59,124,0.1)] xl:h-[151px]">
<ImageNext
src={resolvePartnerImage(partner.avatar, index)}
alt={partner.name}
width={140}
height={72}
className="max-h-full w-full object-contain"
/>
</div>
</a>
) : (
<div className="flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-[151px]">
<ImageNext
src={resolvePartnerImage(partner.avatar, index)}
alt={partner.name}
width={140}
height={72}
className="max-h-full w-full object-contain"
/>
</div>
)}
</SwiperSlide>
))}
</Swiper>
);
}
return (
<div className="rounded-[14px] border border-[#edf1f7] bg-white px-5 py-10 text-center text-sm text-gray-500">
Chưa có thông tin.
</div>
);
};
function VideoAndPartners() { function VideoAndPartners() {
const videosQuery = useQuery({ const videosQuery = useQuery({
...@@ -71,7 +123,9 @@ function VideoAndPartners() { ...@@ -71,7 +123,9 @@ function VideoAndPartners() {
const videos = videosQuery.data?.rows ?? []; const videos = videosQuery.data?.rows ?? [];
const partners = partnersQuery.data?.responseData?.rows?.slice(0, 12) ?? []; const partners = partnersQuery.data?.responseData?.rows?.slice(0, 12) ?? [];
const displayPartners = partners.length > 0 ? partners : fallbackPartners; const hasPartnerError = partnersQuery.isError;
const displayPartners =
hasPartnerError || partners.length === 0 ? [] : partners;
return ( return (
<section className="flex flex-col gap-6 pb-10 xl:flex-row xl:items-stretch"> <section className="flex flex-col gap-6 pb-10 xl:flex-row xl:items-stretch">
...@@ -150,52 +204,7 @@ function VideoAndPartners() { ...@@ -150,52 +204,7 @@ function VideoAndPartners() {
</div> </div>
</div> </div>
<Swiper {renderPartnerContent(displayPartners)}
modules={[Autoplay]}
autoplay={{ delay: 4200, disableOnInteraction: false }}
observer
observeParents
updateOnWindowResize
slidesPerView="auto"
spaceBetween={16}
className="w-full"
>
{displayPartners.map((partner, index) => (
<SwiperSlide
key={partner.id}
className="!h-auto !w-full sm:!w-[calc(50%-8px)] xl:!w-[calc(33.333%-10.67px)]"
>
{partner.website ? (
<a
href={partner.website}
target="_blank"
rel="noreferrer"
className="block"
>
<div className="flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_24px_rgba(31,59,124,0.1)] xl:h-[151px]">
<ImageNext
src={resolvePartnerImage(partner.avatar, index)}
alt={partner.name}
width={140}
height={72}
className="max-h-full w-full object-contain"
/>
</div>
</a>
) : (
<div className="flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-[151px]">
<ImageNext
src={resolvePartnerImage(partner.avatar, index)}
alt={partner.name}
width={140}
height={72}
className="max-h-full w-full object-contain"
/>
</div>
)}
</SwiperSlide>
))}
</Swiper>
</aside> </aside>
</section> </section>
); );
......
...@@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query"; ...@@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useCustomClient } from "@/api/mutator/custom-client"; import { useCustomClient } from "@/api/mutator/custom-client";
import Links, { resolveUploadUrl } from "@/links"; import Links, { resolveUploadUrl } from "@/links";
import { MOCK_HOME_POSTS } from "@lib/mock-home-posts";
type RawHomeCategory = { type RawHomeCategory = {
id?: string | null; id?: string | null;
...@@ -279,6 +280,17 @@ function createEventCalendarQuery(currentMonth: Date) { ...@@ -279,6 +280,17 @@ function createEventCalendarQuery(currentMonth: Date) {
} }
async function fetchHomePosts() { async function fetchHomePosts() {
// Trả về mock ngay khi CMS/BE gặp sự cố — website vẫn hiển thị được nội dung cơ bản.
try {
return await fetchHomePostsFromApi();
} catch (error) {
// eslint-disable-next-line no-console
console.warn("[useHomePosts] CMS unavailable, falling back to mock data", error);
return MOCK_HOME_POSTS;
}
}
async function fetchHomePostsFromApi() {
const featuredQuery = new URLSearchParams({ const featuredQuery = new URLSearchParams({
page: "1", page: "1",
pageSize: "3", pageSize: "3",
...@@ -550,10 +562,11 @@ export function useEventCalendarPosts(currentMonth: Date) { ...@@ -550,10 +562,11 @@ export function useEventCalendarPosts(currentMonth: Date) {
queryKey: ["event-calendar-posts", currentMonth.getFullYear(), currentMonth.getMonth()], queryKey: ["event-calendar-posts", currentMonth.getFullYear(), currentMonth.getMonth()],
queryFn: async () => { queryFn: async () => {
const queryParams = createEventCalendarQuery(currentMonth); const queryParams = createEventCalendarQuery(currentMonth);
try {
const response = await useCustomClient<HomeEnvelope<HomePagedResult<RawHomePost>>>( const response = await useCustomClient<HomeEnvelope<HomePagedResult<RawHomePost>>>(
`/post?${queryParams.toString()}`, `/post?${queryParams.toString()}`,
); );
return (response.responseData?.rows ?? []).map((item) => { return (response.responseData?.rows ?? []).map((item) => {
const categories = (item.categories ?? []) const categories = (item.categories ?? [])
.filter((category) => category?.id && category?.name) .filter((category) => category?.id && category?.name)
...@@ -600,6 +613,11 @@ export function useEventCalendarPosts(currentMonth: Date) { ...@@ -600,6 +613,11 @@ export function useEventCalendarPosts(currentMonth: Date) {
: null, : null,
} satisfies HomePostItem; } satisfies HomePostItem;
}); });
} catch (error) {
// eslint-disable-next-line no-console
console.warn("[useEventCalendarPosts] CMS unavailable, falling back to mock data", error);
return MOCK_HOME_POSTS;
}
}, },
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}); });
......
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { MOCK_FEATURED_MEMBERS_RESPONSE } from "@/app/api/mock-data";
const FEATURED_MEMBER_API_URL = const BACKEND_HOST = process.env.NEXT_PUBLIC_BACKEND_HOST || "https://news.vccihcm.vn";
"https://vccihcm.vn/api/v1.0/organizations?filters=users.status_id+%3D%3D+36ca1cc5-7b6e-4f9f-b973-69c5207deb62&pageSize=12&sortField=created_at&sortOrder=ASC"; const FEATURED_MEMBER_API_URL = `${BACKEND_HOST.replace(/\/+$/, "")}/api/v1.0/vcci/featured-members`;
export async function GET() { export async function GET() {
try { try {
const response = await fetch(FEATURED_MEMBER_API_URL, { const response = await fetch(FEATURED_MEMBER_API_URL, {
headers: { headers: { Accept: "application/json" },
Accept: "application/json",
},
next: { revalidate: 300 }, next: { revalidate: 300 },
}); });
if (!response.ok) { if (!response.ok) {
return NextResponse.json( // Khi upstream lỗi, vẫn trả mock để FE không hiển thị "Chưa có thông tin".
{ message: "Không thể tải dữ liệu hội viên tiêu biểu" }, console.warn(
{ status: response.status }, `[api/featured-members] upstream returned ${response.status}, serving mock data`,
); );
return NextResponse.json(MOCK_FEATURED_MEMBERS_RESPONSE, { status: 200 });
} }
const data = await response.json(); const data = await response.json();
return NextResponse.json(data); return NextResponse.json(data);
} catch (error) { } catch (error) {
console.error("Failed to fetch featured members", error); console.error("[api/featured-members] upstream failed, serving mock data", error);
return NextResponse.json(MOCK_FEATURED_MEMBERS_RESPONSE, { status: 200 });
return NextResponse.json(
{ message: "Không thể tải dữ liệu hội viên tiêu biểu" },
{ status: 500 },
);
} }
} }
/**
* Mock data dự phòng cho 2 endpoint BFF:
* - GET /api/featured-members
* - GET /api/partners
*
* Khi upstream (vccihcm.vn hoặc BE proxy) bị lỗi / timeout, các route này sẽ
* trả về payload có shape giống upstream để FE component vẫn render dữ liệu
* thay vì hiển thị "Chưa có thông tin".
*
* Shape khớp với vccihcm.vn: { responseData: { rows: [...] } }
*/
export type MockMember = {
id: string;
name: string;
avatar: string | null;
};
export type MockPartner = {
id: string;
name: string;
avatar: string | null;
website: string | null;
};
export const MOCK_FEATURED_MEMBERS_RESPONSE = {
responseData: {
rows: [
{ id: "mock-member-1", name: "Công ty Cổ phần Vinamilk", avatar: "/home/hoi-vien-tieu-bieu/1.webp" },
{ id: "mock-member-2", name: "Tập đoàn Bất động sản Novaland", avatar: "/home/hoi-vien-tieu-bieu/2.webp" },
{ id: "mock-member-3", name: "Công ty TNHH Samsung Electronics Việt Nam", avatar: "/home/hoi-vien-tieu-bieu/3.webp" },
{ id: "mock-member-4", name: "Ngân hàng TMCP Ngoại thương Việt Nam (Vietcombank)", avatar: "/home/hoi-vien-tieu-bieu/4.webp" },
{ id: "mock-member-5", name: "Công ty Cổ phần FPT", avatar: "/home/hoi-vien-tieu-bieu/5.webp" },
{ id: "mock-member-6", name: "Tập đoàn Vingroup", avatar: "/home/hoi-vien-tieu-bieu/6.webp" },
{ id: "mock-member-7", name: "Công ty Cổ phần Thủy sản Minh Phú", avatar: "/home/hoi-vien-tieu-bieu/7.webp" },
{ id: "mock-member-8", name: "Tập đoàn Dệt may Việt Nam (Vinatex)", avatar: "/home/hoi-vien-tieu-bieu/8.webp" },
{ id: "mock-member-9", name: "Công ty Cổ phần Hàng không VietJet", avatar: "/home/hoi-vien-tieu-bieu/9.webp" },
] satisfies MockMember[],
},
};
export const MOCK_PARTNERS_RESPONSE = {
responseData: {
rows: [
{ id: "mock-partner-1", name: "Đối tác 1", avatar: "/home/doi-tac/1.webp", website: "https://example.com" },
{ id: "mock-partner-2", name: "Đối tác 2", avatar: "/home/doi-tac/2.webp", website: "https://example.com" },
{ id: "mock-partner-3", name: "Đối tác 3", avatar: "/home/doi-tac/3.webp", website: null },
{ id: "mock-partner-4", name: "Đối tác 4", avatar: "/home/doi-tac/4.webp", website: null },
{ id: "mock-partner-5", name: "Đối tác 5", avatar: "/home/doi-tac/5.webp", website: null },
{ id: "mock-partner-6", name: "Đối tác 6", avatar: "/home/doi-tac/6.webp", website: null },
] satisfies MockPartner[],
},
};
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { MOCK_PARTNERS_RESPONSE } from "@/app/api/mock-data";
const PARTNER_API_URL = const BACKEND_HOST = process.env.NEXT_PUBLIC_BACKEND_HOST || "https://news.vccihcm.vn";
"https://vccihcm.vn/api/v1.0/organizations?filters=type%3D%3DSPONSOR&pageSize=12&sortField=sort_order&sortOrder=ASC"; const PARTNER_API_URL = `${BACKEND_HOST.replace(/\/+$/, "")}/api/v1.0/vcci/partners`;
export async function GET() { export async function GET() {
try { try {
const response = await fetch(PARTNER_API_URL, { const response = await fetch(PARTNER_API_URL, {
headers: { headers: { Accept: "application/json" },
Accept: "application/json",
},
next: { revalidate: 300 }, next: { revalidate: 300 },
}); });
if (!response.ok) { if (!response.ok) {
return NextResponse.json( // Khi upstream lỗi, vẫn trả mock để FE không hiển thị "Chưa có thông tin".
{ message: "Không thể tải dữ liệu đối tác" }, console.warn(
{ status: response.status }, `[api/partners] upstream returned ${response.status}, serving mock data`,
); );
return NextResponse.json(MOCK_PARTNERS_RESPONSE, { status: 200 });
} }
const data = await response.json(); const data = await response.json();
return NextResponse.json(data); return NextResponse.json(data);
} catch (error) { } catch (error) {
console.error("Failed to fetch partners", error); console.error("[api/partners] upstream failed, serving mock data", error);
return NextResponse.json(MOCK_PARTNERS_RESPONSE, { status: 200 });
return NextResponse.json(
{ message: "Không thể tải dữ liệu đối tác" },
{ status: 500 },
);
} }
} }
...@@ -80,6 +80,7 @@ export const normalizeVideoUrl = (url: string) => { ...@@ -80,6 +80,7 @@ export const normalizeVideoUrl = (url: string) => {
}; };
export async function fetchClientVideos(params?: { page?: number; pageSize?: number }) { export async function fetchClientVideos(params?: { page?: number; pageSize?: number }) {
try {
const response = await getVideo({ const response = await getVideo({
page: params?.page ?? 1, page: params?.page ?? 1,
pageSize: params?.pageSize ?? 10, pageSize: params?.pageSize ?? 10,
...@@ -101,4 +102,11 @@ export async function fetchClientVideos(params?: { page?: number; pageSize?: num ...@@ -101,4 +102,11 @@ export async function fetchClientVideos(params?: { page?: number; pageSize?: num
pageSize, pageSize,
totalPages: Math.max(1, Math.ceil(count / pageSize)), totalPages: Math.max(1, Math.ceil(count / pageSize)),
} satisfies ClientVideoListResult; } satisfies ClientVideoListResult;
} catch (error) {
// Khi CMS / BE gặp sự cố — rethrow để UI component tự xử lý
// (hiển thị trạng thái "Chưa có video nào.").
// eslint-disable-next-line no-console
console.warn("[fetchClientVideos] CMS unavailable", error);
throw error;
}
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment