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
cab5fb7a
Commit
cab5fb7a
authored
Jun 23, 2026
by
Lê Bảo Hồng Đức
☄
Browse files
Options
Browse Files
Download
Plain Diff
[tag]0.1-vcci
parents
d1d433e9
7f1f5c15
Pipeline
#50938
passed with stages
in 5 minutes and 29 seconds
Changes
6
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
131 additions
and
80 deletions
+131
-80
pnpm-workspace.yaml
pnpm-workspace.yaml
+4
-0
MAPS_VCCI-HCM.png
public/MAPS_VCCI-HCM.png
+0
-0
AboutVcciHcmPage.tsx
......slug]/templates/information-pages/AboutVcciHcmPage.tsx
+74
-75
DefaultInformationPage.tsx
...g]/templates/information-pages/DefaultInformationPage.tsx
+2
-2
ServicePage.tsx
...in)/[...slug]/templates/information-pages/ServicePage.tsx
+1
-1
rich-text-editor.tsx
src/components/admin/rich-text-editor.tsx
+50
-2
No files found.
pnpm-workspace.yaml
0 → 100644
View file @
cab5fb7a
allowBuilds
:
esbuild
:
true
sharp
:
true
unrs-resolver
:
true
public/MAPS_VCCI-HCM.png
0 → 100644
View file @
cab5fb7a
915 KB
src/app/(main)/[...slug]/templates/information-pages/AboutVcciHcmPage.tsx
View file @
cab5fb7a
...
@@ -2,12 +2,12 @@
...
@@ -2,12 +2,12 @@
import
dayjs
from
"dayjs"
;
import
dayjs
from
"dayjs"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
Play
,
ShieldCheck
,
Target
,
Zap
}
from
"lucide-react"
;
import
{
ShieldCheck
,
Target
,
Zap
}
from
"lucide-react"
;
import
parse
from
"html-react-parser"
;
import
Link
from
"next/link"
;
import
Link
from
"next/link"
;
import
{
useCustomClient
}
from
"@/api/mutator/custom-client"
;
import
{
useCustomClient
}
from
"@/api/mutator/custom-client"
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
{
fetchClientVideos
}
from
"@/lib/api/videos"
;
import
{
buildDynamicPostHref
,
buildVisibleNewsFilters
,
stripHtml
}
from
"../data"
;
import
{
buildDynamicPostHref
,
buildVisibleNewsFilters
}
from
"../data"
;
import
StructuredPostContent
from
"../StructuredPostContent"
;
import
StructuredPostContent
from
"../StructuredPostContent"
;
import
type
{
DynamicPostItem
}
from
"../types"
;
import
type
{
DynamicPostItem
}
from
"../types"
;
...
@@ -38,16 +38,44 @@ const ABOUT_HIGHLIGHTS = [
...
@@ -38,16 +38,44 @@ const ABOUT_HIGHLIGHTS = [
]
as
const
;
]
as
const
;
const
ACTIVITY_AREAS
=
[
const
ACTIVITY_AREAS
=
[
"TP. Hồ Chí Minh"
,
{
"Bình Dương"
,
name
:
"TP. Hồ Chí Minh"
,
"Bình Phước"
,
description
:
"Đồng Nai"
,
"Trung tâm điều phối, kết nối doanh nghiệp và lan tỏa các chương trình hỗ trợ hội viên trên toàn khu vực."
,
"Lâm Đồng"
,
toneClass
:
"from-[#f59e0b] to-[#ef4444]"
,
"Tây Ninh"
,
},
{
name
:
"Đồng Nai"
,
description
:
"Địa bàn công nghiệp trọng điểm, gắn với nhu cầu xúc tiến thương mại và hỗ trợ sản xuất - xuất khẩu."
,
toneClass
:
"from-[#2563eb] to-[#60a5fa]"
,
},
{
name
:
"Lâm Đồng"
,
description
:
"Khu vực phát triển nông nghiệp công nghệ cao, du lịch và các mô hình kinh tế xanh, bền vững."
,
toneClass
:
"from-[#16a34a] to-[#86efac]"
,
},
{
name
:
"Tây Ninh"
,
description
:
"Cửa ngõ giao thương quan trọng, thuận lợi cho kết nối chuỗi cung ứng, logistics và thương mại biên giới."
,
toneClass
:
"from-[#7c3aed] to-[#c4b5fd]"
,
},
]
as
const
;
]
as
const
;
const
TIN_VCCI_CATEGORY_ID
=
"b89b2ba6-a699-47cb-87e4-0643aea549a9"
;
const
TIN_VCCI_CATEGORY_ID
=
"b89b2ba6-a699-47cb-87e4-0643aea549a9"
;
function
renderSummary
(
summary
?:
string
)
{
const
value
=
summary
?.
trim
()
??
""
;
if
(
!
value
||
!
stripHtml
(
value
))
{
return
null
;
}
return
parse
(
value
);
}
type
TinVcciApiRow
=
{
type
TinVcciApiRow
=
{
id
?:
string
|
null
;
id
?:
string
|
null
;
title
?:
string
|
null
;
title
?:
string
|
null
;
...
@@ -75,11 +103,6 @@ type AboutVcciHcmPageProps = {
...
@@ -75,11 +103,6 @@ type AboutVcciHcmPageProps = {
export
default
function
AboutVcciHcmPage
({
export
default
function
AboutVcciHcmPage
({
post
,
post
,
}:
AboutVcciHcmPageProps
)
{
}:
AboutVcciHcmPageProps
)
{
const
videosQuery
=
useQuery
({
queryKey
:
[
"about-vcci-hcm-video"
],
queryFn
:
()
=>
fetchClientVideos
({
page
:
1
,
pageSize
:
1
}),
staleTime
:
60
*
1000
,
});
const
tinVcciQuery
=
useQuery
({
const
tinVcciQuery
=
useQuery
({
queryKey
:
[
"about-vcci-hcm-tin-vcci"
],
queryKey
:
[
"about-vcci-hcm-tin-vcci"
],
queryFn
:
async
()
=>
{
queryFn
:
async
()
=>
{
...
@@ -109,8 +132,8 @@ export default function AboutVcciHcmPage({
...
@@ -109,8 +132,8 @@ export default function AboutVcciHcmPage({
staleTime
:
60
*
1000
,
staleTime
:
60
*
1000
,
});
});
const
introVideo
=
videosQuery
.
data
?.
rows
[
0
]
??
null
;
const
tinVcciItems
=
tinVcciQuery
.
data
??
[];
const
tinVcciItems
=
tinVcciQuery
.
data
??
[];
const
summaryContent
=
renderSummary
(
post
.
summary
);
return
(
return
(
<>
<>
...
@@ -121,10 +144,10 @@ export default function AboutVcciHcmPage({
...
@@ -121,10 +144,10 @@ export default function AboutVcciHcmPage({
</
h1
>
</
h1
>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
{
post
.
summary
?
(
{
summaryContent
?
(
<
p
className=
"mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
<
div
className=
"mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
{
post
.
summary
}
{
summaryContent
}
</
p
>
</
div
>
)
:
null
}
)
:
null
}
<
div
className=
"mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10"
>
<
div
className=
"mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10"
>
...
@@ -140,9 +163,9 @@ export default function AboutVcciHcmPage({
...
@@ -140,9 +163,9 @@ export default function AboutVcciHcmPage({
</
h2
>
</
h2
>
<
div
className=
"mt-6 space-y-4"
>
<
div
className=
"mt-6 space-y-4"
>
{
ACTIVITY_AREAS
.
map
((
item
)
=>
(
{
ACTIVITY_AREAS
.
map
((
item
)
=>
(
<
div
key=
{
item
}
className=
"flex items-center gap-3 text-[18px] text-[#58667d]"
>
<
div
key=
{
item
.
name
}
className=
"flex items-center gap-3 text-[18px] text-[#58667d]"
>
<
span
className=
"h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]"
/>
<
span
className=
"h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]"
/>
<
span
>
{
item
}
</
span
>
<
span
>
{
item
.
name
}
</
span
>
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
...
@@ -234,68 +257,44 @@ export default function AboutVcciHcmPage({
...
@@ -234,68 +257,44 @@ export default function AboutVcciHcmPage({
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"
grid gap-6 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] lg:items-center
"
>
<
div
className=
"
rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10
"
>
<
div
className=
"
space-y-5
"
>
<
div
className=
"
flex items-start justify-between gap-4
"
>
<
div
>
<
div
className=
"min-w-0"
>
<
h2
className=
"text-[
28px] font-bold leading-tight text-[#1f2a44] md:text-[34px
]"
>
<
h2
className=
"text-[
30px] font-bold leading-tight text-[#1f2a44
]"
>
Video về
<
span
className=
"text-[#2f57ff]"
>
VCCI-HCM
</
span
>
Khu vực hoạt động
</
h2
>
</
h2
>
<
div
className=
"mt-3 h-1 w-16 rounded-full bg-[#f5a400]"
/>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
<
p
className=
"mt-4 max-w-3xl text-[16px] leading-8 text-[#5f6f86]"
>
VCCI-HCM hoạt động tại 4 khu vực trọng điểm, bảo đảm hỗ trợ doanh nghiệp theo từng địa bàn cụ thể.
</
p
>
</
div
>
</
div
>
<
p
className=
"max-w-xl text-[15px] leading-7 text-[#66758d]"
>
<
div
className=
"hidden rounded-[18px] border border-[#edf1f6] bg-[#f8fbff] px-4 py-3 text-sm font-medium text-[#2450b5] md:block"
>
Khám phá hoạt động của Liên đoàn Thương mại và Công nghiệp Việt Nam - Chi nhánh TP. Hồ Chí Minh qua video giới thiệu chính thức.
4 điểm hoạt động chính
</
p
>
<
div
className=
"grid max-w-md grid-cols-3 gap-3"
>
{
[
{
value
:
"20+"
,
label
:
"Năm hoạt động"
},
{
value
:
"5000+"
,
label
:
"Hội viên"
},
{
value
:
"6"
,
label
:
"Tỉnh thành"
},
].
map
((
stat
)
=>
(
<
div
key=
{
stat
.
label
}
className=
"rounded-[18px] border border-[#edf1f6] bg-white px-4 py-3 text-center shadow-[0_12px_28px_rgba(17,24,39,0.04)]"
>
<
div
className=
"text-[28px] font-bold leading-none text-[#2450b5]"
>
{
stat
.
value
}
</
div
>
<
div
className=
"mt-2 text-[13px] text-[#6b7a91]"
>
{
stat
.
label
}
</
div
>
</
div
>
))
}
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"overflow-hidden rounded-[28px] bg-[#dde6f5] shadow-[0_20px_44px_rgba(29,65,138,0.18)]"
>
<
div
className=
"mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4"
>
{
introVideo
?
(
{
ACTIVITY_AREAS
.
map
((
item
,
index
)
=>
(
<
a
<
article
href=
{
introVideo
.
watchUrl
}
key=
{
item
.
name
}
target=
"_blank"
className=
"rounded-[22px] border border-[#edf1f6] bg-[#fbfcff] px-5 py-5 shadow-[0_10px_26px_rgba(17,24,39,0.04)]"
rel=
"noreferrer"
className=
"group relative block aspect-video"
>
>
<
ImageNext
<
div
className=
"flex items-center gap-3"
>
src=
{
introVideo
.
thumbnail
}
<
span
className=
"inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-lg font-bold text-[#2450b5]"
>
alt=
{
introVideo
.
name
}
{
index
+
1
}
width=
{
1200
}
height=
{
675
}
className=
"h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
/>
<
div
className=
"absolute inset-0 bg-linear-to-r from-[#183b78]/78 via-[#365f9d]/38 to-[#183b78]/24"
/>
<
div
className=
"absolute inset-0 flex items-center justify-center"
>
<
span
className=
"flex h-18 w-18 items-center justify-center rounded-full bg-white/90 text-[#24469c] shadow-[0_18px_38px_rgba(0,0,0,0.18)]"
>
<
Play
className=
"ml-1 h-8 w-8 fill-current"
/>
</
span
>
</
span
>
<
div
className=
"min-w-0"
>
<
h3
className=
"text-[20px] font-bold leading-tight text-[#1f2a44]"
>
{
item
.
name
}
</
h3
>
</
div
>
</
div
>
</
div
>
<
div
className=
"absolute bottom-4 left-4 rounded-full bg-[#1b2e4f]/72 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm"
>
<
p
className=
"mt-4 text-[15px] leading-7 text-[#5f6f86]"
>
{
introVideo
.
name
}
{
item
.
description
}
</
div
>
</
p
>
</
a
>
</
article
>
)
:
(
))
}
<
div
className=
"flex aspect-video items-center justify-center bg-[#eef3fb] px-6 text-center text-[#6b7a91]"
>
Đang cập nhật video giới thiệu VCCI-HCM.
</
div
>
)
}
</
div
>
</
div
>
</
div
>
</
div
>
...
@@ -316,7 +315,7 @@ export default function AboutVcciHcmPage({
...
@@ -316,7 +315,7 @@ export default function AboutVcciHcmPage({
</
Link
>
</
Link
>
</
div
>
</
div
>
<
div
className=
"grid
pb-6 gap-5
md:grid-cols-2 xl:grid-cols-3"
>
<
div
className=
"grid
gap-5 pb-6
md:grid-cols-2 xl:grid-cols-3"
>
{
tinVcciItems
.
map
((
item
)
=>
(
{
tinVcciItems
.
map
((
item
)
=>
(
<
Link
<
Link
key=
{
item
.
id
}
key=
{
item
.
id
}
...
...
src/app/(main)/[...slug]/templates/information-pages/DefaultInformationPage.tsx
View file @
cab5fb7a
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
import
StructuredPostContent
from
"../StructuredPostContent"
;
import
StructuredPostContent
from
"../StructuredPostContent"
;
import
type
{
DynamicPostItem
}
from
"../types"
;
import
type
{
DynamicPostItem
}
from
"../types"
;
import
parse
from
"html-react-parser"
;
type
DefaultInformationPageProps
=
{
type
DefaultInformationPageProps
=
{
post
:
DynamicPostItem
;
post
:
DynamicPostItem
;
};
};
...
@@ -20,7 +20,7 @@ export default function DefaultInformationPage({
...
@@ -20,7 +20,7 @@ export default function DefaultInformationPage({
{
post
.
summary
?
(
{
post
.
summary
?
(
<
p
className=
"mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
<
p
className=
"mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
{
p
ost
.
summary
}
{
p
arse
(
post
.
summary
)
}
</
p
>
</
p
>
)
:
null
}
)
:
null
}
...
...
src/app/(main)/[...slug]/templates/information-pages/ServicePage.tsx
View file @
cab5fb7a
...
@@ -47,7 +47,7 @@ const SERVICE_SUPPORT_ITEMS = [
...
@@ -47,7 +47,7 @@ const SERVICE_SUPPORT_ITEMS = [
},
},
{
{
key
:
"certificate"
,
key
:
"certificate"
,
title
:
"
Cấp C/O và xác nhận các chứng từ thương mại
"
,
title
:
"
Xác nhận các chứng từ thương mại (Bỏ cấp C/O)
"
,
icon
:
FileBadge2
,
icon
:
FileBadge2
,
},
},
{
{
...
...
src/components/admin/rich-text-editor.tsx
View file @
cab5fb7a
...
@@ -3,6 +3,8 @@
...
@@ -3,6 +3,8 @@
import
dynamic
from
"next/dynamic"
;
import
dynamic
from
"next/dynamic"
;
import
{
useMemo
,
useRef
}
from
"react"
;
import
{
useMemo
,
useRef
}
from
"react"
;
import
type
{
JoditEditorProps
}
from
"jodit-react"
;
import
type
{
JoditEditorProps
}
from
"jodit-react"
;
import
useAuthStore
from
"@/store/useAuthStore"
;
import
links
from
"@/links"
;
const
JoditEditor
=
dynamic
(()
=>
import
(
"jodit-react"
).
then
((
mod
)
=>
mod
.
default
),
{
const
JoditEditor
=
dynamic
(()
=>
import
(
"jodit-react"
).
then
((
mod
)
=>
mod
.
default
),
{
ssr
:
false
,
ssr
:
false
,
...
@@ -104,6 +106,7 @@ export function AdminRichTextEditor({
...
@@ -104,6 +106,7 @@ export function AdminRichTextEditor({
readOnly
=
false
,
readOnly
=
false
,
}:
AdminRichTextEditorProps
)
{
}:
AdminRichTextEditorProps
)
{
const
editor
=
useRef
(
null
);
const
editor
=
useRef
(
null
);
const
accessToken
=
useAuthStore
((
state
)
=>
state
.
appAccessToken
);
const
config
:
JoditEditorProps
[
"config"
]
=
useMemo
(
const
config
:
JoditEditorProps
[
"config"
]
=
useMemo
(
()
=>
({
()
=>
({
...
@@ -113,7 +116,52 @@ export function AdminRichTextEditor({
...
@@ -113,7 +116,52 @@ export function AdminRichTextEditor({
language
:
"vi"
,
language
:
"vi"
,
toolbarButtonSize
:
"middle"
,
toolbarButtonSize
:
"middle"
,
uploader
:
{
uploader
:
{
insertImageAsBase64URI
:
true
,
url
:
`
${
links
.
apiEndpoint
}
/files`
,
method
:
"POST"
,
headers
:
{
...(
accessToken
?
{
Authorization
:
`Bearer
${
accessToken
}
`
}
:
{}),
},
format
:
"json"
,
buildData
:
(
data
:
FormData
)
=>
{
const
next
=
new
FormData
();
data
.
forEach
((
value
,
key
)
=>
{
if
(
key
===
"files[]"
&&
value
instanceof
File
)
{
next
.
append
(
"file"
,
value
);
}
else
{
next
.
append
(
key
,
value
);
}
});
return
next
;
},
isSuccess
:
(
resp
:
unknown
)
=>
{
if
(
typeof
resp
!==
"object"
||
resp
===
null
)
return
false
;
const
r
=
resp
as
Record
<
string
,
unknown
>
;
return
(
r
.
status
===
"success"
||
r
.
status
===
"200"
||
(
!
r
.
violation
&&
!!
r
.
responseData
)
);
},
process
:
(
resp
:
unknown
)
=>
{
if
(
typeof
resp
!==
"object"
||
resp
===
null
)
{
return
{
files
:
[],
error
:
"Invalid response"
};
}
const
r
=
resp
as
Record
<
string
,
unknown
>
;
const
responseData
=
r
.
responseData
as
Record
<
string
,
unknown
>
|
undefined
;
const
url
=
(
typeof
responseData
?.
url
===
"string"
&&
responseData
.
url
)
||
(
typeof
responseData
?.
fileUrl
===
"string"
&&
responseData
.
fileUrl
)
||
(
typeof
responseData
?.
path
===
"string"
&&
responseData
.
path
)
||
(
typeof
responseData
?.
link
===
"string"
&&
responseData
.
link
)
||
(
typeof
responseData
?.
src
===
"string"
&&
responseData
.
src
)
||
""
;
return
{
files
:
url
?
[
url
]
:
[],
baseurl
:
""
,
error
:
url
?
undefined
:
(
r
.
message
as
string
)
||
"Upload failed"
,
msg
:
(
r
.
message
as
string
)
||
""
,
};
},
},
},
buttons
:
[
buttons
:
[
"bold"
,
"bold"
,
...
@@ -182,7 +230,7 @@ export function AdminRichTextEditor({
...
@@ -182,7 +230,7 @@ export function AdminRichTextEditor({
},
},
},
},
}),
}),
[
minHeight
,
placeholder
,
readOnly
],
[
minHeight
,
placeholder
,
readOnly
,
accessToken
],
);
);
return
(
return
(
...
...
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