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

fix ui

parent e4620189
......@@ -72,7 +72,7 @@ function BannerSlideItem({
if (isPending) {
return (
<Skeleton className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px]" />
<Skeleton className="w-full h-[220px] sm:h-[320px] md:h-[430px] lg:h-[540px]" />
);
}
......@@ -80,7 +80,7 @@ function BannerSlideItem({
? src
: file
? resolveCmsFileUrl(file.path)
: "/img-error.png";
: "/thumbnail.png";
return (
<ImageNext
......@@ -89,7 +89,8 @@ function BannerSlideItem({
width={2560}
height={720}
sizes="100vw"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
fallback="/thumbnail.png"
className="w-full h-[220px] sm:h-[320px] md:h-[430px] lg:h-[540px] object-cover"
/>
);
}
......@@ -119,7 +120,7 @@ const Banner = () => {
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">
<div className="w-full h-[220px] sm:h-[320px] md:h-[430px] lg:h-[540px] bg-slate-100 flex items-center justify-center">
<Skeleton className="w-full h-full" />
</div>
);
......@@ -127,7 +128,7 @@ const Banner = () => {
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">
<div className="w-full h-[220px] sm:h-[320px] md:h-[430px] lg:h-[540px] bg-slate-100 flex items-center justify-center">
<Skeleton className="w-full h-full" />
</div>
);
......
......@@ -43,10 +43,10 @@ function BusinessOpportunities() {
<span className="mt-1 h-[40px] w-[2px] shrink-0 rounded-full bg-[#f7b500] transition-opacity duration-200 group-hover:opacity-0" />
<div className="hidden min-w-0 group-hover:block">
<h3 className="line-clamp-2 text-[16px] font-bold leading-[1.45] text-[#264798] md:text-[17px]">
<h3 className="line-clamp-2 text-[15px] font-bold leading-[1.45] text-[#264798] md:text-[16px]">
{item.title}
</h3>
<p className="mt-2 text-[13px] text-[#9aa8c1]">
<p className="mt-1.5 text-[13px] text-[#9aa8c1]">
{dayjs(item.publishedAt || item.createdAt).format("DD/MM/YYYY")}
</p>
</div>
......
......@@ -32,13 +32,13 @@ function Events() {
</Link>
</div>
<div className="grid items-stretch gap-3 xl:grid-cols-[minmax(0,1.02fr)_minmax(270px,0.98fr)]">
<div className="grid items-stretch gap-3 md:grid-cols-[minmax(0,1.02fr)_minmax(270px,0.98fr)]">
{featuredEvent ? (
<Link
href={featuredEvent.externalLink}
className="flex h-full flex-col overflow-hidden rounded-[22px] bg-white text-[#20408f] shadow-[0_14px_28px_rgba(10,39,95,0.18)]"
className="group flex h-full cursor-pointer flex-col overflow-hidden rounded-[22px] bg-white text-[#20408f] shadow-[0_14px_28px_rgba(10,39,95,0.18)]"
>
<div className="h-[220px] overflow-hidden md:h-[235px] xl:h-[248px]">
<div className="relative h-[180px] overflow-hidden md:h-[220px] xl:h-[248px]">
<ImageNext
src={featuredEvent.thumbnail?.url ?? "/thumbnail.png"}
alt={featuredEvent.thumbnail?.alt || featuredEvent.title}
......@@ -46,10 +46,13 @@ function Events() {
height={520}
className="h-full w-full object-cover"
/>
<span className="absolute left-3 top-3 inline-flex rounded-full bg-[#f7b500] px-3 py-1 text-[12px] font-bold text-[#15357a]">
{featuredEvent.categories[0]?.name || "Sự kiện"}
</span>
</div>
<div className="flex flex-col p-3 pt-2.5">
<h3 className="text-[16px] font-bold uppercase leading-[1.28] text-[#22459b] line-clamp-2 md:text-[18px]">
<h3 className="text-[16px] font-bold uppercase leading-[1.28] text-[#22459b] line-clamp-2 transition-colors duration-200 group-hover:text-[#f7b500] md:text-[18px]">
{featuredEvent.title}
</h3>
{(() => {
......@@ -85,7 +88,7 @@ function Events() {
</Link>
) : (
<div className="flex h-full flex-col overflow-hidden rounded-[22px] bg-white text-[#20408f] shadow-[0_14px_28px_rgba(10,39,95,0.12)]">
<div className="h-[220px] bg-[#d7e3f9] md:h-[235px] xl:h-[248px]" />
<div className="h-[180px] bg-[#d7e3f9] md:h-[220px] xl:h-[248px]" />
<div className="space-y-2 p-3 pt-2.5">
<div className="h-6 w-5/6 rounded bg-[#e7eefb]" />
<div className="h-4 w-24 rounded bg-[#eef3fb]" />
......@@ -93,13 +96,13 @@ function Events() {
</div>
)}
<div className="flex h-full flex-col gap-3">
<div className="grid h-full grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-1">
{sideSlots.map((item, index) =>
item ? (
<Link
key={item.id}
href={item.externalLink}
className="flex flex-1 items-center gap-3 rounded-[18px] bg-white/10 p-2.5 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.08)] backdrop-blur-sm transition-colors hover:bg-white/14"
className="group flex flex-1 cursor-pointer items-center gap-3 rounded-[18px] bg-white/10 p-2.5 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.08)] backdrop-blur-sm transition-colors hover:bg-white/14"
>
<div className="h-[64px] w-[64px] shrink-0 overflow-hidden rounded-[12px]">
<ImageNext
......@@ -112,7 +115,7 @@ function Events() {
</div>
<div className="min-w-0">
<h4 className="line-clamp-1 text-[15px] font-semibold leading-[1.35] text-white">
<h4 className="line-clamp-1 text-[15px] font-semibold leading-[1.35] text-white transition-colors duration-200 group-hover:text-[#f7b500]">
{item.title}
</h4>
{(() => {
......@@ -139,11 +142,21 @@ function Events() {
</p>
) : null;
})()}
<p className="mt-1 text-[12px] text-white/78">
{dayjs(item.startedAt || item.publishedAt || item.createdAt).format(
"DD/MM/YYYY",
<div className="mt-1 flex items-center gap-2">
{item.categories[0]?.name && (
<span className="text-[12px] font-medium text-[#f7b500]">
{item.categories[0].name}
</span>
)}
</p>
{item.categories[0]?.name && (
<span className="text-[12px] text-white/50"></span>
)}
<p className="text-[12px] text-white/78">
{dayjs(item.startedAt || item.publishedAt || item.createdAt).format(
"DD/MM/YYYY",
)}
</p>
</div>
</div>
</Link>
) : (
......
......@@ -36,32 +36,32 @@ function FeaturedNews() {
</Link>
</div>
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.14fr)_minmax(0,0.96fr)]">
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.14fr)_minmax(0,0.96fr)]">
{primaryItem ? (
<Link
href={primaryItem.externalLink}
className="group relative block min-h-[260px] overflow-hidden rounded-[24px] bg-[#0d2f5f] shadow-[0_18px_38px_rgba(28,52,120,0.22)] md:min-h-[320px] xl:min-h-[350px]"
className="group relative block cursor-pointer overflow-hidden rounded-[20px] bg-[#0d2f5f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] md:rounded-[24px] md:min-h-[320px] lg:min-h-[380px]"
>
<div className="relative h-full min-h-[260px] md:min-h-[320px] xl:min-h-[350px]">
<div className="relative h-full min-h-[195px] md:min-h-[320px] lg:min-h-[380px]">
<ImageNext
src={primaryItem.thumbnail?.url ?? "/thumbnail.png"}
alt={primaryItem.thumbnail?.alt || primaryItem.title}
width={1200}
height={800}
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.04]"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-linear-to-t from-[#26356d] via-[#53669b]/34 to-transparent" />
<div className="relative flex h-full flex-col justify-end p-4 md:p-5">
<div className="relative flex h-full flex-col justify-end p-3.5 md:p-5">
<span className="mb-2 inline-flex w-fit rounded-[10px] bg-[#ffc400] px-3 py-1 text-sm font-bold text-[#1d3f90]">
{primaryItem.categories[0]?.name || "Tin nổi bật"}
</span>
<h3 className="max-w-3xl line-clamp-3 text-[20px] font-bold leading-[1.28] text-white md:text-[28px] xl:text-[32px]">
<h3 className="max-w-3xl line-clamp-2 text-[16px] font-bold leading-[1.32] text-white transition-colors duration-200 group-hover:text-[#f7b500] md:line-clamp-3 md:text-[28px] lg:text-[32px]">
{primaryItem.title}
</h3>
<p className="mt-2 text-base font-medium text-white/78 md:text-[17px]">
<p className="mt-1.5 text-[15px] font-medium text-white/78 md:mt-2 md:text-[17px]">
{dayjs(primaryItem.publishedAt || primaryItem.createdAt).format(
"DD/MM/YYYY",
)}
......@@ -70,8 +70,8 @@ function FeaturedNews() {
</div>
</Link>
) : (
<div className="relative min-h-[260px] overflow-hidden rounded-3xl bg-[#e9eef8] shadow-[0_18px_38px_rgba(28,52,120,0.12)] md:min-h-[320px] xl:min-h-[350px]">
<div className="flex h-full min-h-[260px] flex-col justify-end p-4 md:min-h-80 md:p-5 xl:min-h-[350px]">
<div className="relative overflow-hidden rounded-[20px] bg-[#e9eef8] shadow-[0_16px_32px_rgba(28,52,120,0.12)] md:rounded-3xl md:min-h-[320px] lg:min-h-[380px]">
<div className="flex h-full min-h-[195px] flex-col justify-end p-3.5 md:min-h-80 md:p-5 lg:min-h-[380px]">
<span className="mb-2 h-8 w-28 rounded-[10px] bg-white/80" />
<div className="h-8 w-3/4 rounded bg-white/90 md:h-10" />
<div className="mt-2 h-5 w-28 rounded bg-white/70" />
......@@ -86,15 +86,15 @@ function FeaturedNews() {
<Link
key={item.id}
href={item.externalLink}
className="group relative block min-h-[195px] overflow-hidden rounded-[20px] bg-[#27447f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] md:min-h-[205px] xl:min-h-[215px]"
className="group relative block cursor-pointer overflow-hidden rounded-[20px] bg-[#27447f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] md:min-h-[205px] lg:min-h-[215px]"
>
<div className="relative h-full min-h-[195px] md:min-h-[205px] xl:min-h-[215px]">
<div className="relative h-full min-h-[195px] md:min-h-[205px] lg:min-h-[215px]">
<ImageNext
src={item.thumbnail?.url ?? "/thumbnail.png"}
alt={item.thumbnail?.alt || item.title}
width={600}
height={420}
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.05]"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-linear-to-t from-[#5a6796] via-[#405083]/34 to-transparent" />
......@@ -103,7 +103,7 @@ function FeaturedNews() {
{item.categories[0]?.name || "Tin nổi bật"}
</span>
<h4 className="line-clamp-2 text-[16px] font-bold leading-[1.32] text-white md:text-[17px]">
<h4 className="line-clamp-2 text-[16px] font-bold leading-[1.32] text-white transition-colors duration-200 group-hover:text-[#f7b500] md:text-[17px]">
{item.title}
</h4>
......@@ -118,9 +118,9 @@ function FeaturedNews() {
) : (
<div
key={`featured-placeholder-${index}`}
className="min-h-[195px] rounded-[20px] bg-[#dde5f3] shadow-[0_16px_32px_rgba(28,52,120,0.1)] md:min-h-[205px] xl:min-h-[215px]"
className="rounded-[20px] bg-[#dde5f3] shadow-[0_16px_32px_rgba(28,52,120,0.1)] md:min-h-[205px] lg:min-h-[215px]"
>
<div className="flex h-full min-h-[195px] flex-col justify-end p-3.5 md:min-h-[205px] xl:min-h-[215px]">
<div className="flex h-full min-h-[195px] flex-col justify-end p-3.5 md:min-h-[205px] lg:min-h-[215px]">
<span className="mb-2 h-7 w-24 rounded-[10px] bg-white/80" />
<div className="h-6 w-5/6 rounded bg-white/90" />
<div className="mt-2 h-4 w-24 rounded bg-white/70" />
......@@ -130,25 +130,28 @@ function FeaturedNews() {
)}
</div>
<div className="overflow-hidden rounded-[28px] bg-linear-to-r from-[#214b95] to-[#2b66bb] px-5 py-5 text-white shadow-[0_18px_38px_rgba(28,52,120,0.2)] md:px-7">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="text-[12px] uppercase tracking-[0.4em] text-white/80">
<div className="flex h-full min-h-full items-center justify-center overflow-hidden rounded-[28px] bg-linear-to-r from-[#214b95] to-[#2b66bb] px-5 py-5 text-white shadow-[0_18px_38px_rgba(28,52,120,0.2)] md:px-7">
<div className="flex w-full flex-col items-center justify-center gap-4 text-center md:flex-row md:gap-6 md:text-left lg:gap-8 xl:gap-12">
<div className="flex flex-col items-center md:items-start">
<p className="text-[11px] uppercase tracking-[0.2em] text-white/80 lg:text-[12px] lg:tracking-[0.4em]">
Quảng bá & tiếp cận
</p>
<h3 className="mt-2 max-w-[520px] text-[20px] font-extrabold uppercase leading-[1.02] xl:text-[28px]">
Cộng đồng doanh nghiệp
<h3 className="mt-2 text-[20px] font-extrabold uppercase leading-[1.1] lg:text-[18px] xl:text-[22px]">
<span>Cộng đồng</span>
<br className="hidden lg:block" />
<span className="lg:hidden"> </span>
<span>doanh nghiệp</span>
</h3>
</div>
<div className="w-full max-w-60 rounded-[999px] bg-white px-4 py-3 text-[#173f88] shadow-[0_10px_24px_rgba(8,25,74,0.12)]">
<div className="flex items-center gap-3 text-sm font-medium">
<Mail className="h-5 w-5 shrink-0" />
<span>info@vcci-hcm.org.vn</span>
<div className="shrink-0 flex flex-col gap-2 rounded-[20px] bg-white px-4 py-3 text-[#173f88] shadow-[0_10px_24px_rgba(8,25,74,0.12)] sm:min-w-fit lg:w-auto lg:rounded-[999px]">
<div className="flex items-center gap-2 text-[13px] font-medium sm:text-sm">
<Mail className="h-4 w-4 shrink-0 sm:h-5 sm:w-5" />
<span className="whitespace-nowrap">info@vcci-hcm.org.vn</span>
</div>
<div className="mt-2 flex items-center gap-3 text-sm font-medium">
<Phone className="h-5 w-5 shrink-0" />
<span>+84 28 3932 6598</span>
<div className="flex items-center gap-2 text-[13px] font-medium sm:text-sm">
<Phone className="h-4 w-4 shrink-0 sm:h-5 sm:w-5" />
<span className="whitespace-nowrap">+84 28 3932 6598</span>
</div>
</div>
</div>
......
......@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
import { Autoplay } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import dayjs from "dayjs";
const MEMBER_CONNECTION_FALLBACK_IMAGE = "/home/20-2048x1365.webp";
const FEATURED_MEMBER_API_URL = `${links.siteURL}api/featured-members`;
......@@ -50,13 +51,9 @@ function Members() {
const [featuredMembers, setFeaturedMembers] = useState<FeaturedMemberState>({
status: "loading",
});
const featuredConnection = memberConnectionPosts[0];
const connectionPosts = memberConnectionPosts.slice(0, 2);
const sectionLink =
categoryLinks.get(categoryNames.ketNoiHoiVien.toLowerCase()) ?? "/hoi-vien/ket-noi-hoi-vien";
const connectionImage =
featuredConnection?.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE;
const connectionImageAlt =
featuredConnection?.thumbnail?.alt || featuredConnection?.title || "VCCI HCM";
const displayMembers =
featuredMembers.status === "ready" ? featuredMembers.rows.slice(0, 9) : [];
......@@ -187,22 +184,113 @@ function Members() {
</h2>
<div className="mt-2.5 h-[4px] w-[40px] rounded-full bg-[#f7b500]" />
</div>
<Link
href={sectionLink}
className="pt-1 text-sm font-semibold text-[#24469c] transition-colors hover:text-[#1b55a1]"
>
Xem tất cả
</Link>
</div>
<Link
href={sectionLink}
className="block overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
>
<div className="aspect-[1.25/1] overflow-hidden rounded-[20px]">
<ImageNext
src={connectionImage}
alt={connectionImageAlt}
width={520}
height={420}
className="h-full w-full object-cover"
/>
</div>
</Link>
<div>
{connectionPosts.length > 0 ? (
<>
{/* Mobile + xl+: Swiper */}
<div className="md:hidden xl:block">
<Swiper
modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop={connectionPosts.length > 1}
slidesPerView={1}
className="w-full overflow-hidden rounded-[20px]"
>
{connectionPosts.map((item) => (
<SwiperSlide key={item.id}>
<Link
href={item.externalLink}
className="group relative block cursor-pointer overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
>
<div className="aspect-[16/10] overflow-hidden xl:aspect-[1.25/1]">
<ImageNext
src={item.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE}
alt={item.thumbnail?.alt || item.title}
width={520}
height={420}
className="h-full w-full object-cover object-[center_80%]"
/>
</div>
<div className="absolute inset-0 bg-linear-to-t from-[#0d2f5f]/85 via-[#0d2f5f]/30 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-4">
<h4 className="line-clamp-2 text-[15px] font-bold leading-[1.32] text-white transition-colors duration-200 group-hover:text-[#f7b500]">
{item.title}
</h4>
<div className="mt-1.5 flex items-center gap-2">
{item.categories[0]?.name && (
<>
<span className="text-[12px] font-medium text-[#f7b500]">
{item.categories[0].name}
</span>
<span className="text-[12px] text-white/50"></span>
</>
)}
<p className="text-[12px] font-medium text-white/78">
{dayjs(item.publishedAt || item.createdAt).format("DD/MM/YYYY")}
</p>
</div>
</div>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
{/* md to < xl: Grid 2 columns */}
<div className="hidden gap-4 md:grid md:grid-cols-2 xl:hidden">
{connectionPosts.map((item) => (
<Link
key={item.id}
href={item.externalLink}
className="group relative block cursor-pointer overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
>
<div className="aspect-[16/10] overflow-hidden">
<ImageNext
src={item.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE}
alt={item.thumbnail?.alt || item.title}
width={520}
height={420}
className="h-full w-full object-cover object-[center_80%]"
/>
</div>
<div className="absolute inset-0 bg-linear-to-t from-[#0d2f5f]/85 via-[#0d2f5f]/30 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-4">
<h4 className="line-clamp-2 text-[15px] font-bold leading-[1.32] text-white transition-colors duration-200 group-hover:text-[#f7b500]">
{item.title}
</h4>
<div className="mt-1.5 flex items-center gap-2">
{item.categories[0]?.name && (
<>
<span className="text-[12px] font-medium text-[#f7b500]">
{item.categories[0].name}
</span>
<span className="text-[12px] text-white/50"></span>
</>
)}
<p className="text-[12px] font-medium text-white/78">
{dayjs(item.publishedAt || item.createdAt).format("DD/MM/YYYY")}
</p>
</div>
</div>
</Link>
))}
</div>
</>
) : (
<div className="rounded-[16px] bg-[#eef3fb] px-5 py-10 text-center text-sm text-[#7f8eab]">
Chưa có thông tin.
</div>
)}
</div>
</aside>
</section>
);
......
......@@ -43,10 +43,10 @@ function PolicyAndLaws() {
<span className="mt-1 h-[40px] w-[2px] shrink-0 rounded-full bg-[#f7b500] transition-opacity duration-200 group-hover:opacity-0" />
<div className="hidden min-w-0 group-hover:block">
<h3 className="line-clamp-2 text-[16px] font-bold leading-[1.45] text-[#264798] md:text-[17px]">
<h3 className="line-clamp-2 text-[15px] font-bold leading-[1.45] text-[#264798] md:text-[16px]">
{item.title}
</h3>
<p className="mt-2 text-[13px] text-[#9aa8c1]">
<p className="mt-1.5 text-[13px] text-[#9aa8c1]">
{dayjs(item.publishedAt || item.createdAt).format("DD/MM/YYYY")}
</p>
</div>
......
'use client';
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import ImageNext from "@/components/shared/image-next";
import Link from "next/link";
function QuickLinks() {
const { quickLinkPosts } = useHomePosts();
const linkSlots = Array.from({ length: 4 }, (_, index) => quickLinkPosts[index] ?? null);
function Advertisements({ count = 2 }: { count?: number }) {
const mdCols = count >= 3 ? "md:grid-cols-3" : "md:grid-cols-2";
return (
<aside className="w-full xl:grid xl:w-[32%] xl:grid-rows-[0.74fr_0.88fr] xl:gap-4">
<div className="rounded-[22px] border border-[#dbe4f2] bg-white p-4 shadow-[0_8px_24px_rgba(31,59,124,0.08)] xl:h-full">
<h2 className="client-section-title uppercase text-[#24469c]">
Liên kết nhanh
</h2>
<div className="mt-3 h-[5px] w-[68px] rounded-full bg-[#f7b500]" />
<div className="mt-4 space-y-3">
{linkSlots.map((item, index) =>
item ? (
<Link
key={item.id}
href={item.externalLink}
className="flex items-start gap-3 text-[15px] leading-[1.32] text-[#556684] transition-colors hover:text-[#21408f]"
>
<span className="mt-1 text-[#e2a500]"></span>
<span className="line-clamp-1">{item.title}</span>
</Link>
) : (
<div
key={`quick-link-placeholder-${index}`}
className="flex items-start gap-3"
>
<span className="mt-1 h-4 w-2 shrink-0 rounded bg-[#f5d774]" />
<span className="h-5 w-5/6 rounded bg-[#eef3fb]" />
</div>
),
)}
<aside className={`flex w-full flex-col gap-4 md:grid ${mdCols} xl:grid xl:order-2 xl:w-[22%] xl:grid-cols-1 xl:gap-4 xl:self-center`}>
<Link
href="https://hardwaretools.com.vn/"
className="block overflow-hidden rounded-[28px] shadow-[0_12px_28px_rgba(31,59,124,0.14)]"
>
<div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]">
<ImageNext
src="/home/20-2048x1365.webp"
alt="Quảng cáo 1"
width={2048}
height={1365}
className="h-full w-full object-cover object-[center_80%]"
/>
</div>
</div>
</Link>
<Link
href="https://hardwaretools.com.vn/"
className="mt-4 block overflow-hidden rounded-[28px] shadow-[0_12px_28px_rgba(31,59,124,0.14)] xl:mt-0 xl:h-full"
className="block overflow-hidden rounded-[28px] shadow-[0_12px_28px_rgba(31,59,124,0.14)]"
>
<div className="aspect-[1.55/1] overflow-hidden xl:h-full xl:aspect-auto">
<div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]">
<ImageNext
src="/home/20-2048x1365.webp"
alt="Liên kết nhanh"
alt="Quảng cáo 2"
width={2048}
height={1365}
className="h-full w-full object-cover"
className="h-full w-full object-cover object-[center_80%]"
/>
</div>
</Link>
{count >= 3 && (
<Link
href="https://hardwaretools.com.vn/"
className="block overflow-hidden rounded-[28px] shadow-[0_12px_28px_rgba(31,59,124,0.14)]"
>
<div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]">
<ImageNext
src="/home/20-2048x1365.webp"
alt="Quảng cáo 3"
width={2048}
height={1365}
className="h-full w-full object-cover object-[center_80%]"
/>
</div>
</Link>
)}
</aside>
);
}
export default QuickLinks;
export default Advertisements;
......@@ -169,7 +169,7 @@ function VideoAndPartners() {
alt={video.name}
width={640}
height={440}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
className="h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-black/18" />
<div className="absolute inset-0 flex items-center justify-center">
......
......@@ -267,6 +267,21 @@ function createCategoryPostsQuery(categoryId: string, pageSize: string) {
});
}
function createMultiCategoryPostsQuery(categoryIds: string[], pageSize: string) {
return new URLSearchParams({
page: "1",
pageSize,
sortField: "release_at",
sortOrder: "desc",
filters: [
`category.id==(${categoryIds.join("|")})`,
"is_hidden==false",
"is_active==true",
"type==news",
].join(","),
});
}
function createEventCalendarQuery(currentMonth: Date) {
// We'll filter by date on client-side for more flexibility
return new URLSearchParams({
......@@ -310,7 +325,10 @@ async function fetchHomePostsFromApi() {
const tinVcciQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.tinVcci, "6");
const tinKinhTeQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.tinKinhTe, "6");
const chuyenDeQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.chuyenDe, "6");
const eventQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.suKien, "5");
const eventQuery = createMultiCategoryPostsQuery(
[HOME_CATEGORY_IDS.suKien, HOME_CATEGORY_IDS.daoTao],
"5"
);
const policyQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.chinhSachPhapLuat, "6");
const quickLinksQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.lienKetNhanh, "6");
const trainingQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.daoTao, "10");
......
'use client'
import FeaturedNews from "./components/featured-news";
import QuickLinks from "./components/quick-links";
import Advertisements from "./components/quick-links";
import News from "./components/news";
import Events from "./components/events";
import BusinessOpportunities from "./components/business-opportunities";
......@@ -29,9 +29,9 @@ const Page = () => {
</Link>
</div> */}
<section className="flex flex-col lg:flex-row pb-8 gap-5 mb-0">
<section className="flex flex-col xl:flex-row pb-8 gap-5 mb-0">
<News />
<QuickLinks />
<Advertisements count={3} />
</section >
<section className="flex flex-col gap-5 xl:flex-row xl:items-stretch" >
......@@ -39,7 +39,7 @@ const Page = () => {
<EventsCalendar />
</section >
<div className="flex flex-col lg:flex-row gap-5 pb-10 mb-0" >
<div className="flex flex-col lg:flex-row gap-5" >
<div className="flex flex-col flex-1">
{/* <div>
<Link href="https://vcci-hcm.org.vn/wp-content/uploads/2022/11/MEDIA-KIT_VCCI-HCM-2022-Final.pdf">
......@@ -52,9 +52,12 @@ const Page = () => {
</Link>
</div> */}
<section className="flex flex-col md:flex-row gap-5 pt-8">
<BusinessOpportunities />
<PolicyAndLaws />
<section className="flex flex-col xl:flex-row gap-5">
<div className="flex flex-col md:flex-row gap-5 pt-8 flex-1 order-2 xl:order-1">
<BusinessOpportunities />
<PolicyAndLaws />
</div>
<Advertisements />
</section>
</div>
......
'use client';
import { useMemo, useState } from "react";
import { Globe2, Newspaper, TrendingUp } from "lucide-react";
import { FileText, Globe2, Newspaper, TrendingUp } from "lucide-react";
import ImageNext from "@/components/shared/image-next";
import type { DynamicPostItem } from "../types";
......@@ -236,7 +236,9 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
<div className="mt-7 overflow-hidden rounded-[30px] border border-[#dce7f7] bg-white shadow-[0_18px_42px_rgba(17,24,39,0.06)]">
<div className="relative min-h-[280px] bg-[#f3f7ff] p-4 sm:p-5">
<div className="absolute inset-x-4 top-4 z-10 flex flex-wrap gap-2 sm:inset-x-6 sm:top-6">
{activeRegion.markets.map((item) => (
{activeRegion.markets.map((item) => {
const hasDoc = item.href && item.href !== "#";
return (
<a
key={`${activeRegion.key}-${item.name}`}
href={item.href}
......@@ -246,8 +248,12 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
>
<span className={`h-2.5 w-2.5 rounded-full ${item.tone}`} />
<span>{item.name}</span>
{hasDoc && (
<FileText className="h-3.5 w-3.5 text-[#2450b5]" />
)}
</a>
))}
);
})}
</div>
<ImageNext
......
......@@ -143,6 +143,28 @@ export default function AdminNewsletterEmailsPage() {
return (
<div className="space-y-8">
<div className="flex items-start gap-3 rounded-xl border border-red-300 bg-red-50 px-4 py-3 text-red-800">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="mt-0.5 h-5 w-5 shrink-0 text-red-500"
>
<path
fillRule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.5 5.25a.75.75 0 0 0-.75.75v6a.75.75 0 0 0 1.5 0v-6a.75.75 0 0 0-.75-.75Zm0 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clipRule="evenodd"
/>
</svg>
<div className="text-sm">
<p className="font-semibold">Cảnh báo xóa dữ liệu</p>
<p className="mt-0.5 text-red-700">
Việc xóa email sẽ làm mất dữ liệu hoàn toàn và không thể hoàn tác
được. Vui lòng cân nhắc thật kỹ trước khi xóa.
</p>
</div>
</div>
<AdminTableLayout
searchValue={search}
searchPlaceholder="Tìm kiếm email đăng ký..."
......
......@@ -326,6 +326,30 @@ export default function HeaderConfigPage() {
return (
<div className="space-y-8">
<div className="flex items-start gap-3 rounded-xl border border-amber-300 bg-amber-50 px-4 py-3 text-amber-800">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="mt-0.5 h-5 w-5 shrink-0 text-amber-500"
>
<path
fillRule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clipRule="evenodd"
/>
</svg>
<div className="text-sm">
<p className="font-semibold">Lưu ý quan trọng</p>
<p className="mt-0.5 text-amber-700">
Việc chỉnh sửa hoặc xóa danh mục sẽ ảnh hưởng trực tiếp đến cấu trúc
Website và có thể gây mất dữ liệu trang liên quan. Vui lòng thao tác
cẩn thận và kiểm tra kỹ trước khi xác nhận. Nếu phát sinh lỗi, xin vui
lòng báo ngay cho bên kỹ thuật để được hỗ trợ.
</p>
</div>
</div>
<HeaderCategoryStats
total={flatRows.length}
root={flatRows.filter((item) => !item.parentId).length}
......
......@@ -33,6 +33,24 @@ import {
const PAGE_SIZE = 10;
function resolveApiError(error: unknown, fallback: string) {
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number; data?: { message?: string; error?: string } } };
const status = axiosError.response?.status;
const apiMessage = axiosError.response?.data?.message || axiosError.response?.data?.error;
if (status && apiMessage) {
return `[Lỗi ${status}] ${apiMessage}`;
}
if (status) {
return `[Lỗi ${status}] ${fallback}`;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return fallback;
}
const inputClassName =
"rounded-2xl border-[#063e8e]/15 bg-white text-gray-700 shadow-sm placeholder:text-gray-400 focus-visible:ring-[#063e8e]/20";
......@@ -305,7 +323,7 @@ export default function AdminMediaPage() {
setItems(result.rows);
setTotal(result.count);
} catch (error) {
toast.error(error instanceof Error ? error.message : "Không thể tải danh sách ảnh");
toast.error(resolveApiError(error, "Không thể tải danh sách ảnh"));
setItems([]);
setTotal(0);
} finally {
......@@ -337,11 +355,11 @@ export default function AdminMediaPage() {
file: data.file,
original: data.name,
});
toast.success("Đã tải ảnh lên");
toast.success("Đã tải ảnh lên thành công");
setDialogOpen(false);
await load();
} catch (error) {
toast.error(error instanceof Error ? error.message : "Không thể tải ảnh lên");
toast.error(resolveApiError(error, "Không thể tải ảnh lên"));
} finally {
setSaving(false);
}
......@@ -352,11 +370,11 @@ export default function AdminMediaPage() {
try {
await deleteCmsFile(deleteTarget.id);
toast.success("Đã xóa ảnh");
toast.success("Đã xóa ảnh thành công");
setDeleteTarget(null);
await load();
} catch (error) {
toast.error(error instanceof Error ? error.message : "Không thể xóa ảnh");
toast.error(resolveApiError(error, "Không thể xóa ảnh"));
}
};
......
......@@ -9,20 +9,6 @@ const LEGACY_MEDIA_HOSTS = new Set([
const normalizeOrigin = (value?: string | null) =>
value?.trim().replace(/\/+$/, "").replace(/\/api\/v\d+\.\d+$/, "") || "";
const extractUploadPath = (pathname: string) => {
const markers = ["/api/uploads/", "/uploads/", "/images/", "/wp-content/uploads/"];
for (const marker of markers) {
const index = pathname.indexOf(marker);
if (index >= 0) {
const cleanPath = pathname.slice(index).replace(/^\/api\/uploads\//, "/uploads/");
return cleanPath;
}
}
return null;
};
const normalizeUploadPath = (pathname: string) => {
if (pathname.includes("/api/uploads/")) {
return pathname.replace(/^.*\/api\/uploads\//, "/uploads/");
......@@ -107,6 +93,13 @@ export const resolveUploadUrl = (value?: string | null) => {
if (/^https?:\/\//i.test(trimmed)) {
try {
const url = new URL(trimmed);
// Keep URLs from current backend/frontend origins as-is
const currentOrigins = [backendOrigin, frontendOrigin].filter(Boolean);
if (currentOrigins.some(origin => url.origin === origin)) {
return trimmed;
}
const normalizedPath = normalizeUploadPath(url.pathname);
if (normalizedPath !== url.pathname) {
......@@ -114,10 +107,8 @@ export const resolveUploadUrl = (value?: string | null) => {
}
if (LEGACY_MEDIA_HOSTS.has(url.hostname)) {
const uploadPath = extractUploadPath(url.pathname);
if (uploadPath) {
return backendOrigin ? `${backendOrigin}${uploadPath}` : uploadPath;
}
// Keep legacy media URLs as-is since they already have correct domain
return trimmed;
}
} catch {
return trimmed;
......
declare module "swiper/css";
declare module "swiper/css/bundle";
declare module "swiper/css/effect-fade";
declare module "swiper/css/navigation";
declare module "swiper/css/pagination";
declare module "swiper/css/scrollbar";
declare module "swiper/css/thumbs";
declare module "swiper/css/free-mode";
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