Commit f64d2172 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

[tag]0.1-vcci

parents f34822d0 d5228e26
Pipeline #52997 passed with stages
in 1 minute and 25 seconds
"use client"; "use client";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay } from "swiper/modules"; import { Autoplay } from "swiper/modules";
import { Swiper as SwiperType } from "swiper/types"; import { Swiper as SwiperType } from "swiper/types";
...@@ -84,18 +84,13 @@ function BannerSlideItem({ ...@@ -84,18 +84,13 @@ function BannerSlideItem({
: "/thumbnail.png"; : "/thumbnail.png";
return ( return (
<Image <SafeImage
src={url} src={url}
fallbackSrc="/thumbnail.png"
alt={alt} alt={alt}
width={2560} width={2560}
height={720} height={720}
sizes="100vw" sizes="100vw"
onError={(e) => {
const img = e.currentTarget as HTMLImageElement;
if (!img.src.includes("thumbnail.png")) {
img.src = "/thumbnail.png";
}
}}
className="w-full h-[220px] sm:h-[320px] md:h-[430px] lg:h-[540px] object-cover" className="w-full h-[220px] sm:h-[320px] md:h-[430px] lg:h-[540px] object-cover"
/> />
); );
......
'use client'; 'use client';
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts"; import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Link from "next/link"; import Link from "next/link";
...@@ -48,8 +48,9 @@ function Events() { ...@@ -48,8 +48,9 @@ function Events() {
className="group flex h-full cursor-pointer flex-col overflow-hidden rounded-[14px] 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-[14px] bg-white text-[#20408f] shadow-[0_14px_28px_rgba(10,39,95,0.18)]"
> >
<div className="relative h-[180px] overflow-hidden md:h-[220px] xl:h-[248px]"> <div className="relative h-[180px] overflow-hidden md:h-[220px] xl:h-[248px]">
<Image <SafeImage
src={featuredEvent.thumbnail?.url ?? featuredFallback} src={featuredEvent.thumbnail?.url}
fallbackSrc={featuredFallback}
alt={featuredEvent.thumbnail?.alt || featuredEvent.title} alt={featuredEvent.thumbnail?.alt || featuredEvent.title}
width={720} width={720}
height={520} height={520}
...@@ -114,8 +115,9 @@ function Events() { ...@@ -114,8 +115,9 @@ function Events() {
className="group flex flex-1 cursor-pointer items-center gap-3 rounded-[14px] 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-[14px] 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-[14px]"> <div className="h-[64px] w-[64px] shrink-0 overflow-hidden rounded-[14px]">
<Image <SafeImage
src={item.thumbnail?.url ?? sideFallbacks[index]} src={item.thumbnail?.url}
fallbackSrc={sideFallbacks[index]}
alt={item.thumbnail?.alt || item.title} alt={item.thumbnail?.alt || item.title}
width={160} width={160}
height={160} height={160}
......
'use client'; 'use client';
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts"; import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
...@@ -51,8 +51,9 @@ function FeaturedNews() { ...@@ -51,8 +51,9 @@ function FeaturedNews() {
className="group relative block cursor-pointer overflow-hidden rounded-[16px] bg-[#0d2f5f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] md:rounded-[16px] md:min-h-[320px] lg:min-h-[380px]" className="group relative block cursor-pointer overflow-hidden rounded-[16px] bg-[#0d2f5f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] md:rounded-[16px] md:min-h-[320px] lg:min-h-[380px]"
> >
<div className="relative h-full min-h-[195px] md:min-h-[320px] lg:min-h-[380px]"> <div className="relative h-full min-h-[195px] md:min-h-[320px] lg:min-h-[380px]">
<Image <SafeImage
src={primaryItem.thumbnail?.url ?? primaryFallback} src={primaryItem.thumbnail?.url}
fallbackSrc={primaryFallback}
alt={primaryItem.thumbnail?.alt || primaryItem.title} alt={primaryItem.thumbnail?.alt || primaryItem.title}
width={1200} width={1200}
height={800} height={800}
...@@ -97,8 +98,9 @@ function FeaturedNews() { ...@@ -97,8 +98,9 @@ function FeaturedNews() {
className="group relative block cursor-pointer overflow-hidden rounded-[16px] bg-[#27447f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] min-h-[165px]" className="group relative block cursor-pointer overflow-hidden rounded-[16px] bg-[#27447f] shadow-[0_16px_32px_rgba(28,52,120,0.2)] min-h-[165px]"
> >
<div className="relative flex h-full min-h-[165px]"> <div className="relative flex h-full min-h-[165px]">
<Image <SafeImage
src={item.thumbnail?.url ?? secondaryFallbacks[index]} src={item.thumbnail?.url}
fallbackSrc={secondaryFallbacks[index]}
alt={item.thumbnail?.alt || item.title} alt={item.thumbnail?.alt || item.title}
width={600} width={600}
height={420} height={420}
......
'use client'; 'use client';
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements"; import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements";
import links from "@/links"; import links from "@/links";
...@@ -14,16 +13,8 @@ function HorizontalAdBanner() { ...@@ -14,16 +13,8 @@ function HorizontalAdBanner() {
const ad = ads[0]; const ad = ads[0];
const href = ad?.link || FALLBACK_HREF; const href = ad?.link || FALLBACK_HREF;
const initialSrc = ad?.file?.path ? links.resolveImageUrl(ad.file.path) : FALLBACK_SRC; const src = ad?.file?.path ? links.resolveImageUrl(ad.file.path) : FALLBACK_SRC;
const [src, setSrc] = useState(initialSrc);
const [errored, setErrored] = useState(false);
// Reset state khi ad thay đổi
if (ad?.file?.path && !errored && src !== initialSrc) {
setSrc(initialSrc);
}
const isGif = src.toLowerCase().endsWith(".gif");
const title = ad?.name || "Quảng cáo VCCI HCM"; const title = ad?.name || "Quảng cáo VCCI HCM";
const alt = ad?.alt || title; const alt = ad?.alt || title;
...@@ -36,19 +27,13 @@ function HorizontalAdBanner() { ...@@ -36,19 +27,13 @@ function HorizontalAdBanner() {
style={{ aspectRatio: "1600 / 200" }} style={{ aspectRatio: "1600 / 200" }}
title={title} title={title}
> >
<Image <SafeImage
src={src} src={src}
fallbackSrc={FALLBACK_SRC}
alt={alt} alt={alt}
fill fill
sizes="100vw" sizes="100vw"
className="object-cover" className="object-cover"
unoptimized={isGif}
onError={() => {
if (src !== FALLBACK_SRC) {
setSrc(FALLBACK_SRC);
setErrored(true);
}
}}
/> />
</Link> </Link>
); );
......
'use client'; 'use client';
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts"; import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import memberImages from "@/constants/memberImages"; import memberImages from "@/constants/memberImages";
import { MOCK_FEATURED_MEMBERS_RESPONSE } from "@/mockdata/bff-fallback"; import { MOCK_FEATURED_MEMBERS_RESPONSE } from "@/mockdata/bff-fallback";
...@@ -113,8 +113,9 @@ function Members() { ...@@ -113,8 +113,9 @@ function Members() {
> >
<div className="flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5"> <div className="flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5">
<div className="flex h-full w-full max-w-[260px] items-center justify-center"> <div className="flex h-full w-full max-w-[260px] items-center justify-center">
<Image <SafeImage
src={resolveMemberImage(member.avatar, index) ?? "/img-error.png"} src={resolveMemberImage(member.avatar, index)}
fallbackSrc="/img-error.png"
alt={member.name ?? ""} alt={member.name ?? ""}
width={260} width={260}
height={180} height={180}
...@@ -130,8 +131,9 @@ function Members() { ...@@ -130,8 +131,9 @@ function Members() {
<> <>
<div className="flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5"> <div className="flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5">
<div className="flex h-full w-full max-w-[260px] items-center justify-center"> <div className="flex h-full w-full max-w-[260px] items-center justify-center">
<Image <SafeImage
src={resolveMemberImage(member.avatar, index) ?? "/img-error.png"} src={resolveMemberImage(member.avatar, index)}
fallbackSrc="/img-error.png"
alt={member.name ?? ""} alt={member.name ?? ""}
width={260} width={260}
height={180} height={180}
...@@ -212,8 +214,9 @@ function Members() { ...@@ -212,8 +214,9 @@ function Members() {
className="group relative block cursor-pointer overflow-hidden rounded-[14px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]" className="group relative block cursor-pointer overflow-hidden rounded-[14px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
> >
<div className="aspect-[16/10] overflow-hidden xl:aspect-[1.25/1]"> <div className="aspect-[16/10] overflow-hidden xl:aspect-[1.25/1]">
<Image <SafeImage
src={item.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE} src={item.thumbnail?.url}
fallbackSrc={MEMBER_CONNECTION_FALLBACK_IMAGE}
alt={item.thumbnail?.alt || item.title} alt={item.thumbnail?.alt || item.title}
width={520} width={520}
height={420} height={420}
...@@ -254,8 +257,9 @@ function Members() { ...@@ -254,8 +257,9 @@ function Members() {
className="group relative block cursor-pointer overflow-hidden rounded-[14px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]" className="group relative block cursor-pointer overflow-hidden rounded-[14px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
> >
<div className="aspect-[16/10] overflow-hidden"> <div className="aspect-[16/10] overflow-hidden">
<Image <SafeImage
src={item.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE} src={item.thumbnail?.url}
fallbackSrc={MEMBER_CONNECTION_FALLBACK_IMAGE}
alt={item.thumbnail?.alt || item.title} alt={item.thumbnail?.alt || item.title}
width={520} width={520}
height={420} height={420}
......
'use client'; 'use client';
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts"; import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Link from "next/link"; import Link from "next/link";
...@@ -83,8 +83,9 @@ function News() { ...@@ -83,8 +83,9 @@ function News() {
className="group block cursor-pointer overflow-hidden rounded-[16px] border border-[#dbe4f2] bg-white shadow-[0_8px_24px_rgba(31,59,124,0.08)]" className="group block cursor-pointer overflow-hidden rounded-[16px] border border-[#dbe4f2] bg-white shadow-[0_8px_24px_rgba(31,59,124,0.08)]"
> >
<div className="relative aspect-[1.4/1] overflow-hidden"> <div className="relative aspect-[1.4/1] overflow-hidden">
<Image <SafeImage
src={featuredArticle.thumbnail?.url ?? "/thumbnail.png"} src={featuredArticle.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={featuredArticle.thumbnail?.alt || featuredArticle.title} alt={featuredArticle.thumbnail?.alt || featuredArticle.title}
width={720} width={720}
height={580} height={580}
...@@ -151,8 +152,9 @@ function News() { ...@@ -151,8 +152,9 @@ function News() {
className="group flex flex-1 cursor-pointer items-center gap-3 rounded-[16px] border border-[#dbe4f2] bg-white px-4 py-2.5 shadow-[0_8px_24px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_28px_rgba(31,59,124,0.12)]" className="group flex flex-1 cursor-pointer items-center gap-3 rounded-[16px] border border-[#dbe4f2] bg-white px-4 py-2.5 shadow-[0_8px_24px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_28px_rgba(31,59,124,0.12)]"
> >
<div className="h-20 w-20 shrink-0 overflow-hidden rounded-[14px]"> <div className="h-20 w-20 shrink-0 overflow-hidden rounded-[14px]">
<Image <SafeImage
src={news.thumbnail?.url ?? "/thumbnail.png"} src={news.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={news.thumbnail?.alt || news.title} alt={news.thumbnail?.alt || news.title}
width={160} width={160}
height={160} height={160}
...@@ -227,8 +229,9 @@ function News() { ...@@ -227,8 +229,9 @@ function News() {
className="group block cursor-pointer overflow-hidden rounded-[16px] border border-[#dbe4f2] bg-white shadow-[0_8px_24px_rgba(31,59,124,0.08)]" className="group block cursor-pointer overflow-hidden rounded-[16px] border border-[#dbe4f2] bg-white shadow-[0_8px_24px_rgba(31,59,124,0.08)]"
> >
<div className="relative aspect-[16/9] overflow-hidden"> <div className="relative aspect-[16/9] overflow-hidden">
<Image <SafeImage
src={featuredArticle.thumbnail?.url ?? "/thumbnail.png"} src={featuredArticle.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={featuredArticle.thumbnail?.alt || featuredArticle.title} alt={featuredArticle.thumbnail?.alt || featuredArticle.title}
width={720} width={720}
height={580} height={580}
...@@ -292,8 +295,9 @@ function News() { ...@@ -292,8 +295,9 @@ function News() {
className="group flex cursor-pointer items-center gap-3 rounded-[16px] border border-[#dbe4f2] bg-white px-4 py-2.5 shadow-[0_8px_24px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_28px_rgba(31,59,124,0.12)]" className="group flex cursor-pointer items-center gap-3 rounded-[16px] border border-[#dbe4f2] bg-white px-4 py-2.5 shadow-[0_8px_24px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_28px_rgba(31,59,124,0.12)]"
> >
<div className="h-16 w-16 shrink-0 overflow-hidden rounded-[14px]"> <div className="h-16 w-16 shrink-0 overflow-hidden rounded-[14px]">
<Image <SafeImage
src={news.thumbnail?.url ?? "/thumbnail.png"} src={news.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={news.thumbnail?.alt || news.title} alt={news.thumbnail?.alt || news.title}
width={160} width={160}
height={160} height={160}
......
'use client'; 'use client';
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link"; import Link from "next/link";
import { useMemo, useState } from "react"; import { useMemo } from "react";
import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements"; import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements";
import links from "@/links"; import links from "@/links";
import type { Advertisement } from "@/api/vcci-news/models/advertisement"; import type { Advertisement } from "@/api/vcci-news/models/advertisement";
...@@ -11,8 +11,7 @@ import { getFallbackImage } from "@/lib/utils/fallback-image"; ...@@ -11,8 +11,7 @@ import { getFallbackImage } from "@/lib/utils/fallback-image";
const FALLBACK_HREF = links.externalApiOrigin; const FALLBACK_HREF = links.externalApiOrigin;
function AdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: string }) { function AdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: string }) {
const initialSrc = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc; const src = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc;
const [src, setSrc] = useState(initialSrc);
return ( return (
<Link <Link
...@@ -23,16 +22,13 @@ function AdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: strin ...@@ -23,16 +22,13 @@ function AdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: strin
title={item.name} title={item.name}
> >
<div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]"> <div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]">
<Image <SafeImage
src={src} src={src}
fallbackSrc={fallbackSrc}
alt={item.alt || item.name} alt={item.alt || item.name}
width={2048} width={2048}
height={1365} height={1365}
className="h-full w-full object-cover object-[center_80%]" className="h-full w-full object-cover object-[center_80%]"
unoptimized
onError={() => {
if (src !== fallbackSrc) setSrc(fallbackSrc);
}}
/> />
</div> </div>
</Link> </Link>
...@@ -49,7 +45,7 @@ function FallbackAdItem({ src }: { src: string }) { ...@@ -49,7 +45,7 @@ function FallbackAdItem({ src }: { src: string }) {
title="Quảng cáo VCCI HCM" title="Quảng cáo VCCI HCM"
> >
<div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]"> <div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]">
<Image <SafeImage
src={src} src={src}
alt="Quảng cáo VCCI HCM" alt="Quảng cáo VCCI HCM"
width={2048} width={2048}
......
"use client"; "use client";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import partnerImages from "@/constants/partnerImages"; import partnerImages from "@/constants/partnerImages";
import { ChevronRight, Play } from "lucide-react"; import { ChevronRight, Play } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
...@@ -68,8 +68,9 @@ const renderPartnerContent = (partners: Organization[]) => { ...@@ -68,8 +68,9 @@ const renderPartnerContent = (partners: Organization[]) => {
className="block" className="block"
> >
<div className="flex h-[96px] items-center justify-center rounded-[16px] 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]"> <div className="flex h-[96px] items-center justify-center rounded-[16px] 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]">
<Image <SafeImage
src={resolvePartnerImage(partner.avatar, index) ?? "/img-error.png"} src={resolvePartnerImage(partner.avatar, index)}
fallbackSrc="/img-error.png"
alt={partner.name ?? ""} alt={partner.name ?? ""}
width={140} width={140}
height={72} height={72}
...@@ -79,8 +80,9 @@ const renderPartnerContent = (partners: Organization[]) => { ...@@ -79,8 +80,9 @@ const renderPartnerContent = (partners: Organization[]) => {
</a> </a>
) : ( ) : (
<div className="flex h-[96px] items-center justify-center rounded-[16px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-[151px]"> <div className="flex h-[96px] items-center justify-center rounded-[16px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-[151px]">
<Image <SafeImage
src={resolvePartnerImage(partner.avatar, index) ?? "/img-error.png"} src={resolvePartnerImage(partner.avatar, index)}
fallbackSrc="/img-error.png"
alt={partner.name ?? ""} alt={partner.name ?? ""}
width={140} width={140}
height={72} height={72}
...@@ -179,7 +181,7 @@ function VideoAndPartners() { ...@@ -179,7 +181,7 @@ function VideoAndPartners() {
className="overflow-hidden rounded-[16px] border border-[#e5ebf4] bg-white shadow-[0_10px_22px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_16px_30px_rgba(31,59,124,0.12)]" className="overflow-hidden rounded-[16px] border border-[#e5ebf4] bg-white shadow-[0_10px_22px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_16px_30px_rgba(31,59,124,0.12)]"
> >
<div className="group relative aspect-[1.95/1] overflow-hidden"> <div className="group relative aspect-[1.95/1] overflow-hidden">
<Image <SafeImage
src={video.thumbnail} src={video.thumbnail}
alt={video.name} alt={video.name}
width={640} width={640}
......
...@@ -5,7 +5,7 @@ import Link from "next/link"; ...@@ -5,7 +5,7 @@ import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Spinner } from "@/components/ui"; import { Spinner } from "@/components/ui";
import { Pagination } from "@/components/base/pagination"; import { Pagination } from "@/components/base/pagination";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category"; import ListCategory from "@/components/base/list-category";
...@@ -155,7 +155,7 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp ...@@ -155,7 +155,7 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
className="group grid items-center gap-5 sm:grid-cols-[250px_minmax(0,1fr)]" className="group grid items-center gap-5 sm:grid-cols-[250px_minmax(0,1fr)]"
> >
<div className="relative overflow-hidden rounded-md bg-[#edf1f5] aspect-[25/15] sm:aspect-[5/3]"> <div className="relative overflow-hidden rounded-md bg-[#edf1f5] aspect-[25/15] sm:aspect-[5/3]">
<Image <SafeImage
src={resolveDynamicPostImage(item.thumbnail)} src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title} alt={item.title}
width={520} width={520}
......
...@@ -5,7 +5,7 @@ import Link from "next/link"; ...@@ -5,7 +5,7 @@ import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Spinner } from "@/components/ui"; import { Spinner } from "@/components/ui";
import { Pagination } from "@/components/base/pagination"; import { Pagination } from "@/components/base/pagination";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category"; import ListCategory from "@/components/base/list-category";
...@@ -108,7 +108,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp ...@@ -108,7 +108,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
> >
<div className="overflow-hidden bg-white shadow-[0_10px_24px_rgba(17,24,39,0.08)]"> <div className="overflow-hidden bg-white shadow-[0_10px_24px_rgba(17,24,39,0.08)]">
<div className="relative aspect-3/4 overflow-hidden bg-white"> <div className="relative aspect-3/4 overflow-hidden bg-white">
<Image <SafeImage
src={resolveDynamicPostImage(item.thumbnail)} src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title} alt={item.title}
width={520} width={520}
......
"use client"; "use client";
import parse from "html-react-parser"; import parse from "html-react-parser";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { getDynamicPostBodyHtml } from "./data"; import { getDynamicPostBodyHtml } from "./data";
import type { DynamicPostContentSection, DynamicPostItem } from "./types"; import type { DynamicPostContentSection, DynamicPostItem } from "./types";
...@@ -32,7 +32,7 @@ function StructuredImageSection({ section }: { section: DynamicPostContentSectio ...@@ -32,7 +32,7 @@ function StructuredImageSection({ section }: { section: DynamicPostContentSectio
key={`${section.id}-${image.id || image.url}-${item.position}`} key={`${section.id}-${image.id || image.url}-${item.position}`}
className="overflow-hidden rounded-[18px] bg-white" className="overflow-hidden rounded-[18px] bg-white"
> >
<Image <SafeImage
src={image.url} src={image.url}
alt={image.alt || image.name || "Hình ảnh bài viết"} alt={image.alt || image.name || "Hình ảnh bài viết"}
width={1200} width={1200}
......
...@@ -6,7 +6,7 @@ import { ShieldCheck, Target, Zap } from "lucide-react"; ...@@ -6,7 +6,7 @@ import { ShieldCheck, Target, Zap } from "lucide-react";
import parse from "html-react-parser"; import parse from "html-react-parser";
import Link from "next/link"; import Link from "next/link";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post"; import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { buildDynamicPostHref, buildVisibleNewsFilters, stripHtml } from "../data"; import { buildDynamicPostHref, buildVisibleNewsFilters, stripHtml } from "../data";
import links from "@/links"; import links from "@/links";
import StructuredPostContent from "../StructuredPostContent"; import StructuredPostContent from "../StructuredPostContent";
...@@ -325,7 +325,7 @@ export default function AboutVcciHcmPage({ ...@@ -325,7 +325,7 @@ export default function AboutVcciHcmPage({
className="group overflow-hidden rounded-[22px] bg-white shadow-[0_18px_38px_rgba(28,52,120,0.16)] transition-transform hover:-translate-y-1" className="group overflow-hidden rounded-[22px] bg-white shadow-[0_18px_38px_rgba(28,52,120,0.16)] transition-transform hover:-translate-y-1"
> >
<div className="relative aspect-[1.28] overflow-hidden"> <div className="relative aspect-[1.28] overflow-hidden">
<Image <SafeImage
src={item.thumbnailUrl} src={item.thumbnailUrl}
alt={item.thumbnailAlt} alt={item.thumbnailAlt}
width={720} width={720}
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { FileText, Globe2, Newspaper, TrendingUp } from "lucide-react"; import { FileText, Globe2, Newspaper, TrendingUp } from "lucide-react";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import type { DynamicPostItem } from "../types"; import type { DynamicPostItem } from "../types";
type MarketProfilePageProps = { type MarketProfilePageProps = {
...@@ -256,7 +256,7 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) { ...@@ -256,7 +256,7 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
})} })}
</div> </div>
<Image <SafeImage
src={activeRegion.image} src={activeRegion.image}
alt={activeRegion.imageAlt} alt={activeRegion.imageAlt}
width={1200} width={1200}
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { Suspense, useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import SidebarAdvertisements from "@/components/shared/sidebar-advertisements"; import SidebarAdvertisements from "@/components/shared/sidebar-advertisements";
import { Pagination } from "@components/base/pagination"; import { Pagination } from "@components/base/pagination";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
...@@ -54,7 +54,7 @@ function SearchResultItem({ item, index }: { item: DynamicPostItem; index: numbe ...@@ -54,7 +54,7 @@ function SearchResultItem({ item, index }: { item: DynamicPostItem; index: numbe
className="group grid gap-5 sm:grid-cols-[250px_minmax(0,1fr)]" className="group grid gap-5 sm:grid-cols-[250px_minmax(0,1fr)]"
> >
<div className="overflow-hidden rounded-md bg-[#edf1f5]"> <div className="overflow-hidden rounded-md bg-[#edf1f5]">
<Image <SafeImage
src={resolveDynamicPostImage(item.thumbnail)} src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title} alt={item.title}
width={520} width={520}
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Play } from "lucide-react"; import { Play } from "lucide-react";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import { Pagination } from "@/components/base/pagination"; import { Pagination } from "@/components/base/pagination";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { useGetApiV10Video } from "@/api/vcci-news/endpoints/video"; import { useGetApiV10Video } from "@/api/vcci-news/endpoints/video";
...@@ -106,7 +106,7 @@ function VideoPageContent() { ...@@ -106,7 +106,7 @@ function VideoPageContent() {
className="group overflow-hidden rounded-[18px] border border-[#e5ebf4] bg-white shadow-[0_12px_30px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_18px_38px_rgba(31,59,124,0.14)]" className="group overflow-hidden rounded-[18px] border border-[#e5ebf4] bg-white shadow-[0_12px_30px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_18px_38px_rgba(31,59,124,0.14)]"
> >
<div className="relative aspect-video overflow-hidden bg-[#edf1f5]"> <div className="relative aspect-video overflow-hidden bg-[#edf1f5]">
<Image <SafeImage
src={video.thumbnail} src={video.thumbnail}
alt={video.name} alt={video.name}
width={900} width={900}
......
...@@ -45,7 +45,7 @@ import { ...@@ -45,7 +45,7 @@ import {
import { PermissionGate } from "@/components/shared/permission-gate"; import { PermissionGate } from "@/components/shared/permission-gate";
import { usePermission } from "@/hooks/usePermission"; import { usePermission } from "@/hooks/usePermission";
import { AdminImagePicker } from "@/components/admin/image-picker"; import { AdminImagePicker } from "@/components/admin/image-picker";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import type { AdminMediaItem } from "@/mockdata/admin-news"; import type { AdminMediaItem } from "@/mockdata/admin-news";
import links from "@/links"; import links from "@/links";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
...@@ -369,7 +369,7 @@ export function AdvertisementList({ ...@@ -369,7 +369,7 @@ export function AdvertisementList({
style={{ aspectRatio: previewAspect, width: type === "horizontal" ? 112 : 72 }} style={{ aspectRatio: previewAspect, width: type === "horizontal" ? 112 : 72 }}
> >
{imageUrl ? ( {imageUrl ? (
<SafeNextImage <SafeImage
src={imageUrl} src={imageUrl}
alt={ad.alt || ad.name} alt={ad.alt || ad.name}
fill fill
...@@ -514,7 +514,7 @@ export function AdvertisementList({ ...@@ -514,7 +514,7 @@ export function AdvertisementList({
style={{ aspectRatio: previewAspect, width: type === "horizontal" ? 200 : 120 }} style={{ aspectRatio: previewAspect, width: type === "horizontal" ? 200 : 120 }}
> >
{form.filePreviewUrl ? ( {form.filePreviewUrl ? (
<SafeNextImage <SafeImage
src={form.filePreviewUrl} src={form.filePreviewUrl}
alt="Preview" alt="Preview"
fill fill
......
"use client"; "use client";
import { ImagePlus, Save } from "lucide-react"; import { ImagePlus, Save } from "lucide-react";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
...@@ -114,7 +114,7 @@ export function ConfigItemDialog({ ...@@ -114,7 +114,7 @@ export function ConfigItemDialog({
<div className="overflow-hidden rounded-3xl border border-dashed border-[#063e8e]/20 bg-[#eef4ff]/60"> <div className="overflow-hidden rounded-3xl border border-dashed border-[#063e8e]/20 bg-[#eef4ff]/60">
<div className="relative aspect-[16/9]"> <div className="relative aspect-[16/9]">
{previewMedia ? ( {previewMedia ? (
<SafeNextImage <SafeImage
src={previewMedia.url} src={previewMedia.url}
alt={previewMedia.alt || previewMedia.name} alt={previewMedia.alt || previewMedia.name}
fill fill
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import type { BaseConfigBannerItem } from "@/mockdata/base-config"; import type { BaseConfigBannerItem } from "@/mockdata/base-config";
import type { AdminMediaItem } from "@/mockdata/admin-news"; import type { AdminMediaItem } from "@/mockdata/admin-news";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
export function ConfigItemPreview({ export function ConfigItemPreview({
title, title,
...@@ -28,7 +28,7 @@ export function ConfigItemPreview({ ...@@ -28,7 +28,7 @@ export function ConfigItemPreview({
> >
<div className="relative aspect-[16/10] overflow-hidden bg-[#eef4ff]"> <div className="relative aspect-[16/10] overflow-hidden bg-[#eef4ff]">
{media ? ( {media ? (
<SafeNextImage <SafeImage
src={media.url} src={media.url}
alt={media.alt || media.name} alt={media.alt || media.name}
fill fill
......
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog"; import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
import { AdminImagePicker } from "@/components/admin/image-picker"; import { AdminImagePicker } from "@/components/admin/image-picker";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -845,7 +845,7 @@ export default function AdminBaseConfigPage() { ...@@ -845,7 +845,7 @@ export default function AdminBaseConfigPage() {
<div className="relative flex min-h-[320px] items-center justify-center overflow-hidden rounded-[24px] border border-dashed border-[#063e8e]/18 bg-[#eef4ff]"> <div className="relative flex min-h-[320px] items-center justify-center overflow-hidden rounded-[24px] border border-dashed border-[#063e8e]/18 bg-[#eef4ff]">
{currentLogoMedia ? ( {currentLogoMedia ? (
<div className="relative h-[220px] w-[220px]"> <div className="relative h-[220px] w-[220px]">
<SafeNextImage <SafeImage
src={currentLogoMedia.url} src={currentLogoMedia.url}
alt={currentLogoMedia.alt || currentLogoMedia.name} alt={currentLogoMedia.alt || currentLogoMedia.name}
fill fill
...@@ -993,7 +993,7 @@ export default function AdminBaseConfigPage() { ...@@ -993,7 +993,7 @@ export default function AdminBaseConfigPage() {
<div className="rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5"> <div className="rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5">
<div className="relative aspect-[16/6] overflow-hidden rounded-[24px] border border-[#063e8e]/12 bg-[#eef4ff]"> <div className="relative aspect-[16/6] overflow-hidden rounded-[24px] border border-[#063e8e]/12 bg-[#eef4ff]">
{currentBannerMedia ? ( {currentBannerMedia ? (
<SafeNextImage <SafeImage
src={currentBannerMedia.url} src={currentBannerMedia.url}
alt={currentBannerMedia.alt || currentBannerMedia.name} alt={currentBannerMedia.alt || currentBannerMedia.name}
fill fill
......
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
Sparkles, Sparkles,
Users, Users,
} from "lucide-react"; } from "lucide-react";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
...@@ -439,7 +439,7 @@ export default function AdminDashboardPage() { ...@@ -439,7 +439,7 @@ export default function AdminDashboardPage() {
> >
<div className="relative h-20 w-28 shrink-0 overflow-hidden rounded-2xl bg-[#eef4ff]"> <div className="relative h-20 w-28 shrink-0 overflow-hidden rounded-2xl bg-[#eef4ff]">
{item.thumbnail ? ( {item.thumbnail ? (
<SafeNextImage <SafeImage
src={item.thumbnail.url} src={item.thumbnail.url}
alt={item.thumbnail.alt || item.thumbnail.name} alt={item.thumbnail.alt || item.thumbnail.name}
fill fill
...@@ -493,7 +493,7 @@ export default function AdminDashboardPage() { ...@@ -493,7 +493,7 @@ export default function AdminDashboardPage() {
<div className="mt-4 grid grid-cols-3 gap-3"> <div className="mt-4 grid grid-cols-3 gap-3">
{mediaItems.slice(0, 3).map((item) => ( {mediaItems.slice(0, 3).map((item) => (
<div key={item.id} className="relative aspect-square overflow-hidden rounded-2xl bg-[#eef4ff]"> <div key={item.id} className="relative aspect-square overflow-hidden rounded-2xl bg-[#eef4ff]">
<SafeNextImage <SafeImage
src={item.url} src={item.url}
alt={item.alt || item.name} alt={item.alt || item.name}
fill fill
......
...@@ -15,7 +15,7 @@ import { AdminRowActions } from "@/components/admin/admin-row-actions"; ...@@ -15,7 +15,7 @@ import { AdminRowActions } from "@/components/admin/admin-row-actions";
import { AdminStatsGrid } from "@/components/admin/admin-stats-grid"; import { AdminStatsGrid } from "@/components/admin/admin-stats-grid";
import { AdminTableLayout } from "@/components/admin/admin-table-layout"; import { AdminTableLayout } from "@/components/admin/admin-table-layout";
import { Pagination } from "@/components/base/pagination"; import { Pagination } from "@/components/base/pagination";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -315,7 +315,7 @@ export default function HeaderCategoryPostsPage() { ...@@ -315,7 +315,7 @@ export default function HeaderCategoryPostsPage() {
<TableCell className="text-center"> <TableCell className="text-center">
<div className="relative mx-auto h-16 w-24 overflow-hidden rounded-xl border border-[#063e8e]/15 bg-[#063e8e]/[0.03]"> <div className="relative mx-auto h-16 w-24 overflow-hidden rounded-xl border border-[#063e8e]/15 bg-[#063e8e]/[0.03]">
{item.thumbnail ? ( {item.thumbnail ? (
<SafeNextImage <SafeImage
src={item.thumbnail.url} src={item.thumbnail.url}
alt={item.thumbnail.alt || item.thumbnail.name} alt={item.thumbnail.alt || item.thumbnail.name}
fill fill
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
X, X,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
...@@ -103,7 +103,7 @@ export function MediaFormDialog({ ...@@ -103,7 +103,7 @@ export function MediaFormDialog({
<div className="overflow-hidden rounded-[28px] border border-[#063e8e]/10 bg-white shadow-[inset_0_1px_0_rgba(255,255,255,0.8)]"> <div className="overflow-hidden rounded-[28px] border border-[#063e8e]/10 bg-white shadow-[inset_0_1px_0_rgba(255,255,255,0.8)]">
<div className="relative aspect-[16/10] bg-[radial-gradient(circle_at_top,#d9e8ff_0%,#f7faff_58%,#ffffff_100%)]"> <div className="relative aspect-[16/10] bg-[radial-gradient(circle_at_top,#d9e8ff_0%,#f7faff_58%,#ffffff_100%)]">
{form.previewUrl ? ( {form.previewUrl ? (
<SafeNextImage <SafeImage
src={form.previewUrl} src={form.previewUrl}
alt={form.name} alt={form.name}
fill fill
......
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog"; import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
import { AdminTableLayout } from "@/components/admin/admin-table-layout"; import { AdminTableLayout } from "@/components/admin/admin-table-layout";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Pagination } from "@/components/base/pagination"; import { Pagination } from "@/components/base/pagination";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -161,7 +161,7 @@ export default function AdminMediaPage() { ...@@ -161,7 +161,7 @@ export default function AdminMediaPage() {
className="group overflow-hidden rounded-[28px] border border-[#063e8e]/10 bg-white shadow-[0_18px_45px_rgba(6,62,142,0.08)] transition duration-300 hover:-translate-y-1 hover:shadow-[0_28px_60px_rgba(6,62,142,0.14)]" className="group overflow-hidden rounded-[28px] border border-[#063e8e]/10 bg-white shadow-[0_18px_45px_rgba(6,62,142,0.08)] transition duration-300 hover:-translate-y-1 hover:shadow-[0_28px_60px_rgba(6,62,142,0.14)]"
> >
<div className="relative aspect-square overflow-hidden bg-[radial-gradient(circle_at_top,#dce9ff_0%,#f8fbff_55%,#ffffff_100%)]"> <div className="relative aspect-square overflow-hidden bg-[radial-gradient(circle_at_top,#dce9ff_0%,#f8fbff_55%,#ffffff_100%)]">
<SafeNextImage <SafeImage
src={resolveCmsFileUrl(item.path)} src={resolveCmsFileUrl(item.path)}
alt={item.original ?? ""} alt={item.original ?? ""}
fill fill
......
...@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation"; ...@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import { AdminImagePicker } from "@/components/admin/image-picker"; import { AdminImagePicker } from "@/components/admin/image-picker";
import { AdminPostContentEditor } from "@/components/admin/post-content-editor"; import { AdminPostContentEditor } from "@/components/admin/post-content-editor";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
...@@ -351,7 +351,7 @@ export function AdminMemberForm({ memberId }: AdminMemberFormProps) { ...@@ -351,7 +351,7 @@ export function AdminMemberForm({ memberId }: AdminMemberFormProps) {
{form.image ? ( {form.image ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="relative aspect-video overflow-hidden rounded-xl border border-[#063e8e]/15"> <div className="relative aspect-video overflow-hidden rounded-xl border border-[#063e8e]/15">
<SafeNextImage <SafeImage
src={form.image.url} src={form.image.url}
alt={form.image.alt || form.image.name} alt={form.image.alt || form.image.name}
fill fill
......
"use client"; "use client";
import { AdminRowActions } from "@/components/admin/admin-row-actions"; import { AdminRowActions } from "@/components/admin/admin-row-actions";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
TableCell, TableCell,
...@@ -47,7 +47,7 @@ export function MemberRow({ ...@@ -47,7 +47,7 @@ export function MemberRow({
<TableCell className="px-4 py-3 text-center"> <TableCell className="px-4 py-3 text-center">
{item.image ? ( {item.image ? (
<div className="mx-auto h-12 w-16 overflow-hidden rounded-lg border border-[#063e8e]/15"> <div className="mx-auto h-12 w-16 overflow-hidden rounded-lg border border-[#063e8e]/15">
<SafeNextImage <SafeImage
src={item.image.url} src={item.image.url}
alt={item.image.alt || item.name} alt={item.image.alt || item.name}
width={64} width={64}
......
...@@ -15,7 +15,7 @@ import { AdminImagePicker } from "@/components/admin/image-picker"; ...@@ -15,7 +15,7 @@ import { AdminImagePicker } from "@/components/admin/image-picker";
import { AdminPostContentEditor } from "@/components/admin/post-content-editor"; import { AdminPostContentEditor } from "@/components/admin/post-content-editor";
import { PostHistoryViewer } from "./post-history-viewer"; import { PostHistoryViewer } from "./post-history-viewer";
import { AdminRichTextEditor } from "@/components/shared/rich-text-editor"; import { AdminRichTextEditor } from "@/components/shared/rich-text-editor";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { PermissionGate } from "@/components/shared/permission-gate"; import { PermissionGate } from "@/components/shared/permission-gate";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
...@@ -530,7 +530,7 @@ export function AdminNewsFormContent() { ...@@ -530,7 +530,7 @@ export function AdminNewsFormContent() {
<div className="relative overflow-hidden rounded-2xl border border-[#063e8e]/15 bg-white"> <div className="relative overflow-hidden rounded-2xl border border-[#063e8e]/15 bg-white">
<div className="relative aspect-[16/11]"> <div className="relative aspect-[16/11]">
{form.thumbnail ? ( {form.thumbnail ? (
<SafeNextImage <SafeImage
src={form.thumbnail.url} src={form.thumbnail.url}
alt={form.thumbnail.alt || form.thumbnail.name} alt={form.thumbnail.alt || form.thumbnail.name}
fill fill
......
...@@ -15,7 +15,7 @@ import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog"; ...@@ -15,7 +15,7 @@ import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
import { AdminRowActions } from "@/components/admin/admin-row-actions"; import { AdminRowActions } from "@/components/admin/admin-row-actions";
import { AdminStatsGrid } from "@/components/admin/admin-stats-grid"; import { AdminStatsGrid } from "@/components/admin/admin-stats-grid";
import { AdminTableLayout } from "@/components/admin/admin-table-layout"; import { AdminTableLayout } from "@/components/admin/admin-table-layout";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
...@@ -449,7 +449,7 @@ export default function AdminNewsPage() { ...@@ -449,7 +449,7 @@ export default function AdminNewsPage() {
<TableCell className="text-center"> <TableCell className="text-center">
<div className="relative mx-auto h-16 w-24 overflow-hidden rounded-xl border border-[#063e8e]/15 bg-[#063e8e]/[0.03]"> <div className="relative mx-auto h-16 w-24 overflow-hidden rounded-xl border border-[#063e8e]/15 bg-[#063e8e]/[0.03]">
{item.thumbnail ? ( {item.thumbnail ? (
<SafeNextImage <SafeImage
src={item.thumbnail.url} src={item.thumbnail.url}
alt={item.thumbnail.alt || item.thumbnail.name} alt={item.thumbnail.alt || item.thumbnail.name}
fill fill
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import type { AdminMediaItem } from "@/mockdata/admin-news"; import type { AdminMediaItem } from "@/mockdata/admin-news";
import { toAdminMediaItem } from "@/lib/utils/file"; import { toAdminMediaItem } from "@/lib/utils/file";
import { getApiV10File, postApiV10FileUpload } from "@/api/vcci-news/endpoints/file"; import { getApiV10File, postApiV10FileUpload } from "@/api/vcci-news/endpoints/file";
...@@ -210,7 +210,7 @@ export function AdminImagePicker({ ...@@ -210,7 +210,7 @@ export function AdminImagePicker({
)} )}
> >
<div className="relative aspect-[4/3] overflow-hidden bg-[#063e8e]/[0.04]"> <div className="relative aspect-[4/3] overflow-hidden bg-[#063e8e]/[0.04]">
<SafeNextImage <SafeImage
src={item.url} src={item.url}
alt={item.alt || item.name} alt={item.alt || item.name}
fill fill
......
...@@ -4,7 +4,7 @@ import * as React from "react"; ...@@ -4,7 +4,7 @@ import * as React from "react";
import { Image as ImageIcon, Pencil, Plus, Type, Upload, X } from "lucide-react"; import { Image as ImageIcon, Pencil, Plus, Type, Upload, X } from "lucide-react";
import { AdminImagePicker } from "@/components/admin/image-picker"; import { AdminImagePicker } from "@/components/admin/image-picker";
import { AdminRichTextEditor } from "@/components/shared/rich-text-editor"; import { AdminRichTextEditor } from "@/components/shared/rich-text-editor";
import { SafeNextImage } from "@/components/admin/safe-next-image"; import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
...@@ -409,7 +409,7 @@ export function AdminPostContentEditor({ ...@@ -409,7 +409,7 @@ export function AdminPostContentEditor({
}} }}
className="relative h-64 w-full cursor-pointer" className="relative h-64 w-full cursor-pointer"
> >
<SafeNextImage <SafeImage
src={currentImage.image.url} src={currentImage.image.url}
alt={currentImage.image.alt || currentImage.image.name} alt={currentImage.image.alt || currentImage.image.name}
fill fill
......
...@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; ...@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Facebook, Linkedin, Menu, Twitter, X, Youtube } from "lucide-react"; import { Facebook, Linkedin, Menu, Twitter, X, Youtube } from "lucide-react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link"; import Link from "next/link";
const fallbackLogo = "/logo.png"; const fallbackLogo = "/logo.png";
import { useGetApiV10Logo } from "@/api/vcci-news/endpoints/logo"; import { useGetApiV10Logo } from "@/api/vcci-news/endpoints/logo";
...@@ -335,18 +335,14 @@ function Header() { ...@@ -335,18 +335,14 @@ function Header() {
href="/" href="/"
className="flex w-[136px] shrink-0 items-center xl:w-[152px]" className="flex w-[136px] shrink-0 items-center xl:w-[152px]"
> >
<Image <SafeImage
width={108} width={108}
height={40} height={40}
className="h-auto max-h-10 w-[108px] object-contain" className="h-auto max-h-10 w-[108px] object-contain"
src={currentLogo?.logo_url ? links.resolveImageUrl(currentLogo.logo_url) : fallbackLogo} src={currentLogo?.logo_url ? links.resolveImageUrl(currentLogo.logo_url) : fallbackLogo}
fallbackSrc={fallbackLogo}
alt={currentLogo?.logo_name || "VCCI-HCM"} alt={currentLogo?.logo_name || "VCCI-HCM"}
priority priority
unoptimized={Boolean(currentLogo?.logo_url)}
onError={(e) => {
const img = e.currentTarget;
if (img.src !== fallbackLogo) img.src = fallbackLogo;
}}
/> />
</Link> </Link>
...@@ -394,18 +390,14 @@ function Header() { ...@@ -394,18 +390,14 @@ function Header() {
className="flex w-[136px] shrink-0 items-center" className="flex w-[136px] shrink-0 items-center"
onClick={() => setToggleMenu(false)} onClick={() => setToggleMenu(false)}
> >
<Image <SafeImage
width={108} width={108}
height={40} height={40}
className="h-auto max-h-10 w-[108px] object-contain" className="h-auto max-h-10 w-[108px] object-contain"
src={currentLogo?.logo_url ? links.resolveImageUrl(currentLogo.logo_url) : fallbackLogo} src={currentLogo?.logo_url ? links.resolveImageUrl(currentLogo.logo_url) : fallbackLogo}
fallbackSrc={fallbackLogo}
alt={currentLogo?.logo_name || "VCCI-HCM"} alt={currentLogo?.logo_name || "VCCI-HCM"}
priority priority
unoptimized={Boolean(currentLogo?.logo_url)}
onError={(e) => {
const img = e.currentTarget;
if (img.src !== fallbackLogo) img.src = fallbackLogo;
}}
/> />
</Link> </Link>
<button <button
......
"use client"; "use client";
import Image, { type ImageProps } from "next/image"; import Image, { type ImageProps } from "next/image";
import * as React from "react"; import { useEffect, useState } from "react";
interface SafeNextImageProps extends Omit<ImageProps, "src"> { interface SafeImageProps extends Omit<ImageProps, "src"> {
src?: string | null; src?: string | null;
fallbackSrc?: string; fallbackSrc?: string;
} }
export function SafeNextImage({ export function SafeImage({
src, src,
alt, alt,
fallbackSrc = "/img-error.png", fallbackSrc = "/img-error.png",
...props ...props
}: SafeNextImageProps) { }: SafeImageProps) {
const [currentSrc, setCurrentSrc] = React.useState(src || fallbackSrc); const [currentSrc, setCurrentSrc] = useState(src || fallbackSrc);
const [hasFailed, setHasFailed] = React.useState(false); const [hasFailed, setHasFailed] = useState(false);
React.useEffect(() => { useEffect(() => {
setCurrentSrc(src || fallbackSrc); setCurrentSrc(src || fallbackSrc);
setHasFailed(false); setHasFailed(false);
}, [fallbackSrc, src]); }, [fallbackSrc, src]);
......
'use client'; 'use client';
import Image from "next/image"; import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link"; import Link from "next/link";
import { useMemo, useState } from "react"; import { useMemo } from "react";
import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements"; import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements";
import links from "@/links"; import links from "@/links";
import type { Advertisement } from "@/api/vcci-news/models/advertisement"; import type { Advertisement } from "@/api/vcci-news/models/advertisement";
...@@ -11,8 +11,7 @@ import { getFallbackImage } from "@/lib/utils/fallback-image"; ...@@ -11,8 +11,7 @@ import { getFallbackImage } from "@/lib/utils/fallback-image";
const FALLBACK_HREF = "https://vcci-hcm.org.vn"; const FALLBACK_HREF = "https://vcci-hcm.org.vn";
function SidebarAdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: string }) { function SidebarAdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: string }) {
const initialSrc = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc; const src = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc;
const [src, setSrc] = useState(initialSrc);
return ( return (
<Link <Link
...@@ -23,15 +22,13 @@ function SidebarAdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc ...@@ -23,15 +22,13 @@ function SidebarAdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc
title={item.name} title={item.name}
> >
<div className="relative aspect-[16/10]"> <div className="relative aspect-[16/10]">
<Image <SafeImage
src={src} src={src}
fallbackSrc={fallbackSrc}
alt={item.alt || item.name} alt={item.alt || item.name}
fill fill
sizes="(max-width: 768px) 100vw, 300px" sizes="(max-width: 768px) 100vw, 300px"
className="h-full w-full object-cover" className="h-full w-full object-cover"
onError={() => {
if (src !== fallbackSrc) setSrc(fallbackSrc);
}}
/> />
</div> </div>
</Link> </Link>
...@@ -48,7 +45,7 @@ function FallbackSidebarAdItem({ src }: { src: string }) { ...@@ -48,7 +45,7 @@ function FallbackSidebarAdItem({ src }: { src: string }) {
title="Quảng cáo VCCI HCM" title="Quảng cáo VCCI HCM"
> >
<div className="relative aspect-[16/10]"> <div className="relative aspect-[16/10]">
<Image <SafeImage
src={src} src={src}
alt="Quảng cáo VCCI HCM" alt="Quảng cáo VCCI HCM"
fill fill
......
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