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
f01580e6
Commit
f01580e6
authored
May 21, 2026
by
Lê Bảo Hồng Đức
☄
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix
parent
1334e92b
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
280 additions
and
33 deletions
+280
-33
next.config.ts
next.config.ts
+6
-0
index.tsx
src/app/(main)/(home)/components/members/index.tsx
+108
-19
index.tsx
src/app/(main)/(home)/components/video-and-patners/index.tsx
+100
-14
route.ts
src/app/api/featured-members/route.ts
+33
-0
route.ts
src/app/api/partners/route.ts
+33
-0
No files found.
next.config.ts
View file @
f01580e6
...
@@ -20,6 +20,12 @@ const nextConfig: NextConfig = {
...
@@ -20,6 +20,12 @@ const nextConfig: NextConfig = {
port
:
""
,
port
:
""
,
pathname
:
"/wp-content/uploads/**"
,
pathname
:
"/wp-content/uploads/**"
,
},
},
{
protocol
:
"https"
,
hostname
:
"vccihcm.vn"
,
port
:
""
,
pathname
:
"/images/**"
,
},
],
],
},
},
};
};
...
...
src/app/(main)/(home)/components/members/index.tsx
View file @
f01580e6
'use client'
;
'use client'
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
{
useHomePosts
}
from
"@/app/(main)/(home)/lib/use-home-posts"
;
import
{
useHomePosts
}
from
"@/app/(main)/(home)/lib/use-home-posts"
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
memberImages
from
"@/constants/memberImages"
;
import
memberImages
from
"@/constants/memberImages"
;
import
Link
from
"next/link"
;
import
Link
from
"next/link"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
Autoplay
}
from
"swiper/modules"
;
import
{
Swiper
,
SwiperSlide
}
from
"swiper/react"
;
import
"swiper/css"
;
const
MEMBER_CONNECTION_FALLBACK_IMAGE
=
"/home/20-2048x1365.webp"
;
const
MEMBER_CONNECTION_FALLBACK_IMAGE
=
"/home/20-2048x1365.webp"
;
const
FEATURED_MEMBER_API_URL
=
"/api/featured-members"
;
const
FEATURED_MEMBER_MORE_URL
=
"https://vccihcm.vn/giao-thuong-b2b?filters=users.status_id+%3D%3D+36ca1cc5-7b6e-4f9f-b973-69c5207deb62&sortField=created_at&sortOrder=ASC"
;
const
VCCI_HCM_ORIGIN
=
"https://vccihcm.vn"
;
type
FeaturedMember
=
{
id
:
string
;
name
:
string
;
avatar
?:
string
|
null
;
};
type
FeaturedMembersResponse
=
{
responseData
?:
{
rows
?:
FeaturedMember
[];
};
};
const
resolveMemberImage
=
(
avatar
:
string
|
null
|
undefined
,
index
:
number
)
=>
{
if
(
avatar
?.
startsWith
(
"http://"
)
||
avatar
?.
startsWith
(
"https://"
))
{
return
avatar
;
}
if
(
avatar
?.
startsWith
(
"/"
))
{
return
`
${
VCCI_HCM_ORIGIN
}${
avatar
}
`
;
}
return
memberImages
[
index
%
memberImages
.
length
]
??
"/img-error.png"
;
};
const
fallbackMembers
:
FeaturedMember
[]
=
Array
.
from
({
length
:
9
},
(
_
,
index
)
=>
({
id
:
`fallback-member-
${
index
}
`
,
name
:
`Hội viên tiêu biểu
${
index
+
1
}
`
,
avatar
:
memberImages
[
index
%
memberImages
.
length
],
}));
function
Members
()
{
function
Members
()
{
const
{
memberConnectionPosts
,
categoryLinks
,
categoryNames
}
=
useHomePosts
();
const
{
memberConnectionPosts
,
categoryLinks
,
categoryNames
}
=
useHomePosts
();
const
[
featuredMembers
,
setFeaturedMembers
]
=
useState
<
FeaturedMember
[]
>
([]);
const
featuredConnection
=
memberConnectionPosts
[
0
];
const
featuredConnection
=
memberConnectionPosts
[
0
];
const
sectionLink
=
const
sectionLink
=
categoryLinks
.
get
(
categoryNames
.
ketNoiHoiVien
.
toLowerCase
())
??
"/hoi-vien/ket-noi-hoi-vien"
;
categoryLinks
.
get
(
categoryNames
.
ketNoiHoiVien
.
toLowerCase
())
??
"/hoi-vien/ket-noi-hoi-vien"
;
...
@@ -16,6 +55,38 @@ function Members() {
...
@@ -16,6 +55,38 @@ function Members() {
featuredConnection
?.
thumbnail
?.
url
??
MEMBER_CONNECTION_FALLBACK_IMAGE
;
featuredConnection
?.
thumbnail
?.
url
??
MEMBER_CONNECTION_FALLBACK_IMAGE
;
const
connectionImageAlt
=
const
connectionImageAlt
=
featuredConnection
?.
thumbnail
?.
alt
||
featuredConnection
?.
title
||
"VCCI HCM"
;
featuredConnection
?.
thumbnail
?.
alt
||
featuredConnection
?.
title
||
"VCCI HCM"
;
const
displayMembers
=
featuredMembers
.
length
>
0
?
featuredMembers
:
fallbackMembers
;
useEffect
(()
=>
{
let
isMounted
=
true
;
const
fetchFeaturedMembers
=
async
()
=>
{
try
{
const
response
=
await
fetch
(
FEATURED_MEMBER_API_URL
,
{
cache
:
"no-store"
,
});
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Cannot load featured members:
${
response
.
status
}
`
);
}
const
data
=
(
await
response
.
json
())
as
FeaturedMembersResponse
;
const
rows
=
data
.
responseData
?.
rows
??
[];
if
(
isMounted
)
{
setFeaturedMembers
(
rows
.
slice
(
0
,
9
));
}
}
catch
(
error
)
{
console
.
error
(
error
);
}
};
fetchFeaturedMembers
();
return
()
=>
{
isMounted
=
false
;
};
},
[]);
return
(
return
(
<
section
className=
"flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch"
>
<
section
className=
"flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch"
>
...
@@ -25,11 +96,13 @@ function Members() {
...
@@ -25,11 +96,13 @@ function Members() {
<
h2
className=
"client-section-title uppercase text-[#20449a]"
>
<
h2
className=
"client-section-title uppercase text-[#20449a]"
>
Hội viên tiêu biểu
Hội viên tiêu biểu
</
h2
>
</
h2
>
<
div
className=
"mt-2.5 h-
[4px] w-[40px]
rounded-full bg-white"
/>
<
div
className=
"mt-2.5 h-
1 w-10
rounded-full bg-white"
/>
</
div
>
</
div
>
<
Link
<
Link
href=
{
sectionLink
}
href=
{
FEATURED_MEMBER_MORE_URL
}
target=
"_blank"
rel=
"noreferrer"
className=
"pt-1 text-sm font-semibold text-[#1e2f5e] transition-colors hover:text-[#20449a]"
className=
"pt-1 text-sm font-semibold text-[#1e2f5e] transition-colors hover:text-[#20449a]"
>
>
Xem thêm
Xem thêm
...
@@ -38,24 +111,40 @@ function Members() {
...
@@ -38,24 +111,40 @@ function Members() {
<
div
className=
"mt-4 border-t border-[#e7aa00] pt-5"
/>
<
div
className=
"mt-4 border-t border-[#e7aa00] pt-5"
/>
<
div
className=
"grid gap-4 sm:grid-cols-3"
>
<
Swiper
{
memberImages
.
slice
(
0
,
3
).
map
((
src
,
index
)
=>
(
modules=
{
[
Autoplay
]
}
<
div
autoplay=
{
{
delay
:
4200
,
disableOnInteraction
:
false
}
}
key=
{
src
}
observer
className=
"rounded-[24px] bg-white p-[7px] shadow-[0_10px_22px_rgba(158,114,0,0.16)]"
observeParents
updateOnWindowResize
slidesPerView=
"auto"
spaceBetween=
{
16
}
className=
"w-full"
>
>
<
div
className=
"flex aspect-[1.06/1] items-center justify-center overflow-hidden rounded-[18px] bg-[#f4f7fb] px-4 py-5"
>
{
displayMembers
.
map
((
member
,
index
)
=>
(
<
SwiperSlide
key=
{
member
.
id
}
className=
"!h-auto !w-full md:!w-[calc(50%-8px)] xl:!w-[calc(33.333%-10.67px)]"
>
<
article
className=
"rounded-[20px] bg-white p-[7px] shadow-[0_10px_22px_rgba(158,114,0,0.16)]"
>
<
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"
>
<
ImageNext
<
ImageNext
src=
{
src
}
src=
{
resolveMemberImage
(
member
.
avatar
,
index
)
}
alt=
{
`Hội viên tiêu biểu ${index + 1}`
}
alt=
{
member
.
name
}
width=
{
32
0
}
width=
{
26
0
}
height=
{
22
0
}
height=
{
18
0
}
className=
"max-h-full
max-w-full object-contain"
className=
"h-[180px] w-[260px]
max-w-full object-contain"
/>
/>
</
div
>
</
div
>
</
div
>
</
div
>
<
h3
className=
"mt-3 line-clamp-2 min-h-[40px] px-1 text-center text-sm font-semibold leading-5 text-[#1e2f5e]"
>
{
member
.
name
}
</
h3
>
</
article
>
</
SwiperSlide
>
))
}
))
}
</
div
>
</
Swiper
>
</
aside
>
</
aside
>
<
aside
className=
"w-full xl:w-[31%] xl:min-w-[320px]"
>
<
aside
className=
"w-full xl:w-[31%] xl:min-w-[320px]"
>
...
...
src/app/(main)/(home)/components/video-and-patners/index.tsx
View file @
f01580e6
...
@@ -6,6 +6,44 @@ import partnerImages from "@/constants/partnerImages";
...
@@ -6,6 +6,44 @@ 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"
;
import
{
fetchClientVideos
}
from
"@/lib/api/videos"
;
import
{
fetchClientVideos
}
from
"@/lib/api/videos"
;
import
{
Autoplay
}
from
"swiper/modules"
;
import
{
Swiper
,
SwiperSlide
}
from
"swiper/react"
;
import
"swiper/css"
;
type
Partner
=
{
id
:
string
;
name
:
string
;
avatar
?:
string
|
null
;
website
?:
string
|
null
;
};
type
PartnerResponse
=
{
responseData
?:
{
rows
?:
Partner
[];
};
};
const
PARTNER_API_URL
=
"/api/partners"
;
const
VCCI_HCM_ORIGIN
=
"https://vccihcm.vn"
;
const
resolvePartnerImage
=
(
avatar
:
string
|
null
|
undefined
,
index
:
number
)
=>
{
if
(
avatar
?.
startsWith
(
"http://"
)
||
avatar
?.
startsWith
(
"https://"
))
{
return
avatar
;
}
if
(
avatar
?.
startsWith
(
"/"
))
{
return
`
${
VCCI_HCM_ORIGIN
}${
avatar
}
`
;
}
return
partnerImages
[
index
%
partnerImages
.
length
]
??
"/img-error.png"
;
};
const
fallbackPartners
:
Partner
[]
=
Array
.
from
({
length
:
6
},
(
_
,
index
)
=>
({
id
:
`fallback-partner-
${
index
}
`
,
name
:
`Đối tác
${
index
+
1
}
`
,
avatar
:
partnerImages
[
index
%
partnerImages
.
length
],
website
:
null
,
}));
function
VideoAndPartners
()
{
function
VideoAndPartners
()
{
const
videosQuery
=
useQuery
({
const
videosQuery
=
useQuery
({
...
@@ -14,7 +52,25 @@ function VideoAndPartners() {
...
@@ -14,7 +52,25 @@ function VideoAndPartners() {
staleTime
:
60
*
1000
,
staleTime
:
60
*
1000
,
});
});
const
partnersQuery
=
useQuery
({
queryKey
:
[
"home-partners"
],
queryFn
:
async
()
=>
{
const
response
=
await
fetch
(
PARTNER_API_URL
,
{
cache
:
"no-store"
,
});
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Cannot load partners:
${
response
.
status
}
`
);
}
return
(
await
response
.
json
())
as
PartnerResponse
;
},
staleTime
:
60
*
1000
,
});
const
videos
=
videosQuery
.
data
?.
rows
??
[];
const
videos
=
videosQuery
.
data
?.
rows
??
[];
const
partners
=
partnersQuery
.
data
?.
responseData
?.
rows
?.
slice
(
0
,
12
)
??
[];
const
displayPartners
=
partners
.
length
>
0
?
partners
:
fallbackPartners
;
return
(
return
(
<
section
className=
"flex flex-col gap-6 pb-10 xl:flex-row xl:items-stretch"
>
<
section
className=
"flex flex-col gap-6 pb-10 xl:flex-row xl:items-stretch"
>
...
@@ -93,22 +149,52 @@ function VideoAndPartners() {
...
@@ -93,22 +149,52 @@ function VideoAndPartners() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:h-[318px] xl:grid-rows-2"
>
<
Swiper
{
partnerImages
.
slice
(
0
,
6
).
map
((
src
,
index
)
=>
(
modules=
{
[
Autoplay
]
}
<
div
autoplay=
{
{
delay
:
4200
,
disableOnInteraction
:
false
}
}
key=
{
src
}
observer
className=
"flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-auto"
observeParents
updateOnWindowResize
slidesPerView=
"auto"
spaceBetween=
{
16
}
className=
"w-full"
>
{
displayPartners
.
map
((
partner
,
index
)
=>
(
<
SwiperSlide
key=
{
partner
.
id
}
className=
"!h-auto !w-full sm:!w-[calc(50%-8px)] xl:!w-[calc(33.333%-10.67px)]"
>
>
{
partner
.
website
?
(
<
a
href=
{
partner
.
website
}
target=
"_blank"
rel=
"noreferrer"
className=
"block"
>
<
div
className=
"flex h-[96px] items-center justify-center rounded-[14px] 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]"
>
<
ImageNext
<
ImageNext
src=
{
src
}
src=
{
resolvePartnerImage
(
partner
.
avatar
,
index
)
}
alt=
{
`Đối tác ${index + 1}`
}
alt=
{
partner
.
name
}
width=
{
140
}
width=
{
140
}
height=
{
72
}
height=
{
72
}
className=
"max-h-full w-full object-contain"
className=
"max-h-full w-full object-contain"
/>
/>
</
div
>
</
div
>
))
}
</
a
>
)
:
(
<
div
className=
"flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-[151px]"
>
<
ImageNext
src=
{
resolvePartnerImage
(
partner
.
avatar
,
index
)
}
alt=
{
partner
.
name
}
width=
{
140
}
height=
{
72
}
className=
"max-h-full w-full object-contain"
/>
</
div
>
</
div
>
)
}
</
SwiperSlide
>
))
}
</
Swiper
>
</
aside
>
</
aside
>
</
section
>
</
section
>
);
);
...
...
src/app/api/featured-members/route.ts
0 → 100644
View file @
f01580e6
import
{
NextResponse
}
from
"next/server"
;
const
FEATURED_MEMBER_API_URL
=
"https://vccihcm.vn/api/v1.0/organizations?filters=users.status_id+%3D%3D+36ca1cc5-7b6e-4f9f-b973-69c5207deb62&pageSize=9&sortField=created_at&sortOrder=ASC"
;
export
async
function
GET
()
{
try
{
const
response
=
await
fetch
(
FEATURED_MEMBER_API_URL
,
{
headers
:
{
Accept
:
"application/json"
,
},
next
:
{
revalidate
:
300
},
});
if
(
!
response
.
ok
)
{
return
NextResponse
.
json
(
{
message
:
"Không thể tải dữ liệu hội viên tiêu biểu"
},
{
status
:
response
.
status
},
);
}
const
data
=
await
response
.
json
();
return
NextResponse
.
json
(
data
);
}
catch
(
error
)
{
console
.
error
(
"Failed to fetch featured members"
,
error
);
return
NextResponse
.
json
(
{
message
:
"Không thể tải dữ liệu hội viên tiêu biểu"
},
{
status
:
500
},
);
}
}
src/app/api/partners/route.ts
0 → 100644
View file @
f01580e6
import
{
NextResponse
}
from
"next/server"
;
const
PARTNER_API_URL
=
"https://vccihcm.vn/api/v1.0/organizations?filters=type%3D%3DSPONSOR&pageSize=12&sortField=sort_order&sortOrder=ASC"
;
export
async
function
GET
()
{
try
{
const
response
=
await
fetch
(
PARTNER_API_URL
,
{
headers
:
{
Accept
:
"application/json"
,
},
next
:
{
revalidate
:
300
},
});
if
(
!
response
.
ok
)
{
return
NextResponse
.
json
(
{
message
:
"Không thể tải dữ liệu đối tác"
},
{
status
:
response
.
status
},
);
}
const
data
=
await
response
.
json
();
return
NextResponse
.
json
(
data
);
}
catch
(
error
)
{
console
.
error
(
"Failed to fetch partners"
,
error
);
return
NextResponse
.
json
(
{
message
:
"Không thể tải dữ liệu đối tác"
},
{
status
:
500
},
);
}
}
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