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

fixL change import image next optimize to components next unoptimize

parent 54b553bc
"use client";
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay } from "swiper/modules";
import { Swiper as SwiperType } from "swiper/types";
......@@ -84,18 +84,13 @@ function BannerSlideItem({
: "/thumbnail.png";
return (
<Image
<SafeImage
src={url}
fallbackSrc="/thumbnail.png"
alt={alt}
width={2560}
height={720}
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"
/>
);
......
'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 dayjs from "dayjs";
import Link from "next/link";
......@@ -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)]"
>
<div className="relative h-[180px] overflow-hidden md:h-[220px] xl:h-[248px]">
<Image
src={featuredEvent.thumbnail?.url ?? featuredFallback}
<SafeImage
src={featuredEvent.thumbnail?.url}
fallbackSrc={featuredFallback}
alt={featuredEvent.thumbnail?.alt || featuredEvent.title}
width={720}
height={520}
......@@ -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"
>
<div className="h-[64px] w-[64px] shrink-0 overflow-hidden rounded-[14px]">
<Image
src={item.thumbnail?.url ?? sideFallbacks[index]}
<SafeImage
src={item.thumbnail?.url}
fallbackSrc={sideFallbacks[index]}
alt={item.thumbnail?.alt || item.title}
width={160}
height={160}
......
'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 dayjs from "dayjs";
import { ChevronRight } from "lucide-react";
......@@ -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]"
>
<div className="relative h-full min-h-[195px] md:min-h-[320px] lg:min-h-[380px]">
<Image
src={primaryItem.thumbnail?.url ?? primaryFallback}
<SafeImage
src={primaryItem.thumbnail?.url}
fallbackSrc={primaryFallback}
alt={primaryItem.thumbnail?.alt || primaryItem.title}
width={1200}
height={800}
......@@ -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]"
>
<div className="relative flex h-full min-h-[165px]">
<Image
src={item.thumbnail?.url ?? secondaryFallbacks[index]}
<SafeImage
src={item.thumbnail?.url}
fallbackSrc={secondaryFallbacks[index]}
alt={item.thumbnail?.alt || item.title}
width={600}
height={420}
......
'use client';
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link";
import { useState } from "react";
import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements";
import links from "@/links";
......@@ -14,16 +13,8 @@ function HorizontalAdBanner() {
const ad = ads[0];
const href = ad?.link || FALLBACK_HREF;
const initialSrc = ad?.file?.path ? links.resolveImageUrl(ad.file.path) : FALLBACK_SRC;
const [src, setSrc] = useState(initialSrc);
const [errored, setErrored] = useState(false);
const src = ad?.file?.path ? links.resolveImageUrl(ad.file.path) : FALLBACK_SRC;
// 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 alt = ad?.alt || title;
......@@ -36,19 +27,13 @@ function HorizontalAdBanner() {
style={{ aspectRatio: "1600 / 200" }}
title={title}
>
<Image
<SafeImage
src={src}
fallbackSrc={FALLBACK_SRC}
alt={alt}
fill
sizes="100vw"
className="object-cover"
unoptimized={isGif}
onError={() => {
if (src !== FALLBACK_SRC) {
setSrc(FALLBACK_SRC);
setErrored(true);
}
}}
/>
</Link>
);
......
'use client';
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 memberImages from "@/constants/memberImages";
import { MOCK_FEATURED_MEMBERS_RESPONSE } from "@/mockdata/bff-fallback";
......@@ -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-full w-full max-w-[260px] items-center justify-center">
<Image
src={resolveMemberImage(member.avatar, index) ?? "/img-error.png"}
<SafeImage
src={resolveMemberImage(member.avatar, index)}
fallbackSrc="/img-error.png"
alt={member.name ?? ""}
width={260}
height={180}
......@@ -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-full w-full max-w-[260px] items-center justify-center">
<Image
src={resolveMemberImage(member.avatar, index) ?? "/img-error.png"}
<SafeImage
src={resolveMemberImage(member.avatar, index)}
fallbackSrc="/img-error.png"
alt={member.name ?? ""}
width={260}
height={180}
......@@ -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)]"
>
<div className="aspect-[16/10] overflow-hidden xl:aspect-[1.25/1]">
<Image
src={item.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE}
<SafeImage
src={item.thumbnail?.url}
fallbackSrc={MEMBER_CONNECTION_FALLBACK_IMAGE}
alt={item.thumbnail?.alt || item.title}
width={520}
height={420}
......@@ -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)]"
>
<div className="aspect-[16/10] overflow-hidden">
<Image
src={item.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE}
<SafeImage
src={item.thumbnail?.url}
fallbackSrc={MEMBER_CONNECTION_FALLBACK_IMAGE}
alt={item.thumbnail?.alt || item.title}
width={520}
height={420}
......
'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 dayjs from "dayjs";
import Link from "next/link";
......@@ -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)]"
>
<div className="relative aspect-[1.4/1] overflow-hidden">
<Image
src={featuredArticle.thumbnail?.url ?? "/thumbnail.png"}
<SafeImage
src={featuredArticle.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={featuredArticle.thumbnail?.alt || featuredArticle.title}
width={720}
height={580}
......@@ -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)]"
>
<div className="h-20 w-20 shrink-0 overflow-hidden rounded-[14px]">
<Image
src={news.thumbnail?.url ?? "/thumbnail.png"}
<SafeImage
src={news.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={news.thumbnail?.alt || news.title}
width={160}
height={160}
......@@ -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)]"
>
<div className="relative aspect-[16/9] overflow-hidden">
<Image
src={featuredArticle.thumbnail?.url ?? "/thumbnail.png"}
<SafeImage
src={featuredArticle.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={featuredArticle.thumbnail?.alt || featuredArticle.title}
width={720}
height={580}
......@@ -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)]"
>
<div className="h-16 w-16 shrink-0 overflow-hidden rounded-[14px]">
<Image
src={news.thumbnail?.url ?? "/thumbnail.png"}
<SafeImage
src={news.thumbnail?.url}
fallbackSrc="/thumbnail.png"
alt={news.thumbnail?.alt || news.title}
width={160}
height={160}
......
'use client';
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements";
import links from "@/links";
import type { Advertisement } from "@/api/vcci-news/models/advertisement";
......@@ -11,8 +11,7 @@ import { getFallbackImage } from "@/lib/utils/fallback-image";
const FALLBACK_HREF = links.externalApiOrigin;
function AdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: string }) {
const initialSrc = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc;
const [src, setSrc] = useState(initialSrc);
const src = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc;
return (
<Link
......@@ -23,16 +22,13 @@ function AdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: strin
title={item.name}
>
<div className="aspect-[16/10] overflow-hidden sm:aspect-[16/10] lg:aspect-[7/4] xl:aspect-[3/2]">
<Image
<SafeImage
src={src}
fallbackSrc={fallbackSrc}
alt={item.alt || item.name}
width={2048}
height={1365}
className="h-full w-full object-cover object-[center_80%]"
unoptimized
onError={() => {
if (src !== fallbackSrc) setSrc(fallbackSrc);
}}
/>
</div>
</Link>
......@@ -49,7 +45,7 @@ function FallbackAdItem({ src }: { src: string }) {
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]">
<Image
<SafeImage
src={src}
alt="Quảng cáo VCCI HCM"
width={2048}
......
"use client";
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import partnerImages from "@/constants/partnerImages";
import { ChevronRight, Play } from "lucide-react";
import Link from "next/link";
......@@ -68,8 +68,9 @@ const renderPartnerContent = (partners: Organization[]) => {
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]">
<Image
src={resolvePartnerImage(partner.avatar, index) ?? "/img-error.png"}
<SafeImage
src={resolvePartnerImage(partner.avatar, index)}
fallbackSrc="/img-error.png"
alt={partner.name ?? ""}
width={140}
height={72}
......@@ -79,8 +80,9 @@ const renderPartnerContent = (partners: Organization[]) => {
</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]">
<Image
src={resolvePartnerImage(partner.avatar, index) ?? "/img-error.png"}
<SafeImage
src={resolvePartnerImage(partner.avatar, index)}
fallbackSrc="/img-error.png"
alt={partner.name ?? ""}
width={140}
height={72}
......@@ -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)]"
>
<div className="group relative aspect-[1.95/1] overflow-hidden">
<Image
<SafeImage
src={video.thumbnail}
alt={video.name}
width={640}
......
......@@ -5,7 +5,7 @@ import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Spinner } from "@/components/ui";
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 { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category";
......@@ -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)]"
>
<div className="relative overflow-hidden rounded-md bg-[#edf1f5] aspect-[25/15] sm:aspect-[5/3]">
<Image
<SafeImage
src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title}
width={520}
......
......@@ -5,7 +5,7 @@ import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Spinner } from "@/components/ui";
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 { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category";
......@@ -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="relative aspect-3/4 overflow-hidden bg-white">
<Image
<SafeImage
src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title}
width={520}
......
"use client";
import parse from "html-react-parser";
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import { getDynamicPostBodyHtml } from "./data";
import type { DynamicPostContentSection, DynamicPostItem } from "./types";
......@@ -32,7 +32,7 @@ function StructuredImageSection({ section }: { section: DynamicPostContentSectio
key={`${section.id}-${image.id || image.url}-${item.position}`}
className="overflow-hidden rounded-[18px] bg-white"
>
<Image
<SafeImage
src={image.url}
alt={image.alt || image.name || "Hình ảnh bài viết"}
width={1200}
......
......@@ -6,7 +6,7 @@ import { ShieldCheck, Target, Zap } from "lucide-react";
import parse from "html-react-parser";
import Link from "next/link";
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 links from "@/links";
import StructuredPostContent from "../StructuredPostContent";
......@@ -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"
>
<div className="relative aspect-[1.28] overflow-hidden">
<Image
<SafeImage
src={item.thumbnailUrl}
alt={item.thumbnailAlt}
width={720}
......
......@@ -2,7 +2,7 @@
import { useMemo, useState } from "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";
type MarketProfilePageProps = {
......@@ -256,7 +256,7 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
})}
</div>
<Image
<SafeImage
src={activeRegion.image}
alt={activeRegion.imageAlt}
width={1200}
......
......@@ -3,7 +3,7 @@
import { Suspense, useEffect, useState } from "react";
import Link from "next/link";
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 { Pagination } from "@components/base/pagination";
import { Button } from "@/components/ui/button";
......@@ -54,7 +54,7 @@ function SearchResultItem({ item, index }: { item: DynamicPostItem; index: numbe
className="group grid gap-5 sm:grid-cols-[250px_minmax(0,1fr)]"
>
<div className="overflow-hidden rounded-md bg-[#edf1f5]">
<Image
<SafeImage
src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title}
width={520}
......
......@@ -3,7 +3,7 @@
import { Suspense } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Play } from "lucide-react";
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import { Pagination } from "@/components/base/pagination";
import { Spinner } from "@/components/ui/spinner";
import { useGetApiV10Video } from "@/api/vcci-news/endpoints/video";
......@@ -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)]"
>
<div className="relative aspect-video overflow-hidden bg-[#edf1f5]">
<Image
<SafeImage
src={video.thumbnail}
alt={video.name}
width={900}
......
......@@ -45,7 +45,7 @@ import {
import { PermissionGate } from "@/components/shared/permission-gate";
import { usePermission } from "@/hooks/usePermission";
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 links from "@/links";
import { useQueryClient } from "@tanstack/react-query";
......@@ -369,7 +369,7 @@ export function AdvertisementList({
style={{ aspectRatio: previewAspect, width: type === "horizontal" ? 112 : 72 }}
>
{imageUrl ? (
<SafeNextImage
<SafeImage
src={imageUrl}
alt={ad.alt || ad.name}
fill
......@@ -514,7 +514,7 @@ export function AdvertisementList({
style={{ aspectRatio: previewAspect, width: type === "horizontal" ? 200 : 120 }}
>
{form.filePreviewUrl ? (
<SafeNextImage
<SafeImage
src={form.filePreviewUrl}
alt="Preview"
fill
......
"use client";
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 {
Dialog,
......@@ -114,7 +114,7 @@ export function ConfigItemDialog({
<div className="overflow-hidden rounded-3xl border border-dashed border-[#063e8e]/20 bg-[#eef4ff]/60">
<div className="relative aspect-[16/9]">
{previewMedia ? (
<SafeNextImage
<SafeImage
src={previewMedia.url}
alt={previewMedia.alt || previewMedia.name}
fill
......
......@@ -2,7 +2,7 @@
import type { BaseConfigBannerItem } from "@/mockdata/base-config";
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({
title,
......@@ -28,7 +28,7 @@ export function ConfigItemPreview({
>
<div className="relative aspect-[16/10] overflow-hidden bg-[#eef4ff]">
{media ? (
<SafeNextImage
<SafeImage
src={media.url}
alt={media.alt || media.name}
fill
......
......@@ -14,7 +14,7 @@ import {
import { toast } from "sonner";
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
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 { Button } from "@/components/ui/button";
import {
......@@ -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]">
{currentLogoMedia ? (
<div className="relative h-[220px] w-[220px]">
<SafeNextImage
<SafeImage
src={currentLogoMedia.url}
alt={currentLogoMedia.alt || currentLogoMedia.name}
fill
......@@ -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="relative aspect-[16/6] overflow-hidden rounded-[24px] border border-[#063e8e]/12 bg-[#eef4ff]">
{currentBannerMedia ? (
<SafeNextImage
<SafeImage
src={currentBannerMedia.url}
alt={currentBannerMedia.alt || currentBannerMedia.name}
fill
......
......@@ -16,7 +16,7 @@ import {
Sparkles,
Users,
} 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 { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
......@@ -439,7 +439,7 @@ export default function AdminDashboardPage() {
>
<div className="relative h-20 w-28 shrink-0 overflow-hidden rounded-2xl bg-[#eef4ff]">
{item.thumbnail ? (
<SafeNextImage
<SafeImage
src={item.thumbnail.url}
alt={item.thumbnail.alt || item.thumbnail.name}
fill
......@@ -493,7 +493,7 @@ export default function AdminDashboardPage() {
<div className="mt-4 grid grid-cols-3 gap-3">
{mediaItems.slice(0, 3).map((item) => (
<div key={item.id} className="relative aspect-square overflow-hidden rounded-2xl bg-[#eef4ff]">
<SafeNextImage
<SafeImage
src={item.url}
alt={item.alt || item.name}
fill
......
......@@ -15,7 +15,7 @@ import { AdminRowActions } from "@/components/admin/admin-row-actions";
import { AdminStatsGrid } from "@/components/admin/admin-stats-grid";
import { AdminTableLayout } from "@/components/admin/admin-table-layout";
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 { Button } from "@/components/ui/button";
import {
......@@ -315,7 +315,7 @@ export default function HeaderCategoryPostsPage() {
<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]">
{item.thumbnail ? (
<SafeNextImage
<SafeImage
src={item.thumbnail.url}
alt={item.thumbnail.alt || item.thumbnail.name}
fill
......
......@@ -8,7 +8,7 @@ import {
X,
} from "lucide-react";
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 {
Dialog,
......@@ -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="relative aspect-[16/10] bg-[radial-gradient(circle_at_top,#d9e8ff_0%,#f7faff_58%,#ffffff_100%)]">
{form.previewUrl ? (
<SafeNextImage
<SafeImage
src={form.previewUrl}
alt={form.name}
fill
......
......@@ -9,7 +9,7 @@ import {
import { toast } from "sonner";
import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
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 { Button } from "@/components/ui/button";
import {
......@@ -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)]"
>
<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)}
alt={item.original ?? ""}
fill
......
......@@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { AdminImagePicker } from "@/components/admin/image-picker";
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
......@@ -351,7 +351,7 @@ export function AdminMemberForm({ memberId }: AdminMemberFormProps) {
{form.image ? (
<div className="space-y-3">
<div className="relative aspect-video overflow-hidden rounded-xl border border-[#063e8e]/15">
<SafeNextImage
<SafeImage
src={form.image.url}
alt={form.image.alt || form.image.name}
fill
......
"use client";
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 {
TableCell,
......@@ -47,7 +47,7 @@ export function MemberRow({
<TableCell className="px-4 py-3 text-center">
{item.image ? (
<div className="mx-auto h-12 w-16 overflow-hidden rounded-lg border border-[#063e8e]/15">
<SafeNextImage
<SafeImage
src={item.image.url}
alt={item.image.alt || item.name}
width={64}
......
......@@ -15,7 +15,7 @@ import { AdminImagePicker } from "@/components/admin/image-picker";
import { AdminPostContentEditor } from "@/components/admin/post-content-editor";
import { PostHistoryViewer } from "./post-history-viewer";
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 { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
......@@ -530,7 +530,7 @@ export function AdminNewsFormContent() {
<div className="relative overflow-hidden rounded-2xl border border-[#063e8e]/15 bg-white">
<div className="relative aspect-[16/11]">
{form.thumbnail ? (
<SafeNextImage
<SafeImage
src={form.thumbnail.url}
alt={form.thumbnail.alt || form.thumbnail.name}
fill
......
......@@ -15,7 +15,7 @@ import { AdminDeleteDialog } from "@/components/admin/admin-delete-dialog";
import { AdminRowActions } from "@/components/admin/admin-row-actions";
import { AdminStatsGrid } from "@/components/admin/admin-stats-grid";
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 { Button } from "@/components/ui/button";
import {
......@@ -449,7 +449,7 @@ export default function AdminNewsPage() {
<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]">
{item.thumbnail ? (
<SafeNextImage
<SafeImage
src={item.thumbnail.url}
alt={item.thumbnail.alt || item.thumbnail.name}
fill
......
......@@ -12,7 +12,7 @@ import {
DialogHeader,
DialogTitle,
} 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 { toAdminMediaItem } from "@/lib/utils/file";
import { getApiV10File, postApiV10FileUpload } from "@/api/vcci-news/endpoints/file";
......@@ -210,7 +210,7 @@ export function AdminImagePicker({
)}
>
<div className="relative aspect-[4/3] overflow-hidden bg-[#063e8e]/[0.04]">
<SafeNextImage
<SafeImage
src={item.url}
alt={item.alt || item.name}
fill
......
......@@ -4,7 +4,7 @@ import * as React from "react";
import { Image as ImageIcon, Pencil, Plus, Type, Upload, X } from "lucide-react";
import { AdminImagePicker } from "@/components/admin/image-picker";
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 { Label } from "@/components/ui/label";
import {
......@@ -409,7 +409,7 @@ export function AdminPostContentEditor({
}}
className="relative h-64 w-full cursor-pointer"
>
<SafeNextImage
<SafeImage
src={currentImage.image.url}
alt={currentImage.image.alt || currentImage.image.name}
fill
......
......@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Facebook, Linkedin, Menu, Twitter, X, Youtube } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link";
const fallbackLogo = "/logo.png";
import { useGetApiV10Logo } from "@/api/vcci-news/endpoints/logo";
......@@ -335,18 +335,14 @@ function Header() {
href="/"
className="flex w-[136px] shrink-0 items-center xl:w-[152px]"
>
<Image
<SafeImage
width={108}
height={40}
className="h-auto max-h-10 w-[108px] object-contain"
src={currentLogo?.logo_url ? links.resolveImageUrl(currentLogo.logo_url) : fallbackLogo}
fallbackSrc={fallbackLogo}
alt={currentLogo?.logo_name || "VCCI-HCM"}
priority
unoptimized={Boolean(currentLogo?.logo_url)}
onError={(e) => {
const img = e.currentTarget;
if (img.src !== fallbackLogo) img.src = fallbackLogo;
}}
/>
</Link>
......@@ -394,18 +390,14 @@ function Header() {
className="flex w-[136px] shrink-0 items-center"
onClick={() => setToggleMenu(false)}
>
<Image
<SafeImage
width={108}
height={40}
className="h-auto max-h-10 w-[108px] object-contain"
src={currentLogo?.logo_url ? links.resolveImageUrl(currentLogo.logo_url) : fallbackLogo}
fallbackSrc={fallbackLogo}
alt={currentLogo?.logo_name || "VCCI-HCM"}
priority
unoptimized={Boolean(currentLogo?.logo_url)}
onError={(e) => {
const img = e.currentTarget;
if (img.src !== fallbackLogo) img.src = fallbackLogo;
}}
/>
</Link>
<button
......
"use client";
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;
fallbackSrc?: string;
}
export function SafeNextImage({
export function SafeImage({
src,
alt,
fallbackSrc = "/img-error.png",
...props
}: SafeNextImageProps) {
const [currentSrc, setCurrentSrc] = React.useState(src || fallbackSrc);
const [hasFailed, setHasFailed] = React.useState(false);
}: SafeImageProps) {
const [currentSrc, setCurrentSrc] = useState(src || fallbackSrc);
const [hasFailed, setHasFailed] = useState(false);
React.useEffect(() => {
useEffect(() => {
setCurrentSrc(src || fallbackSrc);
setHasFailed(false);
}, [fallbackSrc, src]);
......
'use client';
import Image from "next/image";
import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { useAdvertisements } from "@/app/(main)/(home)/lib/use-advertisements";
import links from "@/links";
import type { Advertisement } from "@/api/vcci-news/models/advertisement";
......@@ -11,8 +11,7 @@ import { getFallbackImage } from "@/lib/utils/fallback-image";
const FALLBACK_HREF = "https://vcci-hcm.org.vn";
function SidebarAdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc: string }) {
const initialSrc = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc;
const [src, setSrc] = useState(initialSrc);
const src = item.file?.path ? links.resolveImageUrl(item.file.path) : fallbackSrc;
return (
<Link
......@@ -23,15 +22,13 @@ function SidebarAdItem({ item, fallbackSrc }: { item: Advertisement; fallbackSrc
title={item.name}
>
<div className="relative aspect-[16/10]">
<Image
<SafeImage
src={src}
fallbackSrc={fallbackSrc}
alt={item.alt || item.name}
fill
sizes="(max-width: 768px) 100vw, 300px"
className="h-full w-full object-cover"
onError={() => {
if (src !== fallbackSrc) setSrc(fallbackSrc);
}}
/>
</div>
</Link>
......@@ -48,7 +45,7 @@ function FallbackSidebarAdItem({ src }: { src: string }) {
title="Quảng cáo VCCI HCM"
>
<div className="relative aspect-[16/10]">
<Image
<SafeImage
src={src}
alt="Quảng cáo VCCI HCM"
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