Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
VCCI-News
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Văn Hoàng
VCCI-News
Commits
f64d2172
Commit
f64d2172
authored
Sep 06, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Plain Diff
[tag]0.1-vcci
parents
f34822d0
d5228e26
Pipeline
#52997
passed with stages
in 1 minute and 25 seconds
Changes
32
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
32 changed files
with
124 additions
and
145 deletions
+124
-145
index.tsx
src/app/(main)/(home)/components/banner/index.tsx
+3
-8
index.tsx
src/app/(main)/(home)/components/events/index.tsx
+7
-5
index.tsx
src/app/(main)/(home)/components/featured-news/index.tsx
+7
-5
index.tsx
...p/(main)/(home)/components/horizontal-ad-banner/index.tsx
+4
-19
index.tsx
src/app/(main)/(home)/components/members/index.tsx
+13
-9
index.tsx
src/app/(main)/(home)/components/news/index.tsx
+13
-9
index.tsx
src/app/(main)/(home)/components/quick-links/index.tsx
+6
-10
index.tsx
src/app/(main)/(home)/components/video-and-patners/index.tsx
+8
-6
ArticlePage.tsx
src/app/(main)/[...slug]/templates/ArticlePage.tsx
+2
-2
CatalogPage.tsx
src/app/(main)/[...slug]/templates/CatalogPage.tsx
+2
-2
StructuredPostContent.tsx
src/app/(main)/[...slug]/templates/StructuredPostContent.tsx
+2
-2
AboutVcciHcmPage.tsx
......slug]/templates/information-pages/AboutVcciHcmPage.tsx
+2
-2
MarketProfilePage.tsx
.....slug]/templates/information-pages/MarketProfilePage.tsx
+2
-2
page.tsx
src/app/(main)/search/page.tsx
+2
-2
page.tsx
src/app/(main)/video/page.tsx
+2
-2
advertisement-list.tsx
src/app/admin/advertisements/advertisement-list.tsx
+3
-3
config-item-dialog.tsx
src/app/admin/base-config/_components/config-item-dialog.tsx
+2
-2
config-item-preview.tsx
...app/admin/base-config/_components/config-item-preview.tsx
+2
-2
page.tsx
src/app/admin/base-config/page.tsx
+3
-3
page.tsx
src/app/admin/dashboard/page.tsx
+3
-3
page.tsx
src/app/admin/header-config/[categoryId]/page.tsx
+2
-2
media-form-dialog.tsx
src/app/admin/media/_components/media-form-dialog.tsx
+2
-2
page.tsx
src/app/admin/media/page.tsx
+2
-2
member-form.tsx
src/app/admin/members/[id]/_components/member-form.tsx
+2
-2
member-row.tsx
src/app/admin/members/_components/member-row.tsx
+2
-2
admin-news-form-content.tsx
...p/admin/news/[id]/_components/admin-news-form-content.tsx
+2
-2
page.tsx
src/app/admin/news/page.tsx
+2
-2
image-picker.tsx
src/components/admin/image-picker.tsx
+2
-2
post-content-editor.tsx
src/components/admin/post-content-editor.tsx
+2
-2
header.tsx
src/components/layout/main/header.tsx
+5
-13
safe-image.tsx
src/components/shared/safe-image.tsx
+7
-7
index.tsx
src/components/shared/sidebar-advertisements/index.tsx
+6
-9
No files found.
src/app/(main)/(home)/components/banner/index.tsx
View file @
f64d2172
"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
<
Safe
Image
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"
/>
);
...
...
src/app/(main)/(home)/components/events/index.tsx
View file @
f64d2172
'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
}
...
...
src/app/(main)/(home)/components/featured-news/index.tsx
View file @
f64d2172
'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
}
...
...
src/app/(main)/(home)/components/horizontal-ad-banner/index.tsx
View file @
f64d2172
'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
<
Safe
Image
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
>
);
...
...
src/app/(main)/(home)/components/members/index.tsx
View file @
f64d2172
'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
}
...
...
src/app/(main)/(home)/components/news/index.tsx
View file @
f64d2172
'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
}
...
...
src/app/(main)/(home)/components/quick-links/index.tsx
View file @
f64d2172
'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
<
Safe
Image
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
<
Safe
Image
src=
{
src
}
alt=
"Quảng cáo VCCI HCM"
width=
{
2048
}
...
...
src/app/(main)/(home)/components/video-and-patners/index.tsx
View file @
f64d2172
"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
<
Safe
Image
src=
{
video
.
thumbnail
}
alt=
{
video
.
name
}
width=
{
640
}
...
...
src/app/(main)/[...slug]/templates/ArticlePage.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
src=
{
resolveDynamicPostImage
(
item
.
thumbnail
)
}
alt=
{
item
.
title
}
width=
{
520
}
...
...
src/app/(main)/[...slug]/templates/CatalogPage.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
src=
{
resolveDynamicPostImage
(
item
.
thumbnail
)
}
alt=
{
item
.
title
}
width=
{
520
}
...
...
src/app/(main)/[...slug]/templates/StructuredPostContent.tsx
View file @
f64d2172
"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
<
Safe
Image
src=
{
image
.
url
}
alt=
{
image
.
alt
||
image
.
name
||
"Hình ảnh bài viết"
}
width=
{
1200
}
...
...
src/app/(main)/[...slug]/templates/information-pages/AboutVcciHcmPage.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
src=
{
item
.
thumbnailUrl
}
alt=
{
item
.
thumbnailAlt
}
width=
{
720
}
...
...
src/app/(main)/[...slug]/templates/information-pages/MarketProfilePage.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
src=
{
activeRegion
.
image
}
alt=
{
activeRegion
.
imageAlt
}
width=
{
1200
}
...
...
src/app/(main)/search/page.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
src=
{
resolveDynamicPostImage
(
item
.
thumbnail
)
}
alt=
{
item
.
title
}
width=
{
520
}
...
...
src/app/(main)/video/page.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
src=
{
video
.
thumbnail
}
alt=
{
video
.
name
}
width=
{
900
}
...
...
src/app/admin/advertisements/advertisement-list.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
form
.
filePreviewUrl
}
alt=
"Preview"
fill
...
...
src/app/admin/base-config/_components/config-item-dialog.tsx
View file @
f64d2172
"use client"
;
import
{
ImagePlus
,
Save
}
from
"lucide-react"
;
import
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
previewMedia
.
url
}
alt=
{
previewMedia
.
alt
||
previewMedia
.
name
}
fill
...
...
src/app/admin/base-config/_components/config-item-preview.tsx
View file @
f64d2172
...
...
@@ -2,7 +2,7 @@
import
type
{
BaseConfigBannerItem
}
from
"@/mockdata/base-config"
;
import
type
{
AdminMediaItem
}
from
"@/mockdata/admin-news"
;
import
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
media
.
url
}
alt=
{
media
.
alt
||
media
.
name
}
fill
...
...
src/app/admin/base-config/page.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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]"
>
<
Safe
Next
Image
<
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
currentBannerMedia
.
url
}
alt=
{
currentBannerMedia
.
alt
||
currentBannerMedia
.
name
}
fill
...
...
src/app/admin/dashboard/page.tsx
View file @
f64d2172
...
...
@@ -16,7 +16,7 @@ import {
Sparkles
,
Users
,
}
from
"lucide-react"
;
import
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
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]"
>
<
Safe
Next
Image
<
SafeImage
src=
{
item
.
url
}
alt=
{
item
.
alt
||
item
.
name
}
fill
...
...
src/app/admin/header-config/[categoryId]/page.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
item
.
thumbnail
.
url
}
alt=
{
item
.
thumbnail
.
alt
||
item
.
thumbnail
.
name
}
fill
...
...
src/app/admin/media/_components/media-form-dialog.tsx
View file @
f64d2172
...
...
@@ -8,7 +8,7 @@ import {
X
,
}
from
"lucide-react"
;
import
{
toast
}
from
"sonner"
;
import
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
form
.
previewUrl
}
alt=
{
form
.
name
}
fill
...
...
src/app/admin/media/page.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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%)]"
>
<
Safe
Next
Image
<
SafeImage
src=
{
resolveCmsFileUrl
(
item
.
path
)
}
alt=
{
item
.
original
??
""
}
fill
...
...
src/app/admin/members/[id]/_components/member-form.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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"
>
<
Safe
Next
Image
<
SafeImage
src=
{
form
.
image
.
url
}
alt=
{
form
.
image
.
alt
||
form
.
image
.
name
}
fill
...
...
src/app/admin/members/_components/member-row.tsx
View file @
f64d2172
"use client"
;
import
{
AdminRowActions
}
from
"@/components/admin/admin-row-actions"
;
import
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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"
>
<
Safe
Next
Image
<
SafeImage
src=
{
item
.
image
.
url
}
alt=
{
item
.
image
.
alt
||
item
.
name
}
width=
{
64
}
...
...
src/app/admin/news/[id]/_components/admin-news-form-content.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
form
.
thumbnail
.
url
}
alt=
{
form
.
thumbnail
.
alt
||
form
.
thumbnail
.
name
}
fill
...
...
src/app/admin/news/page.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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
?
(
<
Safe
Next
Image
<
SafeImage
src=
{
item
.
thumbnail
.
url
}
alt=
{
item
.
thumbnail
.
alt
||
item
.
thumbnail
.
name
}
fill
...
...
src/components/admin/image-picker.tsx
View file @
f64d2172
...
...
@@ -12,7 +12,7 @@ import {
DialogHeader
,
DialogTitle
,
}
from
"@/components/ui/dialog"
;
import
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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]"
>
<
Safe
Next
Image
<
SafeImage
src=
{
item
.
url
}
alt=
{
item
.
alt
||
item
.
name
}
fill
...
...
src/components/admin/post-content-editor.tsx
View file @
f64d2172
...
...
@@ -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
{
Safe
NextImage
}
from
"@/components/admin/safe-next
-image"
;
import
{
Safe
Image
}
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"
>
<
Safe
Next
Image
<
SafeImage
src=
{
currentImage
.
image
.
url
}
alt=
{
currentImage
.
image
.
alt
||
currentImage
.
image
.
name
}
fill
...
...
src/components/layout/main/header.tsx
View file @
f64d2172
...
...
@@ -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
<
Safe
Image
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
<
Safe
Image
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
...
...
src/components/
admin/safe-next
-image.tsx
→
src/components/
shared/safe
-image.tsx
View file @
f64d2172
"use client"
;
import
Image
,
{
type
ImageProps
}
from
"next/image"
;
import
*
as
React
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
interface
Safe
Next
ImageProps
extends
Omit
<
ImageProps
,
"src"
>
{
interface
SafeImageProps
extends
Omit
<
ImageProps
,
"src"
>
{
src
?:
string
|
null
;
fallbackSrc
?:
string
;
}
export
function
Safe
Next
Image
({
export
function
SafeImage
({
src
,
alt
,
fallbackSrc
=
"/img-error.png"
,
...
props
}:
Safe
Next
ImageProps
)
{
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
]);
...
...
src/components/shared/sidebar-advertisements/index.tsx
View file @
f64d2172
'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
<
Safe
Image
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
<
Safe
Image
src=
{
src
}
alt=
"Quảng cáo VCCI HCM"
fill
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment