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

Merge branch 'develop-news' of...

Merge branch 'develop-news' of https://gitlab.meu-solutions.com/vanhoangit/vcci-news into develop-news
parents f01580e6 62af7d47
......@@ -27,8 +27,10 @@ const handleCheckBaseRetryLogical = (failureCount: number, error: Error) => {
// Expired token error
if (error.response?.status === EXPIRED_TOKEN_ERROR) {
handleUnAuthorizationError()
return false
if (typeof window !== "undefined" && window.location.pathname.startsWith("/admin")) {
handleUnAuthorizationError();
}
return false;
}
// Denied permission error
......
'use client';
"use client";
import ImageNext from "@/components/shared/image-next";
import { Swiper, SwiperSlide } from "swiper/react";
......@@ -7,40 +7,98 @@ import { Swiper as SwiperType } from "swiper/types";
import { useRef } from "react";
import "swiper/css";
import { useGetBanner } from "@/api/endpoints/banner";
import { useQuery } from "@tanstack/react-query";
import { fetchCmsFileById, resolveCmsFileUrl } from "@/lib/api/files";
import { Skeleton } from "@/components/ui/skeleton";
type ApiEnvelope<T> = {
responseData?: T;
data?: {
responseData?: T;
};
};
const getEnvelopeData = <T,>(payload?: ApiEnvelope<T>) =>
payload?.responseData ?? payload?.data?.responseData;
function BannerSlideItem({ fileId, alt }: { fileId: string; alt: string }) {
const { data: file, isPending } = useQuery({
queryKey: ["file", fileId],
queryFn: () => fetchCmsFileById(fileId),
enabled: !!fileId,
});
if (isPending) {
return (
<Skeleton className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px]" />
);
}
const url = file ? resolveCmsFileUrl(file.path) : "/img-error.png";
return (
<ImageNext
src={url}
alt={alt}
width={2560}
height={720}
sizes="100vw"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
);
}
const Banner = () => {
const swiperRef = useRef<SwiperType | null>(null);
const { data: bannerData } = useGetBanner({
filters: "status@=ACTIVE",
sortField: "display_order",
sortOrder: "asc",
});
const pageData = getEnvelopeData<{ rows?: any[] }>(bannerData);
const rows = pageData?.rows ?? [];
if (!rows || rows.length === 0) {
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>
);
}
return (
<Swiper
modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
loop={rows.length > 1}
slidesPerView={1}
onSwiper={(s) => (swiperRef.current = s)}
className="w-full overflow-hidden"
>
<SwiperSlide>
<ImageNext
src="https://vcci-hcm.org.vn/wp-content/uploads/2025/10/1.1.-Hero-Banner-CEO-2025-Bi-Sai-Nam-2025-Nhe-2560x720-Px.jpg.webp"
alt="Banner"
width={2560}
height={720}
sizes="100vw"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
{rows.map((row: any) => (
<SwiperSlide key={row.id}>
{row.file_id ? (
<BannerSlideItem
fileId={row.file_id}
alt={row.banner_name || "Banner"}
/>
</SwiperSlide>
<SwiperSlide>
) : (
<ImageNext
src="https://vcci-hcm.org.vn/wp-content/uploads/2022/07/Landscape-HCM_3-01.png"
alt="Banner"
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>
))}
</Swiper>
);
}
};
export default Banner;
......@@ -13,18 +13,61 @@ import {
Youtube,
} from "lucide-react";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { subscribeNewsletterEmail } from "@/lib/api/newsletter-subscriptions";
import { getSiteInformation } from "@/api/endpoints/site-information";
import type { SiteInformationData } from "@/api/models";
const socialLinks = [
{ icon: <Facebook className="h-5 w-5" />, link: "https://www.facebook.com/VCCIHCMC/" },
{ icon: <Twitter className="h-5 w-5" />, link: "https://twitter.com/VCCI_HCM" },
{ icon: <Youtube className="h-5 w-5" />, link: "https://www.youtube.com/user/VCCIHCMC" },
type ApiEnvelope<T> = {
responseData?: T;
data?: {
responseData?: T;
};
};
type SocialItem = {
key: string;
url: string;
icon: React.ReactNode;
};
const fallbackSocials: SocialItem[] = [
{
key: "facebook",
url: "https://www.facebook.com/VCCIHCMC/",
icon: <Facebook className="h-5 w-5" />,
},
{
key: "twitter",
url: "https://twitter.com/VCCI_HCM",
icon: <Twitter className="h-5 w-5" />,
},
{
key: "youtube",
url: "https://www.youtube.com/user/VCCIHCMC",
icon: <Youtube className="h-5 w-5" />,
},
{
key: "linkedin",
url: "https://www.linkedin.com/company/vietnam-chamber-of-commerce-and-industry-ho-chi-minh-city-branch-vcci-hcm-?trk=biz-companies-cym",
icon: <Linkedin className="h-5 w-5" />,
link: "https://www.linkedin.com/company/vietnam-chamber-of-commerce-and-industry-ho-chi-minh-city-branch-vcci-hcm-?trk=biz-companies-cym",
},
];
const getEnvelopeData = <T,>(payload?: ApiEnvelope<T> | null) =>
payload?.responseData ?? payload?.data?.responseData;
const getSocialIcon = (key: string): React.ReactNode => {
const normalized = key.toLowerCase();
if (normalized.includes("facebook")) return <Facebook className="h-5 w-5" />;
if (normalized.includes("twitter") || normalized === "x") {
return <Twitter className="h-5 w-5" />;
}
if (normalized.includes("youtube")) return <Youtube className="h-5 w-5" />;
if (normalized.includes("linkedin")) return <Linkedin className="h-5 w-5" />;
return null;
};
const quickLinks = [
{ label: "Giới thiệu", href: "/gioi-thieu" },
{ label: "Hội viên", href: "/danh-ba-hoi-vien" },
......@@ -32,7 +75,8 @@ const quickLinks = [
{ label: "Xúc tiến Thương mại", href: "/xuc-tien-thuong-mai/co-hoi/" },
];
const isValidEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const isValidEmail = (value: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
function Footer() {
const [email, setEmail] = useState("");
......@@ -42,6 +86,68 @@ function Footer() {
const [message, setMessage] = useState("");
const [submitting, setSubmitting] = useState(false);
const { data: siteInformationResponse } =
useQuery<ApiEnvelope<SiteInformationData> | null>({
queryKey: ["site-information"],
queryFn: () => getSiteInformation().catch(() => null),
staleTime: 5 * 60 * 1000,
});
const siteInformation = getEnvelopeData<SiteInformationData>(
siteInformationResponse,
);
const primaryBranch =
siteInformation?.branches?.find((branch) => branch?.is_active) ??
siteInformation?.branches?.[0] ??
null;
const branches = (siteInformation?.branches ?? [])
.filter((branch) => branch?.is_active ?? true)
.sort((left, right) => (left.sort_order ?? 0) - (right.sort_order ?? 0));
const extraBranches = branches.filter(
(branch) => branch?.id && branch?.id !== primaryBranch?.id,
);
const contactInfo = {
name:
primaryBranch?.branch_name ??
siteInformation?.website_name ??
"LIÊN ĐOÀN THƯƠNG MẠI & CÔNG NGHIỆP VIỆT NAM - CHI NHÁNH KHU VỰC THÀNH PHỐ HỒ CHÍ MINH",
address:
siteInformation?.address ??
primaryBranch?.address ??
"171 Võ Thị Sáu, Phường Xuân Hòa, TP. HCM",
telephone:
siteInformation?.telephone ??
primaryBranch?.telephone ??
primaryBranch?.hotline ??
"+84 28 3932 6598",
fax: primaryBranch?.fax ?? "+84 28 3932 5472",
email:
siteInformation?.email ?? primaryBranch?.email ?? "info@vcci-hcm.org.vn",
};
const socialLinks = (() => {
const socials =
siteInformation?.socials ?? siteInformation?.link_socials ?? [];
const active = socials
.filter((item) => item?.is_active && item?.url)
.sort((left, right) => (left.sort_order ?? 0) - (right.sort_order ?? 0))
.map((item) => {
const key = item.icon_key || item.platform || item.label || "";
const icon = getSocialIcon(key);
if (!icon || !item.url) return null;
return {
key: item.id || key,
url: item.url,
icon,
} as SocialItem;
})
.filter((item): item is SocialItem => item !== null);
return active.length ? active : fallbackSocials;
})();
const handleSubmit = async () => {
const trimmedEmail = email.trim();
let hasError = false;
......@@ -75,7 +181,11 @@ function Footer() {
setAccepted(false);
setMessage("Đăng ký nhận thông tin thành công.");
} catch (error) {
setMessage(error instanceof Error ? error.message : "Không thể đăng ký nhận thông tin.");
setMessage(
error instanceof Error
? error.message
: "Không thể đăng ký nhận thông tin.",
);
} finally {
setSubmitting(false);
}
......@@ -144,48 +254,65 @@ function Footer() {
</div>
<div>
<h2 className="client-footer-title uppercase">
Liên hệ
</h2>
<h2 className="client-footer-title uppercase">Liên hệ</h2>
<div className="mt-2.5 h-[4px] w-[48px] rounded-full bg-[#f7b500]" />
<div className="mt-5 space-y-4">
<p className="max-w-[420px] text-[16px] font-semibold leading-[1.5] text-[#dce7ff]">
LIÊN ĐOÀN THƯƠNG MẠI & CÔNG NGHIỆP VIỆT NAM - CHI NHÁNH KHU VỰC THÀNH PHỐ HỒ CHÍ MINH
{contactInfo.name}
</p>
<div className="space-y-2.5 text-[15px] text-[#c7d8ff]">
<div className="flex items-start gap-3">
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-[#f7b500]" />
<span>171 Võ Thị Sáu, Phường Xuân Hòa, TP. HCM</span>
<span>{contactInfo.address}</span>
</div>
<div className="flex items-center gap-3">
<Phone className="h-4 w-4 shrink-0 text-[#f7b500]" />
<span>+84 28 3932 6598</span>
<span>{contactInfo.telephone}</span>
</div>
<div className="flex items-center gap-3">
<Printer className="h-4 w-4 shrink-0 text-[#f7b500]" />
<span>+84 28 3932 5472</span>
<span>{contactInfo.fax}</span>
</div>
<div className="flex items-center gap-3">
<Mail className="h-4 w-4 shrink-0 text-[#f7b500]" />
<a href="mailto:info@vcci-hcm.org.vn">info@vcci-hcm.org.vn</a>
<a href={`mailto:${contactInfo.email}`}>
{contactInfo.email}
</a>
</div>
</div>
{extraBranches.length > 0 ? (
<div className="pt-4">
<p className="text-[14px] font-semibold uppercase text-[#dce7ff]">
Các chi nhánh khác
</p>
<div className="mt-3 space-y-3 text-[14px] text-[#c7d8ff]">
{extraBranches.map((branch) => (
<div key={branch.id} className="space-y-1">
{branch.branch_name ? (
<p className="font-semibold text-[#dce7ff]">
{branch.branch_name}
</p>
) : null}
</div>
))}
</div>
</div>
) : null}
</div>
</div>
<div>
<h2 className="client-footer-title uppercase">
Kết nối
</h2>
<h2 className="client-footer-title uppercase">Kết nối</h2>
<div className="mt-2.5 h-[4px] w-[48px] rounded-full bg-[#f7b500]" />
<div className="mt-5 flex flex-wrap gap-3">
{socialLinks.map((item) => (
<a
key={item.link}
href={item.link}
key={item.key}
href={item.url}
target="_blank"
rel="noreferrer"
className="flex h-11 w-11 items-center justify-center rounded-full bg-[#2a4ec4] text-white transition-colors hover:bg-[#3b60da]"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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