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
4d80d722
Commit
4d80d722
authored
Sep 18, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Plain Diff
[tag]0.1-vcci
parents
2da97d7d
17ad281c
Pipeline
#53249
failed with stages
in 3 minutes and 27 seconds
Changes
28
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
602 additions
and
334 deletions
+602
-334
post.ts
src/api/vcci-news/types/post.ts
+74
-0
DynamicPageClient.tsx
src/app/(main)/[...slug]/DynamicPageClient.tsx
+5
-4
page.tsx
src/app/(main)/[...slug]/page.tsx
+1
-1
AboutVcciHcm.tsx
src/app/(main)/[...slug]/static-pages/AboutVcciHcm.tsx
+5
-4
AnPham.tsx
src/app/(main)/[...slug]/static-pages/AnPham.tsx
+2
-1
MarketProfile.tsx
src/app/(main)/[...slug]/static-pages/MarketProfile.tsx
+2
-1
MemberRegistration.tsx
src/app/(main)/[...slug]/static-pages/MemberRegistration.tsx
+2
-1
Search.tsx
src/app/(main)/[...slug]/static-pages/Search.tsx
+1
-1
Service.tsx
src/app/(main)/[...slug]/static-pages/Service.tsx
+2
-1
ThuVienTaiLieu.tsx
src/app/(main)/[...slug]/static-pages/ThuVienTaiLieu.tsx
+2
-1
data.ts
src/app/(main)/[...slug]/templates/data.ts
+3
-1
index.tsx
...app/(main)/[...slug]/templates/information-page/index.tsx
+50
-0
event-info-card.tsx
...templates/news-detail-page/components/event-info-card.tsx
+103
-0
post-summary-content.tsx
...ates/news-detail-page/components/post-summary-content.tsx
+26
-0
index.tsx
...app/(main)/[...slug]/templates/news-detail-page/index.tsx
+72
-0
index.tsx
src/app/(main)/[...slug]/templates/news-page/index.tsx
+4
-3
types.ts
src/app/(main)/[...slug]/templates/types.ts
+0
-74
AppEditorContent.css
src/components/shared/editor-content/AppEditorContent.css
+0
-13
AppEditorContent.tsx
src/components/shared/editor-content/AppEditorContent.tsx
+0
-59
AppEditorContent.type.ts
...components/shared/editor-content/AppEditorContent.type.ts
+0
-7
index.tsx
src/components/shared/editor-content/index.tsx
+0
-2
image-section.tsx
...mponents/shared/post-content/components/image-section.tsx
+70
-0
text-section.tsx
...omponents/shared/post-content/components/text-section.tsx
+89
-101
index.tsx
src/components/shared/post-content/index.tsx
+27
-0
fallback-sidebar-ad-item.tsx
...ar-advertisements/components/fallback-sidebar-ad-item.tsx
+26
-0
sidebar-ad-item.tsx
...red/sidebar-advertisements/components/sidebar-ad-item.tsx
+29
-0
index.tsx
src/components/shared/sidebar-advertisements/index.tsx
+2
-54
time-picker.tsx
src/components/shared/time-picker.tsx
+5
-5
No files found.
src/api/vcci-news/types/post.ts
View file @
4d80d722
...
@@ -101,3 +101,77 @@ export interface AdminNewsFormValues {
...
@@ -101,3 +101,77 @@ export interface AdminNewsFormValues {
event_dates
?:
string
[];
event_dates
?:
string
[];
post_content
:
AdminNewsContentSection
[];
post_content
:
AdminNewsContentSection
[];
}
}
export
type
DynamicPostCategoryItem
=
{
id
:
string
;
name
:
string
;
slug
:
string
;
url
:
string
;
type
:
string
;
};
export
type
DynamicPostThumbnail
=
{
path
?:
string
|
null
;
original
?:
string
|
null
;
url
?:
string
|
null
;
}
|
null
;
export
type
DynamicPostUser
=
{
id
:
string
;
email
:
string
;
username
:
string
|
null
;
first_name
:
string
|
null
;
last_name
:
string
|
null
;
full_name
:
string
;
avatar_url
:
string
|
null
;
}
|
null
;
export
type
DynamicPostContentSection
=
{
id
:
string
;
type
:
string
;
content
:
string
;
position
:
number
;
image_rows
:
number
;
image_columns
:
number
;
images
:
Array
<
{
position
:
number
;
caption
:
string
|
null
;
image
:
{
id
:
string
;
name
:
string
;
alt
:
string
;
url
:
string
;
path
?:
string
|
null
;
original
?:
string
|
null
;
}
|
null
;
}
>
;
};
export
type
DynamicPostItem
=
{
id
:
string
;
title
:
string
;
slug
:
string
;
content
:
string
;
summary
:
string
;
release_at
:
string
|
null
;
published_at
:
string
|
null
;
created_at
:
string
|
null
;
started_at
:
string
|
null
;
ended_at
:
string
|
null
;
expired_at
:
string
|
null
;
registration_deadline
:
string
|
null
;
location
:
string
|
null
;
participation_fee
:
string
|
null
;
is_featured
:
boolean
;
is_hidden
:
boolean
;
is_active
:
boolean
;
status
:
string
;
type
:
string
;
thumbnail
:
DynamicPostThumbnail
;
categories
:
DynamicPostCategoryItem
[];
content_structure
:
{
post_content
:
DynamicPostContentSection
[];
}
|
null
;
creator
:
DynamicPostUser
;
editor
:
DynamicPostUser
;
};
src/app/(main)/[...slug]/DynamicPageClient.tsx
View file @
4d80d722
...
@@ -3,9 +3,9 @@
...
@@ -3,9 +3,9 @@
import
{
useEffect
,
useMemo
}
from
"react"
;
import
{
useEffect
,
useMemo
}
from
"react"
;
import
{
notFound
,
useParams
,
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
notFound
,
useParams
,
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
Spinner
}
from
"@/components/ui"
;
import
{
Spinner
}
from
"@/components/ui"
;
import
NewsPage
from
"./templates/
NewsP
age"
;
import
NewsPage
from
"./templates/
news-p
age"
;
import
NewsDetailPage
from
"./templates/
NewsDetailP
age"
;
import
NewsDetailPage
from
"./templates/
news-detail-p
age"
;
import
InformationPage
from
"./templates/
InformationP
age"
;
import
InformationPage
from
"./templates/
information-p
age"
;
import
AnPham
from
"./static-pages/AnPham"
;
import
AnPham
from
"./static-pages/AnPham"
;
import
ThuVienTaiLieu
from
"./static-pages/ThuVienTaiLieu"
;
import
ThuVienTaiLieu
from
"./static-pages/ThuVienTaiLieu"
;
import
AboutVcciHcm
from
"./static-pages/AboutVcciHcm"
;
import
AboutVcciHcm
from
"./static-pages/AboutVcciHcm"
;
...
@@ -26,7 +26,8 @@ import SiteMap from "./static-pages/SiteMap";
...
@@ -26,7 +26,8 @@ import SiteMap from "./static-pages/SiteMap";
import
Video
from
"./static-pages/Video"
;
import
Video
from
"./static-pages/Video"
;
import
{
useGetApiV10Post
}
from
"@/api/vcci-news/endpoints/post"
;
import
{
useGetApiV10Post
}
from
"@/api/vcci-news/endpoints/post"
;
import
{
useGetApiV10Category
}
from
"@/api/vcci-news/endpoints/category"
;
import
{
useGetApiV10Category
}
from
"@/api/vcci-news/endpoints/category"
;
import
type
{
DynamicPostItem
,
DynamicCategoryRouteItem
}
from
"./templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"./templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
const
normalizePath
=
(
value
?:
string
|
null
)
=>
{
const
normalizePath
=
(
value
?:
string
|
null
)
=>
{
const
trimmed
=
value
?.
trim
()
??
""
;
const
trimmed
=
value
?.
trim
()
??
""
;
...
...
src/app/(main)/[...slug]/page.tsx
View file @
4d80d722
...
@@ -8,7 +8,7 @@ import {
...
@@ -8,7 +8,7 @@ import {
import
{
getApiV10Post
,
getApiV10PostId
}
from
"@/api/vcci-news/endpoints/post"
;
import
{
getApiV10Post
,
getApiV10PostId
}
from
"@/api/vcci-news/endpoints/post"
;
import
{
getApiV10Category
}
from
"@/api/vcci-news/endpoints/category"
;
import
{
getApiV10Category
}
from
"@/api/vcci-news/endpoints/category"
;
import
{
categoryFallbackRows
}
from
"@/mockdata/categories"
;
import
{
categoryFallbackRows
}
from
"@/mockdata/categories"
;
import
type
{
DynamicPostItem
}
from
"
./templates/types
"
;
import
type
{
DynamicPostItem
}
from
"
@/api/vcci-news/types/post
"
;
import
DynamicPageClient
from
"./DynamicPageClient"
;
import
DynamicPageClient
from
"./DynamicPageClient"
;
const
STATIC_PAGE_SLUGS
=
new
Set
([
const
STATIC_PAGE_SLUGS
=
new
Set
([
...
...
src/app/(main)/[...slug]/static-pages/AboutVcciHcm.tsx
View file @
4d80d722
...
@@ -13,8 +13,9 @@ import {
...
@@ -13,8 +13,9 @@ import {
buildDynamicCategoryMenu
,
buildDynamicCategoryMenu
,
stripHtml
,
stripHtml
,
}
from
"../templates/data"
;
}
from
"../templates/data"
;
import
StructuredPostContent
from
"../templates/StructuredPostContent"
;
import
PostContent
from
"@/components/shared/post-content"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"../templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
const
ABOUT_HIGHLIGHTS
=
[
const
ABOUT_HIGHLIGHTS
=
[
{
{
...
@@ -165,8 +166,8 @@ export default function AboutVcciHcm({ post, category, allCategories }: AboutVcc
...
@@ -165,8 +166,8 @@ export default function AboutVcciHcm({ post, category, allCategories }: AboutVcc
)
:
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"
>
<
div
className=
"about-vcci-page-content
page-detail-content prose tiptap max-w-none overflow-hidden
"
>
<
div
className=
"about-vcci-page-content"
>
<
Structured
PostContent
post=
{
post
}
/>
<
PostContent
post=
{
post
}
/>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/app/(main)/[...slug]/static-pages/AnPham.tsx
View file @
4d80d722
...
@@ -18,7 +18,8 @@ import {
...
@@ -18,7 +18,8 @@ import {
resolveDynamicPostImage
,
resolveDynamicPostImage
,
mapPost
,
mapPost
,
}
from
"../templates/data"
;
}
from
"../templates/data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"../templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
type
AnPhamProps
=
{
type
AnPhamProps
=
{
category
:
DynamicCategoryRouteItem
|
null
;
category
:
DynamicCategoryRouteItem
|
null
;
...
...
src/app/(main)/[...slug]/static-pages/MarketProfile.tsx
View file @
4d80d722
...
@@ -5,7 +5,8 @@ import { FileText, Globe2, Newspaper, TrendingUp } from "lucide-react";
...
@@ -5,7 +5,8 @@ import { FileText, Globe2, Newspaper, TrendingUp } from "lucide-react";
import
{
SafeImage
}
from
"@/components/shared/safe-image"
;
import
{
SafeImage
}
from
"@/components/shared/safe-image"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
{
buildDynamicCategoryMenu
}
from
"../templates/data"
;
import
{
buildDynamicCategoryMenu
}
from
"../templates/data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"../templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
type
RegionMarketItem
=
{
type
RegionMarketItem
=
{
name
:
string
;
name
:
string
;
...
...
src/app/(main)/[...slug]/static-pages/MemberRegistration.tsx
View file @
4d80d722
...
@@ -3,7 +3,8 @@
...
@@ -3,7 +3,8 @@
import
links
from
"@/links"
;
import
links
from
"@/links"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
{
buildDynamicCategoryMenu
}
from
"../templates/data"
;
import
{
buildDynamicCategoryMenu
}
from
"../templates/data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"../templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
const
MEMBERSHIP_REQUIREMENTS
=
[
const
MEMBERSHIP_REQUIREMENTS
=
[
"Đơn xin gia nhập làm hội viên chính thức VCCI (2 bản theo mẫu của VCCI)"
,
"Đơn xin gia nhập làm hội viên chính thức VCCI (2 bản theo mẫu của VCCI)"
,
...
...
src/app/(main)/[...slug]/static-pages/Search.tsx
View file @
4d80d722
...
@@ -19,7 +19,7 @@ import {
...
@@ -19,7 +19,7 @@ import {
resolveDynamicPostImage
,
resolveDynamicPostImage
,
mapPost
,
mapPost
,
}
from
"../templates/data"
;
}
from
"../templates/data"
;
import
type
{
DynamicPostItem
}
from
"
../templates/types
"
;
import
type
{
DynamicPostItem
}
from
"
@/api/vcci-news/types/post
"
;
const
formatPostDate
=
(
value
?:
string
|
null
)
=>
{
const
formatPostDate
=
(
value
?:
string
|
null
)
=>
{
if
(
!
value
)
return
""
;
if
(
!
value
)
return
""
;
...
...
src/app/(main)/[...slug]/static-pages/Service.tsx
View file @
4d80d722
...
@@ -16,7 +16,8 @@ import ListCategory from "@/components/base/list-category";
...
@@ -16,7 +16,8 @@ import ListCategory from "@/components/base/list-category";
import
{
import
{
buildDynamicCategoryMenu
,
buildDynamicCategoryMenu
,
}
from
"../templates/data"
;
}
from
"../templates/data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"../templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
const
SERVICE_SUPPORT_ITEMS
=
[
const
SERVICE_SUPPORT_ITEMS
=
[
{
{
...
...
src/app/(main)/[...slug]/static-pages/ThuVienTaiLieu.tsx
View file @
4d80d722
...
@@ -18,7 +18,8 @@ import {
...
@@ -18,7 +18,8 @@ import {
resolveDynamicPostImage
,
resolveDynamicPostImage
,
mapPost
,
mapPost
,
}
from
"../templates/data"
;
}
from
"../templates/data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"../templates/types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../templates/types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
type
ThuVienTaiLieuProps
=
{
type
ThuVienTaiLieuProps
=
{
category
:
DynamicCategoryRouteItem
|
null
;
category
:
DynamicCategoryRouteItem
|
null
;
...
...
src/app/(main)/[...slug]/templates/data.ts
View file @
4d80d722
...
@@ -4,11 +4,13 @@ import type {
...
@@ -4,11 +4,13 @@ import type {
DynamicCategoryMenuItem
,
DynamicCategoryMenuItem
,
DynamicCategoryRouteItem
,
DynamicCategoryRouteItem
,
DynamicCategoryType
,
DynamicCategoryType
,
}
from
"./types"
;
import
type
{
DynamicPostContentSection
,
DynamicPostContentSection
,
DynamicPostItem
,
DynamicPostItem
,
DynamicPostThumbnail
,
DynamicPostThumbnail
,
DynamicPostUser
,
DynamicPostUser
,
}
from
"
./types
"
;
}
from
"
@/api/vcci-news/types/post
"
;
type
RawPostCategory
=
{
type
RawPostCategory
=
{
id
?:
string
|
null
;
id
?:
string
|
null
;
...
...
src/app/(main)/[...slug]/templates/
InformationPage
.tsx
→
src/app/(main)/[...slug]/templates/
information-page/index
.tsx
View file @
4d80d722
...
@@ -2,9 +2,10 @@
...
@@ -2,9 +2,10 @@
import
ListCategory
from
"@/components/base/list-category"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
parse
from
"html-react-parser"
;
import
parse
from
"html-react-parser"
;
import
{
buildDynamicCategoryMenu
}
from
"./data"
;
import
{
buildDynamicCategoryMenu
}
from
"../data"
;
import
StructuredPostContent
from
"./StructuredPostContent"
;
import
PostContent
from
"@/components/shared/post-content"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
type
InformationPageProps
=
{
type
InformationPageProps
=
{
post
:
DynamicPostItem
;
post
:
DynamicPostItem
;
...
@@ -38,101 +39,10 @@ export default function InformationPage({
...
@@ -38,101 +39,10 @@ export default function InformationPage({
)
:
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"
>
<
div
className=
"page-detail-content prose tiptap max-w-none overflow-hidden"
>
<
PostContent
post=
{
post
}
/>
<
StructuredPostContent
post=
{
post
}
/>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
section
>
</
section
>
<
div
className=
"page-detail-styles"
>
<
style
jsx
global
>
{
`
.page-detail-content {
color: #1f2937;
line-height: 1.85;
width: 100%;
max-width: 100%;
}
.page-detail-content p,
.page-detail-content div {
margin: 0 0 18px;
max-width: 100% !important;
box-sizing: border-box;
}
.page-detail-content h1,
.page-detail-content h2,
.page-detail-content h3,
.page-detail-content h4,
.page-detail-content h5,
.page-detail-content h6 {
margin: 0 0 18px;
color: #111827;
font-weight: 700;
line-height: 1.45;
}
.page-detail-content :is(p, div, span, li, a, strong, em, u, s) {
font-family: inherit;
}
.page-detail-content img {
display: block;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin: 24px auto 10px;
border-radius: 14px;
}
.page-detail-content figure {
display: block !important;
width: 100% !important;
max-width: 100% !important;
margin: 28px 0;
text-align: center;
}
.page-detail-content .article-content,
.page-detail-content .article-content_toc,
.page-detail-content table,
.page-detail-content iframe {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.page-detail-content table {
display: table;
table-layout: fixed;
}
.page-detail-content figcaption,
.page-detail-content .wp-caption-text {
margin-top: 10px;
color: #6b7280;
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.page-detail-content a {
color: #14519f;
font-weight: 600;
}
.page-detail-content ul,
.page-detail-content ol {
margin: 18px 0;
padding-left: 24px;
}
.page-detail-content li {
margin: 8px 0;
}
`
}
</
style
>
</
div
>
</
main
>
</
main
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/app/(main)/[...slug]/templates/
NewsDetailPage
.tsx
→
src/app/(main)/[...slug]/templates/
news-detail-page/components/event-info-card
.tsx
View file @
4d80d722
'use client'
;
import
dayjs
from
"dayjs"
;
import
dayjs
from
"dayjs"
;
import
AppEditorContent
from
"@/components/shared/editor-content"
;
import
{
Calendar
,
MapPin
,
Clock
,
Users
,
CreditCard
}
from
"lucide-react"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
import
EventsCalendar
from
"@/app/(main)/(home)/components/events-calendar"
;
import
SidebarAdvertisements
from
"@/components/shared/sidebar-advertisements"
;
import
{
Calendar
,
MapPin
,
Clock
,
DollarSign
,
Users
,
CreditCard
}
from
"lucide-react"
;
import
{
buildDynamicCategoryMenu
,
findDisplayCategoryForPost
}
from
"./data"
;
import
StructuredPostContent
from
"./StructuredPostContent"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
const
formatDate
=
(
value
:
string
|
null
)
=>
const
formatDate
=
(
value
:
string
|
null
)
=>
value
?
dayjs
(
value
).
format
(
"DD/MM/YYYY"
)
:
""
;
value
?
dayjs
(
value
).
format
(
"DD/MM/YYYY"
)
:
""
;
...
@@ -36,7 +28,7 @@ const hasEventInfo = (post: DynamicPostItem) => {
...
@@ -36,7 +28,7 @@ const hasEventInfo = (post: DynamicPostItem) => {
);
);
};
};
const
EventInfoCard
=
({
post
}:
{
post
:
DynamicPostItem
})
=>
{
export
default
function
EventInfoCard
({
post
}:
{
post
:
DynamicPostItem
})
{
if
(
!
isEventOrTraining
(
post
)
||
!
hasEventInfo
(
post
))
return
null
;
if
(
!
isEventOrTraining
(
post
)
||
!
hasEventInfo
(
post
))
return
null
;
const
startedAt
=
post
.
started_at
;
const
startedAt
=
post
.
started_at
;
...
@@ -108,157 +100,4 @@ const EventInfoCard = ({ post }: { post: DynamicPostItem }) => {
...
@@ -108,157 +100,4 @@ const EventInfoCard = ({ post }: { post: DynamicPostItem }) => {
</
div
>
</
div
>
</
div
>
</
div
>
);
);
};
type
NewsDetailPageProps
=
{
post
:
DynamicPostItem
;
category
:
DynamicCategoryRouteItem
|
null
;
allCategories
:
DynamicCategoryRouteItem
[];
};
export
default
function
NewsDetailPage
({
post
,
category
,
allCategories
,
}:
NewsDetailPageProps
)
{
const
publishedDate
=
dayjs
(
post
.
release_at
??
post
.
published_at
??
post
.
created_at
,
).
format
(
"DD/MM/YYYY"
);
const
primaryCategory
=
findDisplayCategoryForPost
(
post
,
category
,
allCategories
)?.
name
||
category
?.
name
||
"Tin tức"
;
const
categoryMenu
=
category
?
buildDynamicCategoryMenu
(
category
,
allCategories
)
:
[];
return
(
<
div
className=
"min-h-screen bg-white"
>
{
/* {categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null} */
}
<
div
className=
"container mx-auto px-4 py-4 lg:pb-6 sm:px-6 lg:px-10"
>
<
div
className=
"grid grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_340px] xl:gap-12"
>
<
main
className=
"min-w-0"
>
<
div
className=
"mb-5 flex flex-wrap items-center gap-3 text-xs"
>
<
span
className=
"rounded-full bg-[#eaf0ff] px-2.5 py-1 font-semibold text-[#1f4fa3]"
>
{
primaryCategory
}
</
span
>
<
span
className=
"text-[#9aa3ad]"
>
{
publishedDate
}
</
span
>
</
div
>
<
h1
className=
"max-w-4xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]"
>
{
post
.
title
}
</
h1
>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
{
post
.
summary
?
(
<
div
className=
"mt-5 max-w-4xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
<
AppEditorContent
value=
{
post
.
summary
}
/>
</
div
>
)
:
null
}
<
EventInfoCard
post=
{
post
}
/>
<
div
className=
"mt-7 rounded-3xl bg-white px-4 py-5 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 sm:py-6 lg:px-10"
>
<
div
className=
"article-detail-content prose tiptap max-w-none overflow-hidden"
>
<
StructuredPostContent
post=
{
post
}
/>
</
div
>
</
div
>
<
div
className=
"article-detail-styles"
>
<
style
jsx
global
>
{
`
.article-detail-content {
color: #1f2937;
line-height: 1.85;
width: 100%;
max-width: 100%;
}
.article-detail-content p,
.article-detail-content div {
margin: 0 0 18px;
max-width: 100% !important;
box-sizing: border-box;
}
.article-detail-content h1,
.article-detail-content h2,
.article-detail-content h3,
.article-detail-content h4,
.article-detail-content h5,
.article-detail-content h6 {
margin: 0 0 18px;
color: #111827;
font-weight: 700;
line-height: 1.45;
}
.article-detail-content :is(p, div, span, li, a, strong, em, u, s) {
font-family: inherit;
}
.article-detail-content img {
display: block;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin: 24px auto 10px;
border-radius: 14px;
}
.article-detail-content figure {
display: block !important;
width: 100% !important;
max-width: 100% !important;
margin: 28px 0;
text-align: center;
}
.article-detail-content .article-content,
.article-detail-content .article-content_toc,
.article-detail-content table,
.article-detail-content iframe {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.article-detail-content table {
display: table;
table-layout: fixed;
}
.article-detail-content figcaption,
.article-detail-content .wp-caption-text {
margin-top: 10px;
color: #6b7280;
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.article-detail-content a {
color: #14519f;
font-weight: 600;
}
.article-detail-content ul,
.article-detail-content ol {
margin: 18px 0;
padding-left: 24px;
}
.article-detail-content li {
margin: 8px 0;
}
`
}
</
style
>
</
div
>
</
main
>
<
aside
className=
"space-y-5 xl:pt-0"
>
<
EventsCalendar
compact
className=
"xl:w-full xl:min-w-0"
/>
<
SidebarAdvertisements
count=
{
3
}
startIndex=
{
0
}
/>
</
aside
>
</
div
>
</
div
>
</
div
>
);
}
}
src/app/(main)/[...slug]/templates/news-detail-page/components/post-summary-content.tsx
0 → 100644
View file @
4d80d722
import
parse
,
{
domToReact
,
DOMNode
,
Element
,
type
HTMLReactParserOptions
,
}
from
"html-react-parser"
;
const
summaryParserOptions
:
HTMLReactParserOptions
=
{
replace
:
(
node
)
=>
{
if
(
!
(
node
instanceof
Element
))
return
;
const
tagName
=
node
.
tagName
.
toLowerCase
();
if
(
tagName
===
"img"
)
{
return
<></>;
}
if
(
tagName
===
"a"
)
{
return
<>
{
domToReact
(
node
.
children
as
DOMNode
[],
summaryParserOptions
)
}
</>;
}
},
};
export
function
renderPostSummary
(
html
:
string
)
{
return
parse
(
html
,
summaryParserOptions
);
}
src/app/(main)/[...slug]/templates/news-detail-page/index.tsx
0 → 100644
View file @
4d80d722
'use client'
;
import
dayjs
from
"dayjs"
;
import
EventsCalendar
from
"@/app/(main)/(home)/components/events-calendar"
;
import
SidebarAdvertisements
from
"@/components/shared/sidebar-advertisements"
;
import
{
findDisplayCategoryForPost
}
from
"../data"
;
import
{
renderPostSummary
}
from
"./components/post-summary-content"
;
import
PostContent
from
"@/components/shared/post-content"
;
import
EventInfoCard
from
"./components/event-info-card"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
type
NewsDetailPageProps
=
{
post
:
DynamicPostItem
;
category
:
DynamicCategoryRouteItem
|
null
;
allCategories
:
DynamicCategoryRouteItem
[];
};
export
default
function
NewsDetailPage
({
post
,
category
,
allCategories
,
}:
NewsDetailPageProps
)
{
const
publishedDate
=
dayjs
(
post
.
release_at
??
post
.
published_at
??
post
.
created_at
).
format
(
"DD/MM/YYYY"
);
const
primaryCategory
=
findDisplayCategoryForPost
(
post
,
category
,
allCategories
)?.
name
||
category
?.
name
||
"Tin tức"
;
return
(
<
div
className=
"min-h-screen bg-white"
>
<
div
className=
"container mx-auto px-4 py-4 lg:pb-6 sm:px-6 lg:px-10"
>
<
div
className=
"grid grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_340px] xl:gap-12"
>
<
main
className=
"min-w-0"
>
{
/* badge */
}
<
div
className=
"mb-5 flex flex-wrap items-center gap-3 text-xs"
>
<
span
className=
"rounded-full bg-[#eaf0ff] px-2.5 py-1 font-semibold text-[#1f4fa3]"
>
{
primaryCategory
}
</
span
>
<
span
className=
"text-[#9aa3ad]"
>
{
publishedDate
}
</
span
>
</
div
>
{
/* header */
}
<
h1
className=
"max-w-4xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]"
>
{
post
.
title
}
</
h1
>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
{
/* summary */
}
{
post
.
summary
?
(
<
div
className=
"mt-5 max-w-4xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
{
renderPostSummary
(
post
.
summary
)
}
</
div
>
)
:
null
}
{
/* event info */
}
<
EventInfoCard
post=
{
post
}
/>
{
/* content */
}
<
div
className=
"mt-7 rounded-3xl bg-white px-4 py-5 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 sm:py-6 lg:px-10"
>
<
PostContent
post=
{
post
}
/>
</
div
>
</
main
>
{
/* sidebar */
}
<
aside
className=
"space-y-5 xl:pt-0"
>
<
EventsCalendar
compact
className=
"xl:w-full xl:min-w-0"
/>
<
SidebarAdvertisements
count=
{
3
}
startIndex=
{
0
}
/>
</
aside
>
</
div
>
</
div
>
</
div
>
);
}
src/app/(main)/[...slug]/templates/
NewsPage
.tsx
→
src/app/(main)/[...slug]/templates/
news-page/index
.tsx
View file @
4d80d722
...
@@ -18,8 +18,9 @@ import {
...
@@ -18,8 +18,9 @@ import {
findDisplayCategoryForPost
,
findDisplayCategoryForPost
,
getDynamicPostExcerpt
,
getDynamicPostExcerpt
,
resolveDynamicPostImage
,
resolveDynamicPostImage
,
}
from
"./data"
;
}
from
"../data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
type
NewsPageProps
=
{
type
NewsPageProps
=
{
category
:
DynamicCategoryRouteItem
;
category
:
DynamicCategoryRouteItem
;
...
@@ -192,7 +193,7 @@ export default function NewsPage({ category, allCategories }: NewsPageProps) {
...
@@ -192,7 +193,7 @@ export default function NewsPage({ category, allCategories }: NewsPageProps) {
})
})
)
:
(
)
:
(
<
div
className=
"rounded-2xl border border-[#edf1f5] bg-white px-6 py-12 text-center text-gray-600"
>
<
div
className=
"rounded-2xl border border-[#edf1f5] bg-white px-6 py-12 text-center text-gray-600"
>
{
"Ch
\
u01b0a c
\
u00f3 b
\
u00e0i vi
\
u1ebft ph
\
u00f9 h
\
u1ee3p trong danh m
\
u1ee5c n
\
u00e0y."
}
<
p
>
Chưa có bài viết phù hợp trong danh mục này.
</
p
>
</
div
>
</
div
>
)
}
)
}
...
...
src/app/(main)/[...slug]/templates/types.ts
View file @
4d80d722
...
@@ -15,77 +15,3 @@ export type DynamicCategoryMenuItem = {
...
@@ -15,77 +15,3 @@ export type DynamicCategoryMenuItem = {
name
:
string
;
name
:
string
;
static_link
:
string
;
static_link
:
string
;
};
};
export
type
DynamicPostCategoryItem
=
{
id
:
string
;
name
:
string
;
slug
:
string
;
url
:
string
;
type
:
string
;
};
export
type
DynamicPostThumbnail
=
{
path
?:
string
|
null
;
original
?:
string
|
null
;
url
?:
string
|
null
;
}
|
null
;
export
type
DynamicPostUser
=
{
id
:
string
;
email
:
string
;
username
:
string
|
null
;
first_name
:
string
|
null
;
last_name
:
string
|
null
;
full_name
:
string
;
avatar_url
:
string
|
null
;
}
|
null
;
export
type
DynamicPostContentSection
=
{
id
:
string
;
type
:
string
;
content
:
string
;
position
:
number
;
image_rows
:
number
;
image_columns
:
number
;
images
:
Array
<
{
position
:
number
;
caption
:
string
|
null
;
image
:
{
id
:
string
;
name
:
string
;
alt
:
string
;
url
:
string
;
path
?:
string
|
null
;
original
?:
string
|
null
;
}
|
null
;
}
>
;
};
export
type
DynamicPostItem
=
{
id
:
string
;
title
:
string
;
slug
:
string
;
content
:
string
;
summary
:
string
;
release_at
:
string
|
null
;
published_at
:
string
|
null
;
created_at
:
string
|
null
;
started_at
:
string
|
null
;
ended_at
:
string
|
null
;
expired_at
:
string
|
null
;
registration_deadline
:
string
|
null
;
location
:
string
|
null
;
participation_fee
:
string
|
null
;
is_featured
:
boolean
;
is_hidden
:
boolean
;
is_active
:
boolean
;
status
:
string
;
type
:
string
;
thumbnail
:
DynamicPostThumbnail
;
categories
:
DynamicPostCategoryItem
[];
content_structure
:
{
post_content
:
DynamicPostContentSection
[];
}
|
null
;
creator
:
DynamicPostUser
;
editor
:
DynamicPostUser
;
};
src/components/shared/editor-content/AppEditorContent.css
deleted
100644 → 0
View file @
2da97d7d
.app-editor-container
{
min-height
:
unset
!important
;
height
:
auto
!important
;
border
:
none
!important
;
border-radius
:
0px
!important
;
background-color
:
transparent
!important
;
line-height
:
1.5
;
.jodit-wysiwyg
{
padding
:
0
!important
;
overflow-x
:
visible
!important
;
}
}
\ No newline at end of file
src/components/shared/editor-content/AppEditorContent.tsx
deleted
100644 → 0
View file @
2da97d7d
import
{
FC
,
JSX
}
from
"react"
;
import
htmlParse
,
{
DOMNode
,
Element
,
Text
}
from
"html-react-parser"
;
import
{
AppEditorContentProps
}
from
"./AppEditorContent.type"
;
import
"./AppEditorContent.css"
;
const
AppEditorContent
:
FC
<
AppEditorContentProps
>
=
({
value
=
''
,
className
=
''
})
=>
{
const
transform
=
(
node
:
DOMNode
):
JSX
.
Element
|
string
|
undefined
|
null
=>
{
// 1. Xử lý Text Node
if
(
node
instanceof
Text
)
{
return
node
.
data
;
}
if
(
!
(
node
instanceof
Element
))
return
undefined
;
const
tagName
=
node
.
tagName
.
toLowerCase
();
// ✅ FIX LỖI: Ép kiểu (as DOMNode) để Type 'ChildNode' khớp với 'DOMNode'
const
children
=
node
.
children
?
node
.
children
.
map
(
child
=>
transform
(
child
as
DOMNode
))
:
[];
// --- LOGIC XỬ LÝ THEO YÊU CẦU ---
// 2. ✅ Xóa thẻ <img>
if
(
tagName
===
'img'
)
{
return
<></>;
}
// 3. ✅ Xóa thẻ <a> nhưng giữ lại nội dung
if
(
tagName
===
'a'
)
{
// Tr? v? children d? du?c x? l? (l?m ph?ng th? <a>)
return
<>
{
children
}
</>;
}
// 4. ✅ Xử lý thẻ <strong>
if
(
tagName
===
'strong'
)
{
return
(
<
strong
className=
"custom-strong"
>
{
children
}
</
strong
>
);
}
// 5. ✅ Render các thẻ HTML khác (Fallback)
// Trả về undefined để thư viện tự động render các thẻ còn lại (p, div, br,...)
return
undefined
;
};
return
(
<
div
className=
"jodit-container app-editor-container"
>
<
div
className=
{
`jodit-wysiwyg ${className}`
}
>
{
htmlParse
(
value
,
{
replace
:
transform
})
}
</
div
>
</
div
>
);
};
export
default
AppEditorContent
;
\ No newline at end of file
src/components/shared/editor-content/AppEditorContent.type.ts
deleted
100644 → 0
View file @
2da97d7d
// Props
interface
AppEditorContentProps
{
value
:
string
className
?:
string
}
export
type
{
AppEditorContentProps
}
src/components/shared/editor-content/index.tsx
deleted
100644 → 0
View file @
2da97d7d
import
AppEditorContent
from
'./AppEditorContent'
export
default
AppEditorContent
src/components/shared/post-content/components/image-section.tsx
0 → 100644
View file @
4d80d722
import
type
{
DynamicPostContentSection
}
from
"@/api/vcci-news/types/post"
;
import
{
ImageLightbox
}
from
"../../image-lightbox"
;
import
{
SafeImage
}
from
"../../safe-image"
;
import
{
useState
}
from
"react"
;
function
getGridClassName
(
columns
:
number
)
{
if
(
columns
>=
4
)
return
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
;
if
(
columns
===
3
)
return
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
;
if
(
columns
===
2
)
return
"grid-cols-1 sm:grid-cols-2"
;
return
"grid-cols-1"
;
}
export
function
ImageSection
({
section
}:
{
section
:
DynamicPostContentSection
})
{
const
images
=
section
.
images
.
filter
((
item
)
=>
item
.
image
?.
url
);
const
[
activeImage
,
setActiveImage
]
=
useState
<
{
src
:
string
;
alt
:
string
;
caption
?:
string
;
}
|
null
>
(
null
);
if
(
!
images
.
length
)
return
null
;
return
(
<>
<
div
className=
{
`post-image-section my-6 grid gap-4 ${getGridClassName(section.image_columns)}`
}
>
{
images
.
map
((
item
)
=>
{
const
image
=
item
.
image
;
if
(
!
image
?.
url
)
return
null
;
return
(
<
figure
key=
{
`${section.id}-${image.id || image.url}-${item.position}`
}
className=
"group cursor-zoom-in overflow-hidden rounded-[18px] bg-white"
onClick=
{
()
=>
setActiveImage
({
src
:
image
.
url
,
alt
:
image
.
alt
||
image
.
name
||
"Hình ảnh bài viết"
,
caption
:
item
.
caption
??
undefined
,
})
}
>
<
SafeImage
src=
{
image
.
url
}
alt=
{
image
.
alt
||
image
.
name
||
"Hình ảnh bài viết"
}
width=
{
1200
}
height=
{
800
}
className=
"h-auto w-full object-contain transition-transform duration-300 group-hover:scale-[1.02]"
/>
{
item
.
caption
?
(
<
figcaption
className=
"mt-2 text-center text-sm text-gray-600"
>
{
item
.
caption
}
</
figcaption
>
)
:
null
}
</
figure
>
);
})
}
</
div
>
<
ImageLightbox
src=
{
activeImage
?.
src
??
""
}
alt=
{
activeImage
?.
alt
}
caption=
{
activeImage
?.
caption
}
open=
{
Boolean
(
activeImage
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
)
setActiveImage
(
null
);
}
}
/>
</>
);
}
\ No newline at end of file
src/
app/(main)/[...slug]/templates/StructuredPostContent
.tsx
→
src/
components/shared/post-content/components/text-section
.tsx
View file @
4d80d722
"use client"
;
import
{
useState
}
from
"react"
;
import
parse
from
"html-react-parser"
;
import
parse
from
"html-react-parser"
;
import
{
ImageLightbox
}
from
"@/components/shared/image-lightbox"
;
import
type
{
DynamicPostContentSection
}
from
"@/api/vcci-news/types/post"
;
import
{
SafeImage
}
from
"@/components/shared/safe-image"
;
import
{
getDynamicPostBodyHtml
}
from
"./data"
;
import
type
{
DynamicPostContentSection
,
DynamicPostItem
}
from
"./types"
;
type
StructuredPostContentProps
=
{
post
:
DynamicPostItem
;
};
function
getGridClassName
(
columns
:
number
)
{
if
(
columns
>=
4
)
return
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
;
if
(
columns
===
3
)
return
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
;
if
(
columns
===
2
)
return
"grid-cols-1 sm:grid-cols-2"
;
return
"grid-cols-1"
;
}
function
StructuredImageSection
({
section
}:
{
section
:
DynamicPostContentSection
})
{
const
images
=
section
.
images
.
filter
((
item
)
=>
item
.
image
?.
url
);
const
[
activeImage
,
setActiveImage
]
=
useState
<
{
src
:
string
;
alt
:
string
;
caption
?:
string
;
}
|
null
>
(
null
);
if
(
!
images
.
length
)
return
null
;
return
(
<>
<
div
className=
{
`not-prose my-6 grid gap-4 ${getGridClassName(section.image_columns)}`
}
>
{
images
.
map
((
item
)
=>
{
const
image
=
item
.
image
;
if
(
!
image
?.
url
)
return
null
;
return
(
<
figure
key=
{
`${section.id}-${image.id || image.url}-${item.position}`
}
className=
"group cursor-zoom-in overflow-hidden rounded-[18px] bg-white"
onClick=
{
()
=>
setActiveImage
({
src
:
image
.
url
,
alt
:
image
.
alt
||
image
.
name
||
"Hình ảnh bài viết"
,
caption
:
item
.
caption
??
undefined
,
})
}
>
<
SafeImage
src=
{
image
.
url
}
alt=
{
image
.
alt
||
image
.
name
||
"Hình ảnh bài viết"
}
width=
{
1200
}
height=
{
800
}
className=
"h-auto w-full object-contain transition-transform duration-300 group-hover:scale-[1.02]"
/>
{
item
.
caption
?
(
<
figcaption
className=
"mt-2 text-center text-sm text-gray-600"
>
{
item
.
caption
}
</
figcaption
>
)
:
null
}
</
figure
>
);
})
}
</
div
>
<
ImageLightbox
src=
{
activeImage
?.
src
??
""
}
alt=
{
activeImage
?.
alt
}
caption=
{
activeImage
?.
caption
}
open=
{
Boolean
(
activeImage
)
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
)
setActiveImage
(
null
);
}
}
/>
</>
);
}
function
normalizeCaptionShortcodes
(
html
:
string
)
{
function
normalizeCaptionShortcodes
(
html
:
string
)
{
return
html
.
replace
(
/
\[
caption
[^\]]
*]
([\s\S]
*
?)\[\/
caption]/gi
,
(
_match
,
innerContent
:
string
)
=>
{
return
html
.
replace
(
/
\[
caption
[^\]]
*]
([\s\S]
*
?)\[\/
caption]/gi
,
(
_match
,
innerContent
:
string
)
=>
{
...
@@ -195,35 +118,100 @@ function renderStructuredHtml(html: string) {
...
@@ -195,35 +118,100 @@ function renderStructuredHtml(html: string) {
return
parse
(
normalizeImportedLayout
(
normalizeCaptionShortcodes
(
html
)));
return
parse
(
normalizeImportedLayout
(
normalizeCaptionShortcodes
(
html
)));
}
}
export
default
function
StructuredPostContent
({
post
}:
StructuredPostContentProps
)
{
export
function
TextSection
({
section
}:
{
section
:
DynamicPostContentSection
})
{
const
sections
=
(
post
.
content_structure
?.
post_content
??
[])
const
content
=
section
.
content
.
trim
();
.
slice
()
if
(
!
content
)
return
null
;
.
sort
((
left
,
right
)
=>
left
.
position
-
right
.
position
);
if
(
!
sections
.
length
)
{
return
(
return
<>
{
renderStructuredHtml
(
getDynamicPostBodyHtml
(
post
))
}
</>;
<
div
className=
"post-text-section"
>
{
renderStructuredHtml
(
content
)
}
<
style
jsx
global
>
{
`
.post-text-section {
color: #1f2937;
line-height: 1.85;
width: 100%;
max-width: 100%;
}
}
const
hasRenderableSection
=
sections
.
some
(
.post-text-section p,
(
section
)
=>
section
.
content
.
trim
()
||
section
.
images
.
length
,
.post-text-section div {
);
margin: 0 0 18px;
max-width: 100% !important;
box-sizing: border-box;
}
if
(
!
hasRenderableSection
)
{
.post-text-section h1,
return
<>
{
renderStructuredHtml
(
getDynamicPostBodyHtml
(
post
))
}
</>;
.post-text-section h2,
.post-text-section h3,
.post-text-section h4,
.post-text-section h5,
.post-text-section h6 {
margin: 0 0 18px;
color: #111827;
font-weight: 700;
line-height: 1.45;
}
}
return
(
.post-text-section :is(p, div, span, li, a, strong, em, u, s) {
<>
font-family: inherit;
{
sections
.
map
((
section
)
=>
{
if
(
section
.
type
===
"image"
)
{
return
<
StructuredImageSection
key=
{
section
.
id
}
section=
{
section
}
/>;
}
}
const
content
=
section
.
content
.
trim
();
.post-text-section img {
if
(
!
content
)
return
null
;
display: block;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin: 24px auto 10px;
border-radius: 14px;
}
.post-text-section figure {
display: block !important;
width: 100% !important;
max-width: 100% !important;
margin: 28px 0;
text-align: center;
}
.post-text-section .article-content,
.post-text-section .article-content_toc,
.post-text-section table,
.post-text-section iframe {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.post-text-section table {
display: table;
table-layout: fixed;
}
.post-text-section figcaption,
.post-text-section .wp-caption-text {
margin-top: 10px;
color: #6b7280;
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.post-text-section a {
color: #14519f;
font-weight: 600;
}
return
<
div
key=
{
section
.
id
}
>
{
renderStructuredHtml
(
content
)
}
</
div
>;
.post-text-section ul,
})
}
.post-text-section ol {
</>
margin: 18px 0;
padding-left: 24px;
}
.post-text-section li {
margin: 8px 0;
}
`
}
</
style
>
</
div
>
);
);
}
}
src/components/shared/post-content/index.tsx
0 → 100644
View file @
4d80d722
"use client"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
import
{
ImageSection
}
from
"./components/image-section"
;
import
{
TextSection
}
from
"./components/text-section"
;
type
PostContentProps
=
{
post
:
DynamicPostItem
;
};
export
default
function
PostContent
({
post
}:
PostContentProps
)
{
const
sections
=
(
post
.
content_structure
?.
post_content
??
[])
.
slice
()
.
sort
((
left
,
right
)
=>
left
.
position
-
right
.
position
);
return
(
<
div
className=
"overflow-hidden"
>
{
sections
.
map
((
section
)
=>
{
if
(
section
.
type
===
"image"
)
{
return
<
ImageSection
key=
{
section
.
id
}
section=
{
section
}
/>;
}
return
<
TextSection
key=
{
section
.
id
}
section=
{
section
}
/>;
})
}
</
div
>
);
}
src/components/shared/sidebar-advertisements/components/fallback-sidebar-ad-item.tsx
0 → 100644
View file @
4d80d722
import
{
SafeImage
}
from
"@/components/shared/safe-image"
;
import
Link
from
"next/link"
;
const
FALLBACK_HREF
=
"https://vcci-hcm.org.vn"
;
export
function
FallbackSidebarAdItem
({
src
}:
{
src
:
string
})
{
return
(
<
Link
href=
{
FALLBACK_HREF
}
target=
"_blank"
rel=
"noopener noreferrer"
className=
"block overflow-hidden rounded-[16px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
title=
"Quảng cáo VCCI HCM"
>
<
div
className=
"relative aspect-[16/10]"
>
<
SafeImage
src=
{
src
}
alt=
"Quảng cáo VCCI HCM"
fill
sizes=
"(max-width: 768px) 100vw, 300px"
className=
"h-full w-full object-cover"
/>
</
div
>
</
Link
>
);
}
src/components/shared/sidebar-advertisements/components/sidebar-ad-item.tsx
0 → 100644
View file @
4d80d722
import
{
SafeImage
}
from
"@/components/shared/safe-image"
;
import
Link
from
"next/link"
;
import
links
from
"@/links"
;
import
type
{
Advertisement
}
from
"@/api/vcci-news/models/advertisement"
;
export
function
SidebarAdItem
({
item
,
fallbackSrc
}:
{
item
:
Advertisement
;
fallbackSrc
:
string
})
{
const
src
=
item
.
file
?.
path
?
links
.
resolveImageUrl
(
item
.
file
.
path
)
:
fallbackSrc
;
return
(
<
Link
href=
{
item
.
link
}
target=
"_blank"
rel=
"noopener noreferrer"
className=
"block overflow-hidden rounded-[16px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
title=
{
item
.
name
}
>
<
div
className=
"relative aspect-[16/10]"
>
<
SafeImage
src=
{
src
}
fallbackSrc=
{
fallbackSrc
}
alt=
{
item
.
alt
||
item
.
name
}
fill
sizes=
"(max-width: 768px) 100vw, 300px"
className=
"h-full w-full object-cover"
/>
</
div
>
</
Link
>
);
}
src/components/shared/sidebar-advertisements/index.tsx
View file @
4d80d722
'use client'
;
'use client'
;
import
{
SafeImage
}
from
"@/components/shared/safe-image"
;
import
Link
from
"next/link"
;
import
{
useMemo
}
from
"react"
;
import
{
useMemo
}
from
"react"
;
import
{
useAdvertisements
}
from
"@/app/(main)/(home)/lib/use-advertisements"
;
import
{
useAdvertisements
}
from
"@/app/(main)/(home)/lib/use-advertisements"
;
import
links
from
"@/links"
;
import
type
{
Advertisement
}
from
"@/api/vcci-news/models/advertisement"
;
import
{
getFallbackImage
}
from
"@/lib/utils/fallback-image"
;
import
{
getFallbackImage
}
from
"@/lib/utils/fallback-image"
;
import
{
SidebarAdItem
}
from
"./components/sidebar-ad-item"
;
import
{
FallbackSidebarAdItem
}
from
"./components/fallback-sidebar-ad-item"
;
const
FALLBACK_HREF
=
"https://vcci-hcm.org.vn"
;
function
SidebarAdItem
({
item
,
fallbackSrc
}:
{
item
:
Advertisement
;
fallbackSrc
:
string
})
{
const
src
=
item
.
file
?.
path
?
links
.
resolveImageUrl
(
item
.
file
.
path
)
:
fallbackSrc
;
return
(
<
Link
href=
{
item
.
link
}
target=
"_blank"
rel=
"noopener noreferrer"
className=
"block overflow-hidden rounded-[16px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
title=
{
item
.
name
}
>
<
div
className=
"relative aspect-[16/10]"
>
<
SafeImage
src=
{
src
}
fallbackSrc=
{
fallbackSrc
}
alt=
{
item
.
alt
||
item
.
name
}
fill
sizes=
"(max-width: 768px) 100vw, 300px"
className=
"h-full w-full object-cover"
/>
</
div
>
</
Link
>
);
}
function
FallbackSidebarAdItem
({
src
}:
{
src
:
string
})
{
return
(
<
Link
href=
{
FALLBACK_HREF
}
target=
"_blank"
rel=
"noopener noreferrer"
className=
"block overflow-hidden rounded-[16px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
title=
"Quảng cáo VCCI HCM"
>
<
div
className=
"relative aspect-[16/10]"
>
<
SafeImage
src=
{
src
}
alt=
"Quảng cáo VCCI HCM"
fill
sizes=
"(max-width: 768px) 100vw, 300px"
className=
"h-full w-full object-cover"
/>
</
div
>
</
Link
>
);
}
/** Sidebar quảng cáo vuông (stack dọc) cho các trang detail/catalog/search */
function
SidebarAdvertisements
({
count
=
5
,
startIndex
=
0
}:
{
count
?:
number
;
startIndex
?:
number
})
{
function
SidebarAdvertisements
({
count
=
5
,
startIndex
=
0
}:
{
count
?:
number
;
startIndex
?:
number
})
{
const
ads
=
useAdvertisements
(
"square"
);
const
ads
=
useAdvertisements
(
"square"
);
const
visibleAds
=
ads
.
slice
(
startIndex
,
startIndex
+
count
);
const
visibleAds
=
ads
.
slice
(
startIndex
,
startIndex
+
count
);
...
...
src/components/shared/time-picker.tsx
View file @
4d80d722
"use client"
;
"use client"
;
import
*
as
React
from
"react"
;
import
{
Clock
}
from
"lucide-react"
;
import
{
Clock
}
from
"lucide-react"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
useEffect
,
useState
}
from
"react"
;
const
HOURS
=
Array
.
from
({
length
:
24
},
(
_
,
i
)
=>
i
);
const
HOURS
=
Array
.
from
({
length
:
24
},
(
_
,
i
)
=>
i
);
const
MINUTES
=
Array
.
from
({
length
:
12
},
(
_
,
i
)
=>
i
*
5
);
const
MINUTES
=
Array
.
from
({
length
:
12
},
(
_
,
i
)
=>
i
*
5
);
...
@@ -19,11 +19,11 @@ export function TimePicker({
...
@@ -19,11 +19,11 @@ export function TimePicker({
value
:
string
;
value
:
string
;
onChange
:
(
time
:
string
)
=>
void
;
onChange
:
(
time
:
string
)
=>
void
;
})
{
})
{
const
[
open
,
setOpen
]
=
React
.
useState
(
false
);
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
hour
,
setHour
]
=
React
.
useState
<
number
|
null
>
(
null
);
const
[
hour
,
setHour
]
=
useState
<
number
|
null
>
(
null
);
const
[
minute
,
setMinute
]
=
React
.
useState
<
number
|
null
>
(
null
);
const
[
minute
,
setMinute
]
=
useState
<
number
|
null
>
(
null
);
React
.
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
open
)
return
;
if
(
!
open
)
return
;
if
(
value
)
{
if
(
value
)
{
// Trim seconds & timezone (VD: 19:39:00.000Z -> 19:39)
// Trim seconds & timezone (VD: 19:39:00.000Z -> 19:39)
...
...
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