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
29e06f04
Commit
29e06f04
authored
Nov 05, 2025
by
Văn Hoàng
Browse files
Options
Browse Files
Download
Plain Diff
[tag]0.1-vcci
parents
28465113
dbcd6907
Pipeline
#43394
passed with stages
in 6 minutes and 20 seconds
Changes
28
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
861 additions
and
1258 deletions
+861
-1258
page.tsx
src/app/(main)/(home)/[id]/page.tsx
+2
-1
index.tsx
src/app/(main)/(home)/components/card-news/index.tsx
+10
-11
index.tsx
src/app/(main)/(home)/components/event-calendar/index.tsx
+0
-113
page.tsx
src/app/(main)/(home)/page.tsx
+572
-516
ScrollToTopButton.tsx
src/app/(main)/_lib/layout/ScrollToTopButton.tsx
+33
-0
footer.tsx
src/app/(main)/_lib/layout/footer.tsx
+1
-1
index.tsx
...main)/dai-dien-gioi-chu/components/event-filter/index.tsx
+20
-17
page.tsx
src/app/(main)/gioi-thieu/dich-vu-cung-cap/page.tsx
+1
-1
page.tsx
src/app/(main)/hoat-dong/su-kien/page.tsx
+33
-4
page.tsx
src/app/(main)/hoi-vien/page.tsx
+1
-1
layout.tsx
src/app/(main)/layout.tsx
+9
-9
page.tsx
src/app/(main)/thong-tin-truyen-thong/an-pham/[id]/page.tsx
+3
-3
index.tsx
...ng-tin-truyen-thong/an-pham/components/calendar/index.tsx
+0
-222
page.tsx
src/app/(main)/thong-tin-truyen-thong/an-pham/page.tsx
+1
-1
page.tsx
src/app/(main)/thong-tin-truyen-thong/tin-vcci/page.tsx
+1
-1
page.tsx
...main)/xuat-xu-hang-hoa/bieu-mau-c-o-va-cach-khai/page.tsx
+3
-3
index.tsx
...app/(main)/xuat-xu-hang-hoa/components/Calendar/index.tsx
+0
-222
page.tsx
...main)/xuat-xu-hang-hoa/diem-cap-va-thoi-gian-cap/page.tsx
+3
-3
page.tsx
src/app/(main)/xuat-xu-hang-hoa/luat-ap-dung/page.tsx
+3
-3
page.tsx
src/app/(main)/xuat-xu-hang-hoa/muc-dich/page.tsx
+3
-3
page.tsx
src/app/(main)/xuat-xu-hang-hoa/page.tsx
+2
-2
page.tsx
src/app/(main)/xuat-xu-hang-hoa/phi-va-le-phi-cap/page.tsx
+3
-3
page.tsx
src/app/(main)/xuat-xu-hang-hoa/thong-tin-lien-he/page.tsx
+3
-3
page.tsx
src/app/(main)/xuat-xu-hang-hoa/thu-tuc-cap/page.tsx
+3
-3
page.tsx
...app/(main)/xuc-tien-thuong-mai/co-hoi-kinh-doanh/page.tsx
+3
-3
page.tsx
...(main)/xuc-tien-thuong-mai/moi-truong-kinh-doanh/page.tsx
+2
-2
index.tsx
src/components/base/event-calendar/index.tsx
+139
-102
AppEditorContent.tsx
src/components/shared/editor-content/AppEditorContent.tsx
+7
-5
No files found.
src/app/(main)/(home)/[id]/page.tsx
View file @
29e06f04
...
...
@@ -15,6 +15,7 @@ import { useParams } from 'next/navigation'
import
ListCategory
from
'@/components/base/list-category'
import
{
MEDIA_INFORMATION_CATEGORIES
}
from
'@/constants/categories'
import
EventCalendar
from
'@/components/base/event-calendar'
import
parse
from
"html-react-parser"
;
// import { t } from 'i18next'
// Component
...
...
@@ -52,7 +53,7 @@ const NewsDetailPage = () => {
<
hr
/>
</
div
>
<
div
className=
'flex-1 text-app-grey text-base overflow-hidden'
>
<
AppEditorContent
value=
{
data
?.
responseData
?.
description
??
''
}
/
>
<
div
className=
"p-7.5 prose tiptap overflow-hidden"
>
{
parse
(
data
?.
responseData
?.
description
??
''
)
}
</
div
>
</
div
>
</
main
>
{
/* Sidebar */
}
...
...
src/app/(main)/(home)/components/card-news/index.tsx
View file @
29e06f04
import
{
NewsAdminItem
}
from
'@/api/types/news'
import
BASE_URL
from
'@/links'
import
dayjs
from
'dayjs'
;
import
AppEditorContent
from
'@/components/shared/editor-content'
;
import
{
NewsAdminItem
}
from
"@/api/types/news"
;
import
BASE_URL
from
"@/links"
;
import
dayjs
from
"dayjs"
;
import
AppEditorContent
from
"@/components/shared/editor-content"
;
function
CardNews
({
news
}:
{
news
:
NewsAdminItem
})
{
return
(
<
a
href=
{
`${news.id}`
}
className=
'flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3 p-2 sm:p-3 border border-gray-200 bg-white rounded-md'
className=
"flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3"
>
<
img
src=
{
`${BASE_URL.imageEndpoint}${news.thumbnail}`
}
...
...
@@ -18,13 +18,12 @@ function CardNews({ news }: { news: NewsAdminItem }) {
e
.
currentTarget
.
src
=
"/fallback.png"
}
}
/>
<
div
className=
'flex-1'
>
<
p
className=
'text-[#0056b3] font-bold text-sm line-clamp-2'
>
<
div
className=
"flex-1"
>
<
p
className=
"text-[#363636] font-bold text-sm line-clamp-2"
>
{
news
.
title
}
</
p
>
<
p
className=
'text-gray-500 text-sm my-1'
>
{
dayjs
(
news
.
release_at
).
format
(
'DD/MM/YYYY'
)
}
<
p
className=
"text-gray-500 text-sm my-1"
>
{
dayjs
(
news
.
release_at
).
format
(
"DD/MM/YYYY"
)
}
</
p
>
{
/* <AppEditorContent className='line-clamp-2' value={news.description} /> */
}
</
div
>
...
...
@@ -32,4 +31,4 @@ function CardNews({ news }: { news: NewsAdminItem }) {
);
}
export
default
CardNews
;
\ No newline at end of file
export
default
CardNews
;
src/app/(main)/(home)/components/event-calendar/index.tsx
deleted
100644 → 0
View file @
28465113
"use client"
;
import
React
,
{
useState
}
from
"react"
;
import
{
ArrowRight
,
ArrowLeft
}
from
"lucide-react"
;
export
default
function
EventCalendar
()
{
const
mockCalendar
=
{
month
:
10
,
year
:
2025
,
highlighted
:
[
6
,
9
,
12
],
};
const
[
month
,
setMonth
]
=
useState
<
number
>
(
mockCalendar
.
month
);
const
[
year
,
setYear
]
=
useState
<
number
>
(
mockCalendar
.
year
);
return
(
<
div
className=
"bg-white border rounded-md p-4"
>
<
div
className=
"flex items-center justify-between mb-3"
>
<
div
className=
"text-sm font-medium"
>
THÁNG
{
month
}
/
{
year
}
</
div
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"group"
>
<
button
aria
-
label=
"Tháng trước"
onClick=
{
()
=>
{
// prev month
if
(
month
===
1
)
{
setMonth
(
12
);
setYear
((
y
)
=>
y
-
1
);
}
else
{
setMonth
((
m
)
=>
m
-
1
);
}
}
}
className=
"group-hover:border-primary p-1 h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636] "
>
<
ArrowLeft
size=
{
24
}
className=
"group-hover:text-muted-foreground text-[#363636]"
/>
</
button
>
</
div
>
<
div
className=
"group"
>
<
button
aria
-
label=
"Tháng sau"
onClick=
{
()
=>
{
// next month
if
(
month
===
12
)
{
setMonth
(
1
);
setYear
((
y
)
=>
y
+
1
);
}
else
{
setMonth
((
m
)
=>
m
+
1
);
}
}
}
className=
"p-1 group-hover:border-primary h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636]"
>
<
ArrowRight
size=
{
24
}
className=
"group-hover:text-muted-foreground"
/>
</
button
>
</
div
>
</
div
>
</
div
>
<
div
className=
"grid grid-cols-7 gap-1 text-center text-xs"
>
{
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
].
map
((
d
)
=>
(
<
div
key=
{
d
}
className=
"text-gray-400 py-1"
>
{
d
}
</
div
>
))
}
{
(()
=>
{
const
totalDays
=
new
Date
(
year
,
month
,
0
).
getDate
()
const
firstDayIndex
=
new
Date
(
year
,
month
-
1
,
1
).
getDay
()
// 0 (Sun) - 6 (Sat)
// previous month total days
const
prevMonthTotalDays
=
new
Date
(
year
,
month
-
1
,
0
).
getDate
()
const
totalCells
=
firstDayIndex
+
totalDays
const
trailingCount
=
(
7
-
(
totalCells
%
7
))
%
7
return
(
<>
{
Array
.
from
({
length
:
firstDayIndex
}).
map
((
_
,
i
)
=>
{
const
dayNum
=
prevMonthTotalDays
-
(
firstDayIndex
-
1
)
+
i
return
(
<
div
key=
{
`prev-${i}`
}
className=
"py-2 text-sm text-gray-300"
>
{
dayNum
}
</
div
>
)
})
}
{
Array
.
from
({
length
:
totalDays
},
(
_
,
i
)
=>
i
+
1
).
map
((
day
)
=>
{
const
isHighlighted
=
mockCalendar
.
highlighted
.
includes
(
day
)
return
(
<
div
key=
{
day
}
className=
{
`py-2 rounded-full w-10 h-10 flex flex-col justify-center items-center text-sm ${isHighlighted ? 'bg-yellow-500 text-white' : 'text-gray-700'
}`
}
>
{
day
}
</
div
>
)
})
}
{
Array
.
from
({
length
:
trailingCount
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
`next-${i}`
}
className=
"py-2 text-sm text-gray-300"
>
{
i
+
1
}
</
div
>
))
}
</>
)
})()
}
</
div
>
</
div
>
);
}
src/app/(main)/(home)/page.tsx
View file @
29e06f04
This diff is collapsed.
Click to expand it.
src/app/(main)/_lib/layout/ScrollToTopButton.tsx
0 → 100644
View file @
29e06f04
"use client"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
ChevronsUp
}
from
"lucide-react"
;
export
default
function
ScrollToTopButton
()
{
const
[
visible
,
setVisible
]
=
useState
(
false
);
useEffect
(()
=>
{
const
toggleVisibility
=
()
=>
{
setVisible
(
window
.
scrollY
>
300
);
};
window
.
addEventListener
(
"scroll"
,
toggleVisibility
);
return
()
=>
window
.
removeEventListener
(
"scroll"
,
toggleVisibility
);
},
[]);
const
scrollToTop
=
()
=>
{
window
.
scrollTo
({
top
:
0
,
behavior
:
"smooth"
});
};
return
(
<
button
onClick=
{
scrollToTop
}
className=
{
`fixed bottom-25 right-6 bg-[#e8c518] hover:text-[#063e8e] text-white p-3 rounded-lg shadow-lg transition-all duration-500 cursor-pointer ${
visible
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-3 pointer-events-none"
}`
}
>
<
ChevronsUp
size=
{
24
}
/>
</
button
>
);
}
src/app/(main)/_lib/layout/footer.tsx
View file @
29e06f04
...
...
@@ -170,7 +170,7 @@ function Footer() {
</
div
>
</
div
>
<
div
className=
"bg-[#032248] h-[80px] flex items-center justify-center"
>
<
div
className=
"
max-w-[1200px]
w-full p-5"
>
<
div
className=
"
container
w-full p-5"
>
<
p
className=
"text-[14px] text-white"
>
© Bản quyền VCCI-HCM | All rights reserved
</
p
>
...
...
src/app/(main)/dai-dien-gioi-chu/components/event-filter/index.tsx
View file @
29e06f04
...
...
@@ -7,12 +7,7 @@ import { Button } from '@/components/ui/button'
type
Category
=
{
id
:
string
;
title
:
string
}
const
MOCK_CATEGORIES
:
Category
[]
=
[
{
id
:
'topic'
,
title
:
'Chuyên đề'
},
{
id
:
'training'
,
title
:
'Tập huấn NSDLĐ'
},
{
id
:
'law'
,
title
:
'Xây dựng và Phổ biến pháp luật'
},
{
id
:
'trade'
,
title
:
'Xúc tiến thương mại và Đầu tư'
}
]
type
FilterPayload
=
{
upcoming
:
boolean
...
...
@@ -22,14 +17,20 @@ type FilterPayload = {
fromDate
:
string
toDate
:
string
}
export
const
EventFilter
:
React
.
FC
<
{
onFilter
?:
(
payload
:
FilterPayload
)
=>
void
;
onReset
?:
()
=>
void
}
>
=
({
onFilter
,
onReset
})
=>
{
export
const
EventFilter
:
React
.
FC
<
{
categories
?:
Category
[]
onFilter
?:
(
payload
:
FilterPayload
)
=>
void
onReset
?:
()
=>
void
}
>
=
({
categories
:
categoriesProp
,
onFilter
,
onReset
})
=>
{
const
[
upcoming
,
setUpcoming
]
=
useState
(
false
)
const
[
past
,
setPast
]
=
useState
(
false
)
const
[
query
,
setQuery
]
=
useState
(
''
)
// Use categories passed via props if available; otherwise use an empty array
const
categoriesList
=
categoriesProp
??
[]
const
[
categories
,
setCategories
]
=
useState
<
Record
<
string
,
boolean
>>
(()
=>
{
const
map
:
Record
<
string
,
boolean
>
=
{}
MOCK_CATEGORIES
.
forEach
((
c
)
=>
(
map
[
c
.
id
]
=
false
))
categoriesList
.
forEach
((
c
)
=>
(
map
[
c
.
id
]
=
false
))
return
map
})
const
[
fromDate
,
setFromDate
]
=
useState
(
''
)
...
...
@@ -85,14 +86,16 @@ export const EventFilter: React.FC<{ onFilter?: (payload: FilterPayload) => void
/>
</
div
>
<
div
className=
"mb-4"
>
{
MOCK_CATEGORIES
.
map
((
c
)
=>
(
<
label
key=
{
c
.
id
}
className=
"flex items-center gap-2 mb-2"
>
<
Checkbox
checked=
{
!!
categories
[
c
.
id
]
}
onCheckedChange=
{
()
=>
toggleCategory
(
c
.
id
)
}
/>
<
span
className=
"text-sm"
>
{
c
.
title
}
</
span
>
</
label
>
))
}
</
div
>
{
categoriesList
&&
categoriesList
.
length
>
0
&&
(
<
div
className=
"mb-4"
>
{
categoriesList
.
map
((
c
)
=>
(
<
label
key=
{
c
.
id
}
className=
"flex items-center gap-2 mb-2"
>
<
Checkbox
checked=
{
!!
categories
[
c
.
id
]
}
onCheckedChange=
{
()
=>
toggleCategory
(
c
.
id
)
}
/>
<
span
className=
"text-sm"
>
{
c
.
title
}
</
span
>
</
label
>
))
}
</
div
>
)
}
<
div
className=
"mb-4"
>
<
Label
className=
"block text-sm mb-1"
>
Từ ngày:
</
Label
>
...
...
src/app/(main)/gioi-thieu/dich-vu-cung-cap/page.tsx
View file @
29e06f04
'use client'
import
React
from
"react"
;
import
ListCategory
from
"../components/list-category"
;
import
EventCalendar
from
"../components/event-calendar"
;
import
EventCalendar
from
'@/components/base/event-calendar'
;
const
Page
=
()
=>
{
return
(
...
...
src/app/(main)/hoat-dong/su-kien/page.tsx
View file @
29e06f04
...
...
@@ -4,7 +4,6 @@ import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import
{
EVENT_CATEGORIES
}
from
"@constants/categories"
;
import
EventFilter
from
"@app/dai-dien-gioi-chu/components/event-filter"
;
import
NewsContent
from
"@app/dai-dien-gioi-chu/components/card-news"
;
// ...existing code...
import
{
Pagination
}
from
"@components/base/pagination"
;
import
Image
from
"next/image"
;
import
{
useGetEvents
}
from
'@api/endpoints/event'
...
...
@@ -14,8 +13,8 @@ import { GetNewsResponseType } from "@api/types/NewsPage.type";
import
{
PATHS
}
from
"@constants/paths"
;
import
{
Spinner
}
from
"@components/ui/spinner"
;
export
default
function
Page
()
{
const
[
submitSearch
]
=
useState
(
""
);
const
[
page
,
setPage
]
=
useState
(
1
);
const
[
filtersString
,
setFiltersString
]
=
useState
<
string
|
undefined
>
(
''
)
const
pageSize
=
5
;
const
{
data
:
allData
,
isLoading
}
=
useGetEvents
<
EventApiResponse
>
({
...
...
@@ -23,7 +22,7 @@ export default function Page() {
currentPage
:
String
(
page
),
sortField
:
'start_time'
,
sortOrder
:
'ASC'
,
filters
:
submitSearch
?
`title @=
${
submitSearch
}
,start_time>
${
new
Date
()}
`
:
`start_time>
${
new
Date
()}
`
,
filters
:
filtersString
??
undefined
,
});
return
(
<
div
className=
"min-h-screen container mx-auto p-4"
>
...
...
@@ -61,7 +60,37 @@ export default function Page() {
{
/* Sidebar */
}
<
aside
className=
"space-y-6"
>
<
EventFilter
/>
<
EventFilter
onFilter=
{
(
payload
)
=>
{
const
parts
:
string
[]
=
[]
// query
if
(
payload
.
query
)
parts
.
push
(
`title @=${payload.query}`
)
const
nowIso
=
new
Date
().
toISOString
()
// upcoming / past
if
(
payload
.
upcoming
&&
!
payload
.
past
)
{
parts
.
push
(
`start_time>${nowIso}`
)
}
else
if
(
payload
.
past
&&
!
payload
.
upcoming
)
{
parts
.
push
(
`start_time<=${nowIso}`
)
}
if
(
payload
.
fromDate
)
{
const
fromIso
=
new
Date
(
payload
.
fromDate
).
toISOString
()
parts
.
push
(
`created_at>=${fromIso}`
)
}
if
(
payload
.
toDate
)
{
const
toIso
=
new
Date
(
payload
.
toDate
).
toISOString
()
parts
.
push
(
`created_at<=${toIso}`
)
}
const
filters
=
parts
.
length
>
0
?
parts
.
join
(
','
)
:
undefined
setFiltersString
(
filters
)
setPage
(
1
)
}
}
onReset=
{
()
=>
{
setFiltersString
(
undefined
)
setPage
(
1
)
}
}
/>
<
div
className=
"bg-white border rounded-md overflow-hidden"
>
<
div
className=
"w-full h-56 relative bg-gray-100"
>
...
...
src/app/(main)/hoi-vien/page.tsx
View file @
29e06f04
'use client'
import
React
from
"react"
;
import
ListCategory
from
"./components/list-category"
;
import
EventCalendar
from
"
./components
/event-calendar"
;
import
EventCalendar
from
"
@/components/base
/event-calendar"
;
const
Page
=
()
=>
{
return
(
...
...
src/app/(main)/layout.tsx
View file @
29e06f04
import
Header
from
"@/app/(main)/_lib/layout/header"
import
Footer
from
"@/app/(main)/_lib/layout/footer"
import
Header
from
"@/app/(main)/_lib/layout/header"
;
import
Footer
from
"@/app/(main)/_lib/layout/footer"
;
import
React
from
"react"
;
import
ScrollToTopButton
from
"./_lib/layout/ScrollToTopButton"
;
export
default
function
Layout
({
children
,
...
...
@@ -10,10 +9,11 @@ export default function Layout({
children
:
React
.
ReactNode
;
}
>
)
{
return
(
<
main
className=
"bg-background"
>
<
Header
/>
{
children
}
<
Footer
/>
</
main
>
<
main
className=
"flex flex-col min-h-screen bg-background"
>
<
Header
/>
<
div
className=
"flex-1"
>
{
children
}
</
div
>
<
ScrollToTopButton
/>
<
Footer
/>
</
main
>
);
}
src/app/(main)/thong-tin-truyen-thong/an-pham/[id]/page.tsx
View file @
29e06f04
...
...
@@ -4,7 +4,7 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@/constants/categories";
import
Image
from
"next/image"
;
import
Link
from
"next/link"
;
import
{
notFound
,
useParams
}
from
"next/navigation"
;
import
Calendar
from
"../components/
calendar"
;
import
EventCalendar
from
"@/components/base/event-
calendar"
;
const
publications
=
[
{
...
...
@@ -68,7 +68,7 @@ export default function PublicationDetail() {
return
(
<
div
className=
"bg-[#f6f6f6] min-h-screen"
>
<
div
className=
"
max-w-[1200px]
mx-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
mx-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
categories=
{
MEDIA_INFORMATION_CATEGORIES
}
/>
</
div
>
...
...
@@ -106,7 +106,7 @@ export default function PublicationDetail() {
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full"
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/thong-tin-truyen-thong/an-pham/components/calendar/index.tsx
deleted
100644 → 0
View file @
28465113
"use client"
;
import
React
,
{
useState
,
useMemo
}
from
"react"
;
import
{
format
,
startOfMonth
,
endOfMonth
,
eachDayOfInterval
,
isSameMonth
,
isSameDay
,
addMonths
,
subMonths
,
}
from
"date-fns"
;
import
{
ArrowLeft
,
ArrowRight
,
ChevronLeft
,
ChevronRight
}
from
"lucide-react"
;
import
{
vi
}
from
"date-fns/locale"
;
interface
Event
{
date
:
Date
;
title
:
string
;
type
:
"event"
|
"training"
;
description
?:
string
;
}
export
default
function
Calendar
()
{
const
[
currentMonth
,
setCurrentMonth
]
=
useState
(
new
Date
());
const
today
=
new
Date
();
// Dữ liệu mẫu
const
events
:
Event
[]
=
[
{
date
:
new
Date
(
2025
,
10
,
1
),
title
:
"Đào tạo nội bộ"
,
type
:
"training"
,
description
:
"Khóa học kỹ năng mềm"
,
},
{
date
:
new
Date
(
2025
,
10
,
3
),
title
:
"Họp cổ đông"
,
type
:
"event"
,
description
:
"Báo cáo Q3"
,
},
{
date
:
new
Date
(
2025
,
10
,
13
),
title
:
"Đào tạo kỹ thuật"
,
type
:
"training"
,
description
:
"React Advanced"
,
},
{
date
:
new
Date
(
2025
,
10
,
14
),
title
:
"Đào tạo an toàn"
,
type
:
"training"
,
description
:
"An toàn lao động"
,
},
{
date
:
new
Date
(
2025
,
10
,
20
),
title
:
"Hội thảo thuế"
,
type
:
"event"
,
description
:
"Cập nhật luật thuế thu nhập doanh nghiệp số 67/2025/QH15..."
,
},
{
date
:
new
Date
(
2025
,
10
,
28
),
title
:
"Sự kiện nội bộ"
,
type
:
"event"
,
description
:
"Team building"
,
},
];
const
monthStart
=
startOfMonth
(
currentMonth
);
const
monthEnd
=
endOfMonth
(
currentMonth
);
const
monthDays
=
eachDayOfInterval
({
start
:
monthStart
,
end
:
monthEnd
});
const
firstDayOfWeek
=
monthStart
.
getDay
();
// 0 = CN, 1 = T2...
const
startDate
=
new
Date
(
monthStart
);
startDate
.
setDate
(
startDate
.
getDate
()
-
firstDayOfWeek
);
const
days
=
[];
for
(
let
i
=
0
;
i
<
42
;
i
++
)
{
const
day
=
new
Date
(
startDate
);
day
.
setDate
(
startDate
.
getDate
()
+
i
);
days
.
push
(
day
);
}
const
getEventForDay
=
(
date
:
Date
)
=>
events
.
filter
((
e
)
=>
isSameDay
(
e
.
date
,
date
));
const
formatMonthTitle
=
()
=>
{
return
`THÁNG
${
format
(
currentMonth
,
"M/yyyy"
)}
`
.
toUpperCase
();
};
return
(
<>
<
div
className=
"w-full mx-auto bg-white rounded-lg p-4 "
>
{
/* Header */
}
<
div
className=
"flex items-center justify-between mb-4 px-3"
>
<
h2
className=
"text-[15px] font-bold text-[#063E8E]"
>
{
formatMonthTitle
()
}
</
h2
>
<
div
className=
"flex gap-3"
>
<
button
onClick=
{
()
=>
setCurrentMonth
(
subMonths
(
currentMonth
,
1
))
}
className=
"p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<
ArrowLeft
className=
"group-hover:text-[#e8c518] text-[#363636] w-5 h-5"
/>
</
button
>
<
button
onClick=
{
()
=>
setCurrentMonth
(
addMonths
(
currentMonth
,
1
))
}
className=
"p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<
ArrowRight
className=
"group-hover:text-[#e8c518] text-[#363636] w-5 h-5"
/>
</
button
>
</
div
>
</
div
>
{
/* Days of week */
}
<
div
className=
"grid grid-cols-7 text-center text-sm font-medium text-gray-600 mb-1"
>
{
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
].
map
((
day
)
=>
(
<
div
key=
{
day
}
className=
"py-2 text-[15px] text-[#063E8E]"
>
{
day
}
</
div
>
))
}
</
div
>
{
/* Calendar grid */
}
<
div
className=
"grid grid-cols-7 gap-1 text-sm"
>
{
days
.
map
((
day
,
idx
)
=>
{
const
dayEvents
=
getEventForDay
(
day
);
const
isCurrentMonth
=
isSameMonth
(
day
,
currentMonth
);
const
isToday
=
isSameDay
(
day
,
today
);
const
hasEvent
=
dayEvents
.
some
((
e
)
=>
e
.
type
===
"event"
);
const
hasTraining
=
dayEvents
.
some
((
e
)
=>
e
.
type
===
"training"
);
return
(
<
div
key=
{
idx
}
className=
{
`
relative group aspect-square flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-[#A4A4A4]" : "text-[#333333]"}
${hasEvent || hasTraining ? "text-white" : "text-[#333333]"}
${isToday ? "text-red-600 font-bold" : ""}
hover:bg-gray-50 transition
`
}
>
<
span
className=
"relative z-10"
>
{
format
(
day
,
"d"
)
}
</
span
>
{
/* Event/Training dots */
}
{
(
hasEvent
||
hasTraining
)
&&
(
<
div
className=
"absolute inset-0 flex items-center justify-center pointer-events-none"
>
<
div
className=
"flex"
>
{
hasEvent
&&
(
<
div
className=
"w-10 h-10 bg-blue-600 rounded-full"
></
div
>
)
}
{
hasTraining
&&
(
<
div
className=
"w-10 h-10 bg-yellow-500 rounded-full"
></
div
>
)
}
</
div
>
</
div
>
)
}
{
/* Tooltip on hover */
}
{
dayEvents
.
length
>
0
&&
(
<
div
className=
"absolute top-full left-1/2 -translate-x-1/2 mt-2
w-64 p-3 bg-gray-900 text-white text-xs rounded-lg
shadow-xl opacity-0 pointer-events-none
group-hover:opacity-90 transition-opacity z-50"
>
<
div
className=
"space-y-2"
>
{
dayEvents
.
map
((
event
,
i
)
=>
(
<
div
key=
{
i
}
className=
"border-b border-gray-700 border-opacity-20 last:border-0 pb-2 last:pb-0"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
{
`w-3 h-3 rounded-full ${
event.type === "event"
? "bg-blue-400"
: "bg-yellow-400"
}`
}
></
div
>
<
span
className=
"font-medium"
>
{
event
.
title
}
</
span
>
</
div
>
{
event
.
description
&&
(
<
p
className=
"text-gray-300 mt-1 text-xs"
>
{
event
.
description
}
</
p
>
)
}
</
div
>
))
}
</
div
>
{
/* Mũi tên nhọn HƯỚNG LÊN (chỉ vào ngày) */
}
<
div
className=
"absolute bottom-full left-1/2 -translate-x-1/2 -mb-1
w-0 h-0
border-l-8 border-l-transparent
border-r-8 border-r-transparent
border-b-8 border-b-gray-900"
></
div
>
</
div
>
)
}
</
div
>
);
})
}
</
div
>
{
/* Legend */
}
<
div
className=
"flex justify-center gap-6 mt-4 text-xs"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 bg-blue-600 rounded-full"
></
div
>
<
span
>
Sự kiện
</
span
>
</
div
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 bg-yellow-500 rounded-full"
></
div
>
<
span
>
Đào tạo
</
span
>
</
div
>
</
div
>
</
div
>
</>
);
}
src/app/(main)/thong-tin-truyen-thong/an-pham/page.tsx
View file @
29e06f04
...
...
@@ -7,7 +7,7 @@ import PublicationList from "./components/publicationList";
export
default
function
Page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
categories=
{
MEDIA_INFORMATION_CATEGORIES
}
/>
</
div
>
...
...
src/app/(main)/thong-tin-truyen-thong/tin-vcci/page.tsx
View file @
29e06f04
...
...
@@ -5,7 +5,7 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code...
import
NewsContent
from
"@app/dai-dien-gioi-chu/components/card-news"
;
import
ListFilter
from
"@app/dai-dien-gioi-chu/components/list-filter"
;
import
EventCalendar
from
"@
app/dai-dien-gioi-chu/components
/event-calendar"
;
import
EventCalendar
from
"@
/components/base
/event-calendar"
;
import
{
Pagination
}
from
"@components/base/pagination"
;
import
Image
from
"next/image"
;
import
{
useGetNews
}
from
"@api/endpoints/news"
;
...
...
src/app/(main)/xuat-xu-hang-hoa/bieu-mau-c-o-va-cach-khai/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -122,7 +122,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/components/Calendar/index.tsx
deleted
100644 → 0
View file @
28465113
"use client"
;
import
React
,
{
useState
,
useMemo
}
from
"react"
;
import
{
format
,
startOfMonth
,
endOfMonth
,
eachDayOfInterval
,
isSameMonth
,
isSameDay
,
addMonths
,
subMonths
,
}
from
"date-fns"
;
import
{
ArrowLeft
,
ArrowRight
,
ChevronLeft
,
ChevronRight
}
from
"lucide-react"
;
import
{
vi
}
from
"date-fns/locale"
;
interface
Event
{
date
:
Date
;
title
:
string
;
type
:
"event"
|
"training"
;
description
?:
string
;
}
export
default
function
Calendar
()
{
const
[
currentMonth
,
setCurrentMonth
]
=
useState
(
new
Date
());
const
today
=
new
Date
();
// Dữ liệu mẫu
const
events
:
Event
[]
=
[
{
date
:
new
Date
(
2025
,
10
,
1
),
title
:
"Đào tạo nội bộ"
,
type
:
"training"
,
description
:
"Khóa học kỹ năng mềm"
,
},
{
date
:
new
Date
(
2025
,
10
,
3
),
title
:
"Họp cổ đông"
,
type
:
"event"
,
description
:
"Báo cáo Q3"
,
},
{
date
:
new
Date
(
2025
,
10
,
13
),
title
:
"Đào tạo kỹ thuật"
,
type
:
"training"
,
description
:
"React Advanced"
,
},
{
date
:
new
Date
(
2025
,
10
,
14
),
title
:
"Đào tạo an toàn"
,
type
:
"training"
,
description
:
"An toàn lao động"
,
},
{
date
:
new
Date
(
2025
,
10
,
20
),
title
:
"Hội thảo thuế"
,
type
:
"event"
,
description
:
"Cập nhật luật thuế thu nhập doanh nghiệp số 67/2025/QH15..."
,
},
{
date
:
new
Date
(
2025
,
10
,
28
),
title
:
"Sự kiện nội bộ"
,
type
:
"event"
,
description
:
"Team building"
,
},
];
const
monthStart
=
startOfMonth
(
currentMonth
);
const
monthEnd
=
endOfMonth
(
currentMonth
);
const
monthDays
=
eachDayOfInterval
({
start
:
monthStart
,
end
:
monthEnd
});
const
firstDayOfWeek
=
monthStart
.
getDay
();
// 0 = CN, 1 = T2...
const
startDate
=
new
Date
(
monthStart
);
startDate
.
setDate
(
startDate
.
getDate
()
-
firstDayOfWeek
);
const
days
=
[];
for
(
let
i
=
0
;
i
<
42
;
i
++
)
{
const
day
=
new
Date
(
startDate
);
day
.
setDate
(
startDate
.
getDate
()
+
i
);
days
.
push
(
day
);
}
const
getEventForDay
=
(
date
:
Date
)
=>
events
.
filter
((
e
)
=>
isSameDay
(
e
.
date
,
date
));
const
formatMonthTitle
=
()
=>
{
return
`THÁNG
${
format
(
currentMonth
,
"M/yyyy"
)}
`
.
toUpperCase
();
};
return
(
<>
<
div
className=
"w-full mx-auto bg-white rounded-lg p-4 "
>
{
/* Header */
}
<
div
className=
"flex items-center justify-between mb-4 px-3"
>
<
h2
className=
"text-[15px] font-bold text-[#063E8E]"
>
{
formatMonthTitle
()
}
</
h2
>
<
div
className=
"flex gap-3"
>
<
button
onClick=
{
()
=>
setCurrentMonth
(
subMonths
(
currentMonth
,
1
))
}
className=
"p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<
ArrowLeft
className=
"group-hover:text-[#e8c518] text-[#363636] w-5 h-5"
/>
</
button
>
<
button
onClick=
{
()
=>
setCurrentMonth
(
addMonths
(
currentMonth
,
1
))
}
className=
"p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<
ArrowRight
className=
"group-hover:text-[#e8c518] text-[#363636] w-5 h-5"
/>
</
button
>
</
div
>
</
div
>
{
/* Days of week */
}
<
div
className=
"grid grid-cols-7 text-center text-sm font-medium text-gray-600 mb-1"
>
{
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
].
map
((
day
)
=>
(
<
div
key=
{
day
}
className=
"py-2 text-[15px] text-[#063E8E]"
>
{
day
}
</
div
>
))
}
</
div
>
{
/* Calendar grid */
}
<
div
className=
"grid grid-cols-7 gap-1 text-sm"
>
{
days
.
map
((
day
,
idx
)
=>
{
const
dayEvents
=
getEventForDay
(
day
);
const
isCurrentMonth
=
isSameMonth
(
day
,
currentMonth
);
const
isToday
=
isSameDay
(
day
,
today
);
const
hasEvent
=
dayEvents
.
some
((
e
)
=>
e
.
type
===
"event"
);
const
hasTraining
=
dayEvents
.
some
((
e
)
=>
e
.
type
===
"training"
);
return
(
<
div
key=
{
idx
}
className=
{
`
relative group aspect-square flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-[#A4A4A4]" : "text-[#333333]"}
${hasEvent || hasTraining ? "text-white" : "text-[#333333]"}
${isToday ? "text-red-600 font-bold" : ""}
hover:bg-gray-50 transition
`
}
>
<
span
className=
"relative z-10"
>
{
format
(
day
,
"d"
)
}
</
span
>
{
/* Event/Training dots */
}
{
(
hasEvent
||
hasTraining
)
&&
(
<
div
className=
"absolute inset-0 flex items-center justify-center pointer-events-none"
>
<
div
className=
"flex"
>
{
hasEvent
&&
(
<
div
className=
"w-10 h-10 bg-blue-600 rounded-full"
></
div
>
)
}
{
hasTraining
&&
(
<
div
className=
"w-10 h-10 bg-yellow-500 rounded-full"
></
div
>
)
}
</
div
>
</
div
>
)
}
{
/* Tooltip on hover */
}
{
dayEvents
.
length
>
0
&&
(
<
div
className=
"absolute top-full left-1/2 -translate-x-1/2 mt-2
w-64 p-3 bg-gray-900 text-white text-xs rounded-lg
shadow-xl opacity-0 pointer-events-none
group-hover:opacity-90 transition-opacity z-50"
>
<
div
className=
"space-y-2"
>
{
dayEvents
.
map
((
event
,
i
)
=>
(
<
div
key=
{
i
}
className=
"border-b border-gray-700 border-opacity-20 last:border-0 pb-2 last:pb-0"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
{
`w-3 h-3 rounded-full ${
event.type === "event"
? "bg-blue-400"
: "bg-yellow-400"
}`
}
></
div
>
<
span
className=
"font-medium"
>
{
event
.
title
}
</
span
>
</
div
>
{
event
.
description
&&
(
<
p
className=
"text-gray-300 mt-1 text-xs"
>
{
event
.
description
}
</
p
>
)
}
</
div
>
))
}
</
div
>
{
/* Mũi tên nhọn HƯỚNG LÊN (chỉ vào ngày) */
}
<
div
className=
"absolute bottom-full left-1/2 -translate-x-1/2 -mb-1
w-0 h-0
border-l-8 border-l-transparent
border-r-8 border-r-transparent
border-b-8 border-b-gray-900"
></
div
>
</
div
>
)
}
</
div
>
);
})
}
</
div
>
{
/* Legend */
}
<
div
className=
"flex justify-center gap-6 mt-4 text-xs"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 bg-blue-600 rounded-full"
></
div
>
<
span
>
Sự kiện
</
span
>
</
div
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 bg-yellow-500 rounded-full"
></
div
>
<
span
>
Đào tạo
</
span
>
</
div
>
</
div
>
</
div
>
</>
);
}
src/app/(main)/xuat-xu-hang-hoa/diem-cap-va-thoi-gian-cap/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -137,7 +137,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/luat-ap-dung/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -269,7 +269,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/muc-dich/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -98,7 +98,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
ListCategory
from
"./components/list-category"
;
import
Calendar
from
"./components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
import
Image
from
"next/image"
;
export
default
function
page
()
{
...
...
@@ -202,7 +202,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/phi-va-le-phi-cap/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -57,7 +57,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/thong-tin-lien-he/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -154,7 +154,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuat-xu-hang-hoa/thu-tuc-cap/page.tsx
View file @
29e06f04
import
React
from
"react"
;
import
Image
from
"next/image"
;
import
ListCategory
from
"../components/list-category"
;
import
Calendar
from
"../components/C
alendar"
;
import
EventCalendar
from
"@/components/base/event-c
alendar"
;
export
default
function
page
()
{
return
(
<
div
className=
"bg-[#f6f6f6]"
>
<
div
className=
"
max-w-[1200px]
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"
container
m-auto flex flex-col gap-5 mb-[50px]"
>
<
div
className=
"border-[#e5e7f2] border-[1px]"
>
<
ListCategory
/>
</
div
>
...
...
@@ -313,7 +313,7 @@ export default function page() {
</
div
>
</
div
>
<
div
className=
"lg:w-[calc(35%-10px)] w-full "
>
<
Calendar
/>
<
Event
Calendar
/>
<
div
className=
"relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden"
>
<
Image
src=
"/banner.webp"
...
...
src/app/(main)/xuc-tien-thuong-mai/co-hoi-kinh-doanh/page.tsx
View file @
29e06f04
...
...
@@ -3,7 +3,7 @@ import React, { useState } from "react";
import
ListCategory
from
"@app/dai-dien-gioi-chu/components/list-category"
;
import
{
TRADE_PROMOTION_CATEGORIES
}
from
"@constants/categories"
;
import
NewsContent
from
"@app/dai-dien-gioi-chu/components/card-news"
;
import
EventCalendar
from
"@
app/dai-dien-gioi-chu/components
/event-calendar"
;
import
EventCalendar
from
"@
/components/base
/event-calendar"
;
import
{
Pagination
}
from
"@components/base/pagination"
;
import
Image
from
"next/image"
;
import
{
useGetNews
}
from
"@api/endpoints/news"
;
...
...
@@ -19,12 +19,12 @@ export default function Page() {
pageSize
:
String
(
pageSize
),
currentPage
:
String
(
page
),
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters
:
'category@=Cơ hội kinh doanh'
filters
:
'category@=Cơ hội kinh doanh'
});
return
(
<
div
className=
"min-h-screen container mx-auto p-4"
>
<
div
className=
"w-full flex flex-col gap-5"
>
<
ListCategory
categories=
{
TRADE_PROMOTION_CATEGORIES
}
/>
<
ListCategory
categories=
{
TRADE_PROMOTION_CATEGORIES
}
/>
<
div
className=
"grid grid-cols-1 lg:grid-cols-3 gap-6"
>
{
/* Main content */
}
...
...
src/app/(main)/xuc-tien-thuong-mai/moi-truong-kinh-doanh/page.tsx
View file @
29e06f04
...
...
@@ -5,7 +5,7 @@ import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import
NewsContent
from
"@app/dai-dien-gioi-chu/components/card-news"
;
import
{
Pagination
}
from
"@components/base/pagination"
;
import
Image
from
"next/image"
;
import
EventCalendar
from
"@
app/dai-dien-gioi-chu/components
/event-calendar"
;
import
EventCalendar
from
"@
/components/base
/event-calendar"
;
import
{
useGetNews
}
from
"@api/endpoints/news"
;
import
{
GetNewsResponseType
}
from
"@api/types/NewsPage.type"
;
import
{
PATHS
}
from
"@constants/paths"
;
...
...
@@ -19,7 +19,7 @@ export default function Page() {
pageSize
:
String
(
pageSize
),
currentPage
:
String
(
page
),
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters
:
'category@=Môi trường kinh doanh'
filters
:
'category@=Môi trường kinh doanh'
});
return
(
<
div
className=
"min-h-screen container mx-auto p-4"
>
...
...
src/components/base/event-calendar/index.tsx
View file @
29e06f04
"use client"
;
import
React
,
{
useState
}
from
"react"
;
import
{
ArrowRight
,
ArrowLeft
}
from
"lucide-react"
;
import
{
useState
,
useMemo
}
from
"react"
;
import
{
format
,
startOfMonth
,
endOfMonth
,
eachDayOfInterval
,
isSameMonth
,
isSameDay
,
addMonths
,
subMonths
,
}
from
"date-fns"
;
import
{
ArrowLeft
,
ArrowRight
}
from
"lucide-react"
;
import
{
useGetEvents
}
from
"@/api/endpoints/event"
;
import
{
EventApiResponse
}
from
"@/api/types/event"
;
export
default
function
EventCalendar
()
{
const
mockCalendar
=
{
month
:
10
,
year
:
2025
,
highlighted
:
[
6
,
9
,
12
],
};
const
[
month
,
setMonth
]
=
useState
<
number
>
(
mockCalendar
.
month
);
const
[
year
,
setYear
]
=
useState
<
number
>
(
mockCalendar
.
year
);
const
[
currentMonth
,
setCurrentMonth
]
=
useState
(
new
Date
());
const
today
=
new
Date
();
// Fetch event data from API
const
{
data
}
=
useGetEvents
<
EventApiResponse
>
();
const
events
=
data
?.
responseData
.
rows
??
[];
// Calculate the days to display in the current month grid
const
days
=
useMemo
(()
=>
{
const
start
=
startOfMonth
(
currentMonth
);
const
end
=
endOfMonth
(
currentMonth
);
const
firstDayOfWeek
=
start
.
getDay
();
// Sunday = 0
const
gridStart
=
new
Date
(
start
);
gridStart
.
setDate
(
start
.
getDate
()
-
firstDayOfWeek
);
const
result
:
Date
[]
=
[];
for
(
let
i
=
0
;
i
<
42
;
i
++
)
{
const
day
=
new
Date
(
gridStart
);
day
.
setDate
(
gridStart
.
getDate
()
+
i
);
result
.
push
(
day
);
}
return
result
;
},
[
currentMonth
]);
// Helper: get all events for a specific day
const
getEventForDay
=
(
date
:
Date
)
=>
events
.
filter
((
e
)
=>
isSameDay
(
new
Date
(
e
.
start_time
),
date
));
// Month title formatting (Vietnamese)
const
formatMonthTitle
=
()
=>
`THÁNG
${
format
(
currentMonth
,
"M/yyyy"
)}
`
.
toUpperCase
();
return
(
<
div
className=
"bg-white border rounded-md p-4"
>
<
div
className=
"flex items-center justify-between mb-3"
>
<
div
className=
"text-sm font-medium"
>
THÁNG
{
month
}
/
{
year
}
</
div
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"group"
>
<
button
aria
-
label=
"Tháng trước"
onClick=
{
()
=>
{
// prev month
if
(
month
===
1
)
{
setMonth
(
12
);
setYear
((
y
)
=>
y
-
1
);
}
else
{
setMonth
((
m
)
=>
m
-
1
);
}
}
}
className=
"group-hover:border-primary p-1 h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636] "
>
<
ArrowLeft
size=
{
24
}
className=
"group-hover:text-muted-foreground text-[#363636]"
/>
</
button
>
</
div
>
<
div
className=
"group"
>
<
button
aria
-
label=
"Tháng sau"
onClick=
{
()
=>
{
// next month
if
(
month
===
12
)
{
setMonth
(
1
);
setYear
((
y
)
=>
y
+
1
);
}
else
{
setMonth
((
m
)
=>
m
+
1
);
}
}
}
className=
"p-1 group-hover:border-primary h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636]"
>
<
ArrowRight
size=
{
24
}
className=
"group-hover:text-muted-foreground"
/>
</
button
>
</
div
>
<
div
className=
"w-full mx-auto bg-white rounded-lg p-4"
>
{
/* ===== HEADER ===== */
}
<
div
className=
"flex items-center justify-between mb-4 px-3"
>
<
h2
className=
"text-[15px] font-bold text-[#063E8E]"
>
{
formatMonthTitle
()
}
</
h2
>
{
/* Month navigation buttons */
}
<
div
className=
"flex gap-3"
>
<
button
onClick=
{
()
=>
setCurrentMonth
(
subMonths
(
currentMonth
,
1
))
}
className=
"p-2 rounded-full group border border-[#363636] hover:border-[#063e8e] transition"
>
<
ArrowLeft
className=
"group-hover:text-[#e8c518] text-[#363636] w-5 h-5"
/>
</
button
>
<
button
onClick=
{
()
=>
setCurrentMonth
(
addMonths
(
currentMonth
,
1
))
}
className=
"p-2 rounded-full group border border-[#363636] hover:border-[#063e8e] transition"
>
<
ArrowRight
className=
"group-hover:text-[#e8c518] text-[#363636] w-5 h-5"
/>
</
button
>
</
div
>
</
div
>
<
div
className=
"grid grid-cols-7 gap-1 text-center text-xs"
>
{
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
].
map
((
d
)
=>
(
<
div
key=
{
d
}
className=
"text-gray-400 py-1"
>
{
d
}
{
/* ===== DAYS OF WEEK ===== */
}
<
div
className=
"grid grid-cols-7 text-center text-sm font-medium mb-1"
>
{
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
].
map
((
day
)
=>
(
<
div
key=
{
day
}
className=
"py-2 text-[15px] text-[#063E8E]"
>
{
day
}
</
div
>
))
}
{
(()
=>
{
const
totalDays
=
new
Date
(
year
,
month
,
0
).
getDate
()
const
firstDayIndex
=
new
Date
(
year
,
month
-
1
,
1
).
getDay
()
// 0 (Sun) - 6 (Sat)
// previous month total days
const
prevMonthTotalDays
=
new
Date
(
year
,
month
-
1
,
0
).
getDate
()
const
totalCells
=
firstDayIndex
+
totalDays
const
trailingCount
=
(
7
-
(
totalCells
%
7
))
%
7
return
(
<>
{
Array
.
from
({
length
:
firstDayIndex
}).
map
((
_
,
i
)
=>
{
const
dayNum
=
prevMonthTotalDays
-
(
firstDayIndex
-
1
)
+
i
return
(
<
div
key=
{
`prev-${i}`
}
className=
"py-2 text-sm text-gray-300"
>
{
dayNum
}
</
div
>
)
})
}
{
Array
.
from
({
length
:
totalDays
},
(
_
,
i
)
=>
i
+
1
).
map
((
day
)
=>
{
const
isHighlighted
=
mockCalendar
.
highlighted
.
includes
(
day
)
return
(
<
div
key=
{
day
}
className=
{
`py-2 rounded-full w-10 h-10 flex flex-col justify-center items-center text-sm ${
isHighlighted ? 'bg-yellow-500 text-white' : 'text-gray-700'
}`
}
>
{
day
}
</
div
>
)
})
}
{
Array
.
from
({
length
:
trailingCount
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
`next-${i}`
}
className=
"py-2 text-sm text-gray-300"
>
{
i
+
1
}
</
div
>
{
/* ===== CALENDAR GRID ===== */
}
<
div
className=
"grid grid-cols-7 gap-1 text-sm"
>
{
days
.
map
((
day
,
idx
)
=>
{
const
dayEvents
=
getEventForDay
(
day
);
const
isCurrentMonth
=
isSameMonth
(
day
,
currentMonth
);
const
isToday
=
isSameDay
(
day
,
today
);
// Example condition: color days that have "B2B" events
const
hasB2BEvent
=
dayEvents
.
some
((
e
)
=>
e
.
type
===
"B2B"
);
return
(
<
div
key=
{
idx
}
className=
{
`
relative group aspect-square flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-gray-400" : ""}
${hasB2BEvent ? "bg-blue-600 text-white" : "text-[#333333]"}
${isToday ? "text-red-600 font-bold" : ""}
transition
`
}
>
{
/* Day number */
}
<
span
className=
"relative z-10"
>
{
format
(
day
,
"d"
)
}
</
span
>
{
/* Tooltip showing event details on hover */
}
{
dayEvents
.
length
>
0
&&
(
<
div
className=
"absolute top-full left-1/2 -translate-x-1/2 mt-2
w-64 p-3 bg-gray-900 text-white text-xs rounded-lg
shadow-xl opacity-0 pointer-events-none
group-hover:opacity-90 transition-opacity z-50"
>
<
div
className=
"space-y-2"
>
{
dayEvents
.
map
((
event
,
i
)
=>
(
<
div
key=
{
i
}
className=
"border-b border-gray-700 border-opacity-20 last:border-0 pb-2 last:pb-0"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 rounded-full bg-blue-400"
/>
<
span
className=
"font-medium"
>
{
event
.
name
}
</
span
>
</
div
>
</
div
>
))
}
</
div
>
))
}
</>
)
})()
}
{
/* Tooltip arrow */
}
<
div
className=
"absolute bottom-full left-1/2 -translate-x-1/2 -mb-1
w-0 h-0
border-l-8 border-l-transparent
border-r-8 border-r-transparent
border-b-8 border-b-gray-900"
/>
</
div
>
)
}
</
div
>
);
})
}
</
div
>
{
/* ===== LEGEND ===== */
}
<
div
className=
"flex justify-center gap-6 mt-4 text-xs"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"w-3 h-3 bg-blue-600 rounded-full"
/>
<
span
>
Sự kiện
</
span
>
</
div
>
</
div
>
</
div
>
);
...
...
src/components/shared/editor-content/AppEditorContent.tsx
View file @
29e06f04
import
{
FC
,
JSX
}
from
'react'
;
import
htmlParse
,
{
DOMNode
,
Element
,
Text
}
from
'html-react-parser'
;
import
{
AppEditorContentProps
}
from
'./AppEditorContent.type'
;
import
'./AppEditorContent.css'
;
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
=>
{
...
...
@@ -49,7 +49,9 @@ const AppEditorContent: FC<AppEditorContentProps> = ({ value = '', className = '
return
(
<
div
className=
"jodit-container app-editor-container"
>
<
div
className=
{
`jodit-wysiwyg ${className}`
}
>
{
htmlParse
(
value
,
{
replace
:
transform
})
}
</
div
>
<
div
className=
{
`jodit-wysiwyg ${className}`
}
>
{
htmlParse
(
value
,
{
replace
:
transform
})
}
</
div
>
</
div
>
);
};
...
...
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