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
80e61988
Commit
80e61988
authored
May 18, 2026
by
Lê Bảo Hồng Đức
☄
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
super update UI
parent
a877b692
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1017 additions
and
311 deletions
+1017
-311
index.tsx
src/app/(main)/(home)/components/events-calendar/index.tsx
+34
-9
index.tsx
src/app/(main)/(home)/components/members/index.tsx
+19
-19
index.tsx
src/app/(main)/(home)/components/video-and-patners/index.tsx
+4
-4
page.tsx
src/app/(main)/(home)/page.tsx
+1
-1
page.tsx
src/app/(main)/[...slug]/page.tsx
+13
-0
ArticleDetailPage.tsx
src/app/(main)/[...slug]/templates/ArticleDetailPage.tsx
+116
-26
ArticlePage.tsx
src/app/(main)/[...slug]/templates/ArticlePage.tsx
+154
-63
CatalogPage.tsx
src/app/(main)/[...slug]/templates/CatalogPage.tsx
+213
-0
EventDetailPage.tsx
src/app/(main)/[...slug]/templates/EventDetailPage.tsx
+10
-10
EventPage.tsx
src/app/(main)/[...slug]/templates/EventPage.tsx
+6
-6
InformationPage.tsx
src/app/(main)/[...slug]/templates/InformationPage.tsx
+91
-16
header.tsx
src/app/(main)/_lib/layout/header.tsx
+35
-33
page.tsx
src/app/(main)/search/page.tsx
+239
-86
page.tsx
src/app/admin/base-config/page.tsx
+18
-16
layout.tsx
src/app/admin/layout.tsx
+24
-4
styles.css
src/app/styles.css
+13
-0
admin-stats-grid.tsx
src/components/admin/admin-stats-grid.tsx
+3
-3
admin-table-layout.tsx
src/components/admin/admin-table-layout.tsx
+6
-6
admin-header.tsx
src/components/shared/admin-header.tsx
+6
-6
admin-sidebar.tsx
src/components/shared/admin-sidebar.tsx
+12
-3
No files found.
src/app/(main)/(home)/components/events-calendar/index.tsx
View file @
80e61988
...
@@ -5,6 +5,7 @@ import { addMonths, format, getDay, startOfMonth, subMonths } from "date-fns";
...
@@ -5,6 +5,7 @@ import { addMonths, format, getDay, startOfMonth, subMonths } from "date-fns";
import
dayjs
from
"dayjs"
;
import
dayjs
from
"dayjs"
;
import
{
ChevronLeft
,
ChevronRight
}
from
"lucide-react"
;
import
{
ChevronLeft
,
ChevronRight
}
from
"lucide-react"
;
import
{
useMemo
,
useState
}
from
"react"
;
import
{
useMemo
,
useState
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
const
weekDays
=
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
];
const
weekDays
=
[
"CN"
,
"T2"
,
"T3"
,
"T4"
,
"T5"
,
"T6"
,
"T7"
];
...
@@ -17,7 +18,13 @@ const isTrainingEvent = (item: HomePostItem) =>
...
@@ -17,7 +18,13 @@ const isTrainingEvent = (item: HomePostItem) =>
return
key
.
includes
(
"đào tạo"
)
||
key
.
includes
(
"dao-tao"
);
return
key
.
includes
(
"đào tạo"
)
||
key
.
includes
(
"dao-tao"
);
});
});
function
EventsCalendar
()
{
function
EventsCalendar
({
className
,
compact
=
false
,
}:
{
className
?:
string
;
compact
?:
boolean
;
})
{
const
{
eventCalendarPosts
}
=
useHomePosts
();
const
{
eventCalendarPosts
}
=
useHomePosts
();
const
firstEventDate
=
eventCalendarPosts
[
0
]?.
registrationDeadline
const
firstEventDate
=
eventCalendarPosts
[
0
]?.
registrationDeadline
...
@@ -71,18 +78,36 @@ function EventsCalendar() {
...
@@ -71,18 +78,36 @@ function EventsCalendar() {
const
highlightedEvent
=
selectedEvents
[
0
]
??
monthEvents
[
0
];
const
highlightedEvent
=
selectedEvents
[
0
]
??
monthEvents
[
0
];
return
(
return
(
<
aside
className=
"w-full rounded-[28px] bg-white p-4 text-[#24469c] shadow-[0_18px_38px_rgba(16,61,130,0.16)] md:p-5 xl:w-[28%] xl:min-w-[320px]"
>
<
aside
className=
{
cn
(
"w-full rounded-[28px] bg-white text-[#24469c] shadow-[0_18px_38px_rgba(16,61,130,0.16)]"
,
compact
?
"p-4"
:
"p-4 md:p-5"
,
className
??
"xl:w-[28%] xl:min-w-[320px]"
,
)
}
>
<
div
className=
"flex items-start justify-between gap-3"
>
<
div
className=
"flex items-start justify-between gap-3"
>
<
div
>
<
div
className=
"min-w-0"
>
<
h2
className=
"client-section-title uppercase"
>
<
h2
className=
{
cn
(
"uppercase"
,
compact
?
"text-[26px] font-bold leading-tight tracking-normal"
:
"client-section-title"
,
)
}
>
Lịch sự kiện
Lịch sự kiện
</
h2
>
</
h2
>
<
p
className=
"mt-1.5 text-[12px] uppercase tracking-[0.28em] text-[#7f8eab]"
>
<
p
className=
{
cn
(
"mt-1.5 text-[12px] uppercase text-[#7f8eab]"
,
compact
?
"tracking-[0.18em]"
:
"tracking-[0.28em]"
,
)
}
>
{
`THÁNG ${format(currentMonth, "MM/yyyy")}`
}
{
`THÁNG ${format(currentMonth, "MM/yyyy")}`
}
</
p
>
</
p
>
</
div
>
</
div
>
<
div
className=
"flex gap-2"
>
<
div
className=
"flex
shrink-0
gap-2"
>
<
button
<
button
type=
"button"
type=
"button"
onClick=
{
()
=>
setCurrentMonth
(
subMonths
(
currentMonth
,
1
))
}
onClick=
{
()
=>
setCurrentMonth
(
subMonths
(
currentMonth
,
1
))
}
...
@@ -100,8 +125,8 @@ function EventsCalendar() {
...
@@ -100,8 +125,8 @@ function EventsCalendar() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"mt-3 h-[4px] w-[60px] rounded-full bg-[#f7b500]"
/>
<
div
className=
{
cn
(
"h-[4px] w-[60px] rounded-full bg-[#f7b500]"
,
compact
?
"mt-2.5"
:
"mt-3"
)
}
/>
<
div
className=
"mt-4 border-t border-[#ebf0f8] pt-3.5"
>
<
div
className=
{
cn
(
"border-t border-[#ebf0f8] pt-3.5"
,
compact
?
"mt-3"
:
"mt-4"
)
}
>
<
div
className=
"grid grid-cols-7 gap-y-2.5 text-center text-[11px] font-semibold uppercase text-[#9aabc6]"
>
<
div
className=
"grid grid-cols-7 gap-y-2.5 text-center text-[11px] font-semibold uppercase text-[#9aabc6]"
>
{
weekDays
.
map
((
day
)
=>
(
{
weekDays
.
map
((
day
)
=>
(
<
div
key=
{
day
}
>
{
day
}
</
div
>
<
div
key=
{
day
}
>
{
day
}
</
div
>
...
@@ -159,7 +184,7 @@ function EventsCalendar() {
...
@@ -159,7 +184,7 @@ function EventsCalendar() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"mt-4 flex
items-center gap-5
text-[12px] font-medium text-[#45608f]"
>
<
div
className=
"mt-4 flex
flex-wrap items-center gap-x-5 gap-y-2
text-[12px] font-medium text-[#45608f]"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"h-2.5 w-2.5 rounded-full bg-[#1e3f9a]"
/>
<
span
className=
"h-2.5 w-2.5 rounded-full bg-[#1e3f9a]"
/>
<
span
>
Sự kiện
</
span
>
<
span
>
Sự kiện
</
span
>
...
...
src/app/(main)/(home)/components/members/index.tsx
View file @
80e61988
...
@@ -5,11 +5,17 @@ import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
...
@@ -5,11 +5,17 @@ import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import
memberImages
from
"@/constants/memberImages"
;
import
memberImages
from
"@/constants/memberImages"
;
import
Link
from
"next/link"
;
import
Link
from
"next/link"
;
const
MEMBER_CONNECTION_FALLBACK_IMAGE
=
"/home/20-2048x1365.webp"
;
function
Members
()
{
function
Members
()
{
const
{
memberConnectionPosts
,
categoryLinks
,
categoryNames
}
=
useHomePosts
();
const
{
memberConnectionPosts
,
categoryLinks
,
categoryNames
}
=
useHomePosts
();
const
featuredConnection
=
memberConnectionPosts
[
0
];
const
featuredConnection
=
memberConnectionPosts
[
0
];
const
sectionLink
=
const
sectionLink
=
categoryLinks
.
get
(
categoryNames
.
ketNoiHoiVien
.
toLowerCase
())
??
"/hoi-vien/ket-noi-hoi-vien"
;
categoryLinks
.
get
(
categoryNames
.
ketNoiHoiVien
.
toLowerCase
())
??
"/hoi-vien/ket-noi-hoi-vien"
;
const
connectionImage
=
featuredConnection
?.
thumbnail
?.
url
??
MEMBER_CONNECTION_FALLBACK_IMAGE
;
const
connectionImageAlt
=
featuredConnection
?.
thumbnail
?.
alt
||
featuredConnection
?.
title
||
"VCCI HCM"
;
return
(
return
(
<
section
className=
"flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch"
>
<
section
className=
"flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch"
>
...
@@ -62,26 +68,20 @@ function Members() {
...
@@ -62,26 +68,20 @@ function Members() {
</
div
>
</
div
>
</
div
>
</
div
>
{
featuredConnection
?
(
<
Link
<
Link
href=
{
sectionLink
}
href=
{
featuredConnection
.
externalLink
}
className=
"block overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
className=
"block overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
>
>
<
div
className=
"aspect-[1.25/1] overflow-hidden rounded-[20px]"
>
<
div
className=
"aspect-[1.25/1] overflow-hidden rounded-[20px]"
>
<
ImageNext
<
ImageNext
src=
{
connectionImage
}
src=
{
featuredConnection
.
thumbnail
?.
url
??
"/thumbnail.png"
}
alt=
{
connectionImageAlt
}
alt=
{
featuredConnection
.
thumbnail
?.
alt
||
featuredConnection
.
title
}
width=
{
520
}
width=
{
520
}
height=
{
420
}
height=
{
420
}
className=
"h-full w-full object-cover"
className=
"h-full w-full object-cover"
/>
/>
</
div
>
</
Link
>
)
:
(
<
div
className=
"overflow-hidden rounded-[20px] bg-[#eef3fb] shadow-[0_16px_32px_rgba(31,59,124,0.08)]"
>
<
div
className=
"aspect-[1.25/1] rounded-[20px] bg-[#e3ebf8]"
/>
</
div
>
</
div
>
)
}
</
Link
>
</
aside
>
</
aside
>
</
section
>
</
section
>
);
);
...
...
src/app/(main)/(home)/components/video-and-patners/index.tsx
View file @
80e61988
...
@@ -22,7 +22,7 @@ const videos = [
...
@@ -22,7 +22,7 @@ const videos = [
function
VideoAndPartners
()
{
function
VideoAndPartners
()
{
return
(
return
(
<
section
className=
"flex flex-col gap-6 pb-10 xl:flex-row xl:items-st
art
"
>
<
section
className=
"flex flex-col gap-6 pb-10 xl:flex-row xl:items-st
retch
"
>
<
div
className=
"flex-1"
>
<
div
className=
"flex-1"
>
<
div
className=
"mb-5 flex items-start justify-between gap-3"
>
<
div
className=
"mb-5 flex items-start justify-between gap-3"
>
<
div
>
<
div
>
...
@@ -75,7 +75,7 @@ function VideoAndPartners() {
...
@@ -75,7 +75,7 @@ function VideoAndPartners() {
</
div
>
</
div
>
</
div
>
</
div
>
<
aside
className=
"
w-ful
l xl:w-[43%]"
>
<
aside
className=
"
flex w-full flex-co
l xl:w-[43%]"
>
<
div
className=
"mb-5 flex items-start justify-between gap-3"
>
<
div
className=
"mb-5 flex items-start justify-between gap-3"
>
<
div
>
<
div
>
<
h2
className=
"client-section-title uppercase text-[#24469c]"
>
<
h2
className=
"client-section-title uppercase text-[#24469c]"
>
...
@@ -85,11 +85,11 @@ function VideoAndPartners() {
...
@@ -85,11 +85,11 @@ function VideoAndPartners() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"grid grid-cols-2 gap-4 sm:grid-cols-3"
>
<
div
className=
"grid grid-cols-2 gap-4 sm:grid-cols-3
xl:h-[318px] xl:grid-rows-2
"
>
{
partnerImages
.
slice
(
0
,
6
).
map
((
src
,
index
)
=>
(
{
partnerImages
.
slice
(
0
,
6
).
map
((
src
,
index
)
=>
(
<
div
<
div
key=
{
src
}
key=
{
src
}
className=
"flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)]"
className=
"flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)]
xl:h-auto
"
>
>
<
ImageNext
<
ImageNext
src=
{
src
}
src=
{
src
}
...
...
src/app/(main)/(home)/page.tsx
View file @
80e61988
...
@@ -29,7 +29,7 @@ const Page = () => {
...
@@ -29,7 +29,7 @@ const Page = () => {
</Link>
</Link>
</div> */
}
</div> */
}
<
section
className=
"flex flex-col lg:flex-row pb-
16
gap-5 mb-0"
>
<
section
className=
"flex flex-col lg:flex-row pb-
8
gap-5 mb-0"
>
<
News
/>
<
News
/>
<
QuickLinks
/>
<
QuickLinks
/>
</
section
>
</
section
>
...
...
src/app/(main)/[...slug]/page.tsx
View file @
80e61988
...
@@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/react-query";
...
@@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/react-query";
import
{
Spinner
}
from
"@/components/ui"
;
import
{
Spinner
}
from
"@/components/ui"
;
import
ArticlePage
from
"./templates/ArticlePage"
;
import
ArticlePage
from
"./templates/ArticlePage"
;
import
ArticleDetailPage
from
"./templates/ArticleDetailPage"
;
import
ArticleDetailPage
from
"./templates/ArticleDetailPage"
;
import
CatalogPage
from
"./templates/CatalogPage"
;
import
InformationPage
from
"./templates/InformationPage"
;
import
InformationPage
from
"./templates/InformationPage"
;
import
{
import
{
fetchDynamicCategories
,
fetchDynamicCategories
,
...
@@ -98,6 +99,18 @@ export default function DynamicPage() {
...
@@ -98,6 +99,18 @@ export default function DynamicPage() {
}
}
if
(
resolvedCategory
?.
type
===
"news"
)
{
if
(
resolvedCategory
?.
type
===
"news"
)
{
if
(
resolvedCategory
.
slug
===
"an-pham"
||
resolvedCategory
.
slug
===
"thu-vien-tai-lieu"
)
{
return
(
<
CatalogPage
category=
{
resolvedCategory
}
allCategories=
{
categoryQuery
.
data
??
[]
}
/>
);
}
return
(
return
(
<
ArticlePage
<
ArticlePage
category=
{
resolvedCategory
}
category=
{
resolvedCategory
}
...
...
src/app/(main)/[...slug]/templates/ArticleDetailPage.tsx
View file @
80e61988
...
@@ -2,12 +2,9 @@
...
@@ -2,12 +2,9 @@
import
dayjs
from
"dayjs"
;
import
dayjs
from
"dayjs"
;
import
parse
from
"html-react-parser"
;
import
parse
from
"html-react-parser"
;
import
EventCalendar
from
"@/components/base/event-calendar"
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
EventsCalendar
from
"@/app/(main)/(home)/components/events-calendar"
;
import
{
import
{
getDynamicPostBodyHtml
}
from
"./data"
;
buildDynamicCategoryMenu
,
getDynamicPostBodyHtml
,
}
from
"./data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
type
ArticleDetailPageProps
=
{
type
ArticleDetailPageProps
=
{
...
@@ -19,35 +16,128 @@ type ArticleDetailPageProps = {
...
@@ -19,35 +16,128 @@ type ArticleDetailPageProps = {
export
default
function
ArticleDetailPage
({
export
default
function
ArticleDetailPage
({
post
,
post
,
category
,
category
,
allCategories
,
}:
ArticleDetailPageProps
)
{
}:
ArticleDetailPageProps
)
{
const
categoryMenu
=
category
const
publishedDate
=
dayjs
(
?
buildDynamicCategoryMenu
(
category
,
allCategories
)
post
.
release_at
??
post
.
published_at
??
post
.
created_at
,
:
[];
).
format
(
"DD/MM/YYYY"
);
const
primaryCategory
=
post
.
categories
[
0
]?.
name
||
category
?.
name
||
"Tin tức"
;
return
(
return
(
<
div
className=
"container w-full flex justify-center items-center pb-10"
>
<
div
className=
"min-h-screen bg-[#fbfbfa]"
>
<
div
className=
"flex flex-col gap-5 w-full"
>
<
div
className=
"container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10"
>
{
categoryMenu
.
length
>
0
?
<
ListCategory
categories=
{
categoryMenu
}
/>
:
<
br
/>
}
<
div
className=
"grid grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_340px] xl:gap-12"
>
<
div
className=
"grid grid-cols-1 lg:grid-cols-3 gap-5"
>
<
main
className=
"min-w-0"
>
<
main
className=
"lg:col-span-2 bg-white border rounded-md p-8"
>
<
div
className=
"mb-5 flex flex-wrap items-center gap-3 text-xs"
>
<
div
className=
"pb-5 text-primary text-2xl leading-normal font-medium"
>
<
span
className=
"rounded-full bg-[#eaf0ff] px-2.5 py-1 font-semibold text-[#1f4fa3]"
>
{
post
.
title
}
{
primaryCategory
}
</
div
>
<
div
className=
"flex items-center gap-2 text-sm mb-4"
>
<
span
className=
"text-base text-blue-700"
>
{
dayjs
(
post
.
release_at
??
post
.
published_at
??
post
.
created_at
).
format
(
"DD/MM/YYYY"
)
}
</
span
>
</
span
>
<
span
className=
"text-[#9aa3ad]"
>
{
publishedDate
}
</
span
>
</
div
>
</
div
>
<
hr
className=
"my-5"
/>
<
div
className=
"flex-1 text-app-grey text-base overflow-hidden"
>
<
h1
className=
"max-w-4xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]"
>
<
div
className=
"prose tiptap max-w-none overflow-hidden"
>
{
post
.
title
}
</
h1
>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
{
post
.
summary
?
(
<
p
className=
"mt-5 max-w-4xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
{
post
.
summary
}
</
p
>
)
:
null
}
<
div
className=
"mt-7 rounded-[24px] 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"
>
{
parse
(
getDynamicPostBodyHtml
(
post
))
}
{
parse
(
getDynamicPostBodyHtml
(
post
))
}
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"article-detail-styles"
>
<
style
jsx
global
>
{
`
.article-detail-content {
color: #1f2937;
font-size: 16px;
line-height: 1.85;
}
.article-detail-content p,
.article-detail-content div {
margin: 0 0 18px;
}
.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 img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
margin: 24px auto 10px;
border-radius: 14px;
}
.article-detail-content figure {
margin: 28px 0;
}
.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
>
</
main
>
<
aside
className=
"space-y-6"
>
<
EventCalendar
/>
<
aside
className=
"space-y-5"
>
<
EventsCalendar
compact
className=
"xl:w-full xl:min-w-0"
/>
<
div
className=
"overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
>
<
div
className=
"relative min-h-[390px] bg-[#1f334f]"
>
<
ImageNext
src=
"/banner.webp"
alt=
"Đối tác quảng bá"
width=
{
640
}
height=
{
760
}
className=
"absolute inset-0 h-full w-full object-cover"
/>
<
div
className=
"absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent"
/>
<
div
className=
"absolute bottom-8 left-7 right-7 text-white"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-white/70"
>
Đối tác quảng bá
</
div
>
<
div
className=
"mt-3 text-2xl font-bold leading-tight"
>
Business Combo cho doanh nghiệp hội viên
</
div
>
</
div
>
</
div
>
</
div
>
</
aside
>
</
aside
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/app/(main)/[...slug]/templates/ArticlePage.tsx
View file @
80e61988
'use client'
;
'use client'
;
import
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
Link
from
"next/link"
;
import
{
usePathname
,
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
usePathname
,
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
Spinner
}
from
"@/components/ui"
;
import
{
Spinner
}
from
"@/components/ui"
;
import
{
Pagination
}
from
"@/components/base/pagination"
;
import
{
Pagination
}
from
"@/components/base/pagination"
;
import
ListFilter
from
"@/components/base/list-filter
"
;
import
ImageNext
from
"@/components/shared/image-next
"
;
import
EventCalendar
from
"@/components/base/event-calendar
"
;
import
{
Button
}
from
"@/components/ui/button
"
;
import
ListCategory
from
"@/components/base/list-category
"
;
import
{
Input
}
from
"@/components/ui/input
"
;
import
{
import
{
buildDynamicCategoryMenu
,
buildPostFilters
,
buildPostFilters
,
fetchDynamicPostList
,
fetchDynamicPostList
,
resolveDynamicPostImage
,
stripHtml
,
stripHtml
,
}
from
"./data"
;
}
from
"./data"
;
import
type
{
DynamicCategoryRouteItem
}
from
"./types"
;
import
type
{
DynamicCategoryRouteItem
}
from
"./types"
;
import
CardNews
from
"@/components/base/card-news"
;
type
ArticlePageProps
=
{
type
ArticlePageProps
=
{
category
:
DynamicCategoryRouteItem
;
category
:
DynamicCategoryRouteItem
;
allCategories
:
DynamicCategoryRouteItem
[];
allCategories
:
DynamicCategoryRouteItem
[];
};
};
const
formatPostDate
=
(
value
?:
string
|
null
)
=>
{
if
(
!
value
)
return
""
;
const
date
=
new
Date
(
value
);
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
""
;
return
new
Intl
.
DateTimeFormat
(
"vi-VN"
,
{
day
:
"2-digit"
,
month
:
"2-digit"
,
year
:
"numeric"
,
}).
format
(
date
);
};
const
getTagClassName
=
(
index
:
number
)
=>
{
const
classes
=
[
"bg-[#eaf0ff] text-[#1f4fa3]"
,
"bg-[#e9f7ee] text-[#138040]"
,
"bg-[#fff0e3] text-[#d47a16]"
,
"bg-[#ffe9f0] text-[#d22f62]"
,
];
return
classes
[
index
%
classes
.
length
];
};
export
default
function
ArticlePage
({
category
,
allCategories
}:
ArticlePageProps
)
{
export
default
function
ArticlePage
({
category
,
allCategories
}:
ArticlePageProps
)
{
const
searchParams
=
useSearchParams
();
const
searchParams
=
useSearchParams
();
const
router
=
useRouter
();
const
router
=
useRouter
();
...
@@ -29,9 +53,10 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
...
@@ -29,9 +53,10 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
const
searchParamsString
=
searchParams
.
toString
();
const
searchParamsString
=
searchParams
.
toString
();
const
initialPage
=
Number
(
searchParams
.
get
(
"page"
)
??
"1"
);
const
initialPage
=
Number
(
searchParams
.
get
(
"page"
)
??
"1"
);
const
[
searchInput
,
setSearchInput
]
=
useState
(
""
);
const
[
submitSearch
,
setSubmitSearch
]
=
useState
(
""
);
const
[
submitSearch
,
setSubmitSearch
]
=
useState
(
""
);
const
[
page
,
setPage
]
=
useState
(
initialPage
);
const
[
page
,
setPage
]
=
useState
(
initialPage
);
const
pageSize
=
6
;
const
pageSize
=
10
;
const
keyword
=
submitSearch
.
trim
();
const
keyword
=
submitSearch
.
trim
();
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -51,10 +76,6 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
...
@@ -51,10 +76,6 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
}
}
},
[
page
,
pathname
,
router
,
searchParamsString
]);
},
[
page
,
pathname
,
router
,
searchParamsString
]);
useEffect
(()
=>
{
setPage
(
1
);
},
[
submitSearch
,
category
.
id
]);
const
postsQuery
=
useQuery
({
const
postsQuery
=
useQuery
({
queryKey
:
[
"dynamic-posts"
,
category
.
id
,
page
,
pageSize
,
keyword
],
queryKey
:
[
"dynamic-posts"
,
category
.
id
,
page
,
pageSize
,
keyword
],
queryFn
:
()
=>
queryFn
:
()
=>
...
@@ -73,78 +94,98 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
...
@@ -73,78 +94,98 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
staleTime
:
60
*
1000
,
staleTime
:
60
*
1000
,
});
});
const
categoryMenu
=
useMemo
(
()
=>
buildDynamicCategoryMenu
(
category
,
allCategories
),
[
category
,
allCategories
],
);
const
totalPages
=
postsQuery
.
data
?.
totalPages
??
1
;
const
totalPages
=
postsQuery
.
data
?.
totalPages
??
1
;
const
currentPage
=
Math
.
min
(
page
,
totalPages
);
const
currentPage
=
Math
.
min
(
page
,
totalPages
);
const
paginatedPosts
=
postsQuery
.
data
?.
rows
??
[];
const
paginatedPosts
=
postsQuery
.
data
?.
rows
??
[];
const
categoryIndexMap
=
useMemo
(()
=>
{
const
entries
=
allCategories
.
map
((
item
,
index
)
=>
[
item
.
id
,
index
]
as
const
);
return
new
Map
(
entries
);
},
[
allCategories
]);
return
(
return
(
<
div
className=
"min-h-screen
container mx-auto
"
>
<
div
className=
"min-h-screen
bg-[#fbfbfa]
"
>
{
postsQuery
.
isLoading
?
(
{
postsQuery
.
isLoading
?
(
<
div
className=
"flex justify-center items-center w-full h-64"
>
<
div
className=
"flex justify-center items-center w-full h-64"
>
<
Spinner
/>
<
Spinner
/>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"w-full flex flex-col gap-5"
>
<
div
className=
"container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10"
>
{
categoryMenu
.
length
>
0
?
<
ListCategory
categories=
{
categoryMenu
}
/>
:
<
br
/>
}
<
div
className=
"mb-8"
>
<
h1
className=
"text-3xl font-bold leading-tight text-[#111827] md:text-4xl"
>
{
category
.
name
}
</
h1
>
<
div
className=
"mt-2 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
</
div
>
<
div
className=
"
grid grid-cols-1 lg:grid-cols-3 gap-6
"
>
<
div
className=
"
flex flex-col gap-10 xl:flex-row xl:gap-14
"
>
<
main
className=
"
lg:col-span-2 bg-white
"
>
<
main
className=
"
order-2 min-w-0 xl:order-1 xl:flex-1
"
>
<
div
className=
"
pb-5 overflow-hidden
"
>
<
div
className=
"
space-y-9
"
>
{
paginatedPosts
.
length
?
(
{
paginatedPosts
.
length
?
(
paginatedPosts
.
map
((
item
)
=>
{
paginatedPosts
.
map
((
item
,
index
)
=>
{
const
fallbackDescription
=
item
.
content_structure
?.
post_content
const
fallbackDescription
=
item
.
content_structure
?.
post_content
?.
map
((
section
)
=>
section
.
content
)
?.
map
((
section
)
=>
section
.
content
)
.
join
(
" "
);
.
join
(
" "
);
const
description
=
item
.
summary
||
stripHtml
(
item
.
content
)
||
stripHtml
(
fallbackDescription
);
const
primaryCategory
=
item
.
categories
[
0
];
const
tagIndex
=
categoryIndexMap
.
get
(
primaryCategory
?.
id
??
""
)
??
index
;
const
date
=
formatPostDate
(
item
.
release_at
??
item
.
published_at
??
item
.
created_at
,
);
return
(
return
(
<
CardNews
<
article
key=
{
item
.
id
}
key=
{
item
.
id
}
news=
{
{
className=
"border-b border-[#eceff3] pb-8 last:border-b-0"
id
:
item
.
id
,
>
title
:
item
.
title
,
<
Link
thumbnail
:
href=
{
item
.
external_link
}
item
.
thumbnail
?.
path
??
className=
"group grid gap-5 sm:grid-cols-[250px_minmax(0,1fr)]"
item
.
thumbnail
?.
original
??
>
item
.
thumbnail
?.
url
??
<
div
className=
"overflow-hidden rounded-md bg-[#edf1f5]"
>
""
,
<
ImageNext
external_link
:
item
.
external_link
,
src=
{
resolveDynamicPostImage
(
item
.
thumbnail
)
}
description
:
alt=
{
item
.
title
}
item
.
summary
||
width=
{
520
}
stripHtml
(
item
.
content
)
||
height=
{
360
}
stripHtml
(
fallbackDescription
),
className=
"h-[170px] w-full object-cover transition-transform duration-500 group-hover:scale-[1.03] sm:h-[150px]"
release_at
:
/>
item
.
release_at
??
item
.
published_at
??
item
.
created_at
??
""
,
</
div
>
is_active
:
item
.
is_active
,
created_at
:
item
.
created_at
??
""
,
<
div
className=
"min-w-0 pt-1"
>
created_by
:
null
,
<
div
className=
"flex flex-wrap items-center gap-3 text-xs"
>
updated_at
:
item
.
created_at
??
""
,
<
span
updated_by
:
null
,
className=
{
`rounded-full px-2.5 py-1 font-semibold ${getTagClassName(tagIndex)}`
}
mode
:
"NOW"
,
>
category
:
category
.
name
,
{
primaryCategory
?.
name
||
category
.
name
}
page_config
:
{
</
span
>
id
:
category
.
id
,
{
date
?
<
span
className=
"text-[#9aa3ad]"
>
{
date
}
</
span
>
:
null
}
name
:
category
.
name
,
</
div
>
static_link
:
category
.
url
,
static_link_en
:
category
.
url
,
<
h2
className=
"mt-3 line-clamp-2 text-[18px] font-bold leading-snug text-[#111827] transition-colors group-hover:text-[#144c9c]"
>
code
:
category
.
slug
,
{
item
.
title
}
},
</
h2
>
}
}
link=
{
item
.
external_link
}
{
description
?
(
/>
<
p
className=
"mt-2 line-clamp-3 text-sm leading-6 text-[#5f6875]"
>
{
description
}
</
p
>
)
:
null
}
</
div
>
</
Link
>
</
article
>
);
);
})
})
)
:
(
)
:
(
<
div
className=
"rounded-
lg border
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."
}
{
"Ch
\
u01b0a c
\
u00f3 b
\
u00e0i vi
\
u1ebft ph
\
u00f9 h
\
u1ee3p trong danh m
\
u1ee5c n
\
u00e0y."
}
</
div
>
</
div
>
)
}
)
}
<
div
className=
"
w-full flex justify-center mt-4
"
>
<
div
className=
"
flex w-full justify-center pt-2
"
>
<
Pagination
<
Pagination
pageCount=
{
totalPages
}
pageCount=
{
totalPages
}
page=
{
currentPage
}
page=
{
currentPage
}
...
@@ -156,12 +197,62 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
...
@@ -156,12 +197,62 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
</
div
>
</
div
>
</
main
>
</
main
>
<
aside
className=
"space-y-6"
>
<
aside
className=
"order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0"
>
<
ListFilter
onSearch=
{
setSubmitSearch
}
onReset=
{
()
=>
setSubmitSearch
(
""
)
}
/>
<
form
<
EventCalendar
/>
className=
"rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
<
div
className=
"bg-white border rounded-md overflow-hidden"
>
onSubmit=
{
(
event
)
=>
{
<
div
className=
"w-full relative bg-gray-100"
>
event
.
preventDefault
();
<
img
src=
"/banner.webp"
alt=
{
"Qu
\
u1ea3ng c
\
u00e1o"
}
className=
"object-cover"
/>
setPage
(
1
);
setSubmitSearch
(
searchInput
);
}
}
>
<
h2
className=
"text-lg font-bold text-[#111827]"
>
Tìm kiếm
</
h2
>
<
Input
value=
{
searchInput
}
onChange=
{
(
event
)
=>
setSearchInput
(
event
.
target
.
value
)
}
placeholder=
"Tên bài viết ..."
className=
"mt-4 h-11 rounded-xl border-[#edf1f5] bg-[#f8fafc] text-sm placeholder:text-gray-700"
/>
<
div
className=
"mt-4 grid grid-cols-2 gap-3"
>
<
Button
type=
"submit"
className=
"h-11 rounded-xl bg-[#14519f] text-white hover:bg-[#0f4386]"
>
Tìm kiếm
</
Button
>
<
Button
type=
"button"
variant=
"outline"
className=
"h-11 rounded-xl border-[#edf1f5] bg-white text-[#4b5563]"
onClick=
{
()
=>
{
setSearchInput
(
""
);
setPage
(
1
);
setSubmitSearch
(
""
);
}
}
>
Bỏ tìm
</
Button
>
</
div
>
</
form
>
<
div
className=
"overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
>
<
div
className=
"relative min-h-[390px] bg-[#1f334f]"
>
<
ImageNext
src=
"/banner.webp"
alt=
"Đối tác quảng bá"
width=
{
640
}
height=
{
760
}
className=
"absolute inset-0 h-full w-full object-cover"
/>
<
div
className=
"absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent"
/>
<
div
className=
"absolute bottom-8 left-7 right-7 text-white"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-white/70"
>
Đối tác quảng bá
</
div
>
<
div
className=
"mt-3 text-2xl font-bold leading-tight"
>
Business Combo cho doanh nghiệp hội viên
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
aside
>
</
aside
>
...
...
src/app/(main)/[...slug]/templates/CatalogPage.tsx
0 → 100644
View file @
80e61988
'use client'
;
import
{
useEffect
,
useState
}
from
"react"
;
import
Link
from
"next/link"
;
import
{
usePathname
,
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
Spinner
}
from
"@/components/ui"
;
import
{
Pagination
}
from
"@/components/base/pagination"
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
buildPostFilters
,
fetchDynamicPostList
,
resolveDynamicPostImage
,
}
from
"./data"
;
import
type
{
DynamicCategoryRouteItem
}
from
"./types"
;
type
CatalogPageProps
=
{
category
:
DynamicCategoryRouteItem
;
allCategories
:
DynamicCategoryRouteItem
[];
};
const
getCatalogImageClassName
=
(
index
:
number
)
=>
index
%
4
===
0
?
"object-cover"
:
index
%
4
===
1
?
"object-contain bg-[#f0f3f8]"
:
index
%
4
===
2
?
"object-cover object-center"
:
"object-contain bg-[#eef4fb]"
;
export
default
function
CatalogPage
({
category
,
allCategories
}:
CatalogPageProps
)
{
const
searchParams
=
useSearchParams
();
const
router
=
useRouter
();
const
pathname
=
usePathname
();
const
searchParamsString
=
searchParams
.
toString
();
const
initialPage
=
Number
(
searchParams
.
get
(
"page"
)
??
"1"
);
const
[
searchInput
,
setSearchInput
]
=
useState
(
""
);
const
[
submitSearch
,
setSubmitSearch
]
=
useState
(
""
);
const
[
page
,
setPage
]
=
useState
(
initialPage
);
const
pageSize
=
8
;
const
keyword
=
submitSearch
.
trim
();
useEffect
(()
=>
{
const
params
=
new
URLSearchParams
(
searchParamsString
);
if
(
page
>
1
)
{
params
.
set
(
"page"
,
String
(
page
));
}
else
{
params
.
delete
(
"page"
);
}
const
qs
=
params
.
toString
();
const
nextUrl
=
qs
?
`
${
pathname
}
?
${
qs
}
`
:
pathname
;
const
currentUrl
=
searchParamsString
?
`
${
pathname
}
?
${
searchParamsString
}
`
:
pathname
;
if
(
nextUrl
!==
currentUrl
)
{
router
.
replace
(
nextUrl
,
{
scroll
:
false
});
}
},
[
page
,
pathname
,
router
,
searchParamsString
]);
const
postsQuery
=
useQuery
({
queryKey
:
[
"catalog-posts"
,
category
.
id
,
page
,
pageSize
,
keyword
],
queryFn
:
()
=>
fetchDynamicPostList
({
page
,
pageSize
,
filters
:
buildPostFilters
([
`category.id==
${
category
.
id
}
`
,
"is_hidden==false"
,
"is_active==true"
,
"status==published"
,
"type==news"
,
keyword
?
`title@=
${
keyword
}
`
:
null
,
]),
}),
staleTime
:
60
*
1000
,
});
const
totalPages
=
postsQuery
.
data
?.
totalPages
??
1
;
const
currentPage
=
Math
.
min
(
page
,
totalPages
);
const
paginatedPosts
=
postsQuery
.
data
?.
rows
??
[];
return
(
<
div
className=
"min-h-screen bg-[#fbfbfa]"
>
{
postsQuery
.
isLoading
?
(
<
div
className=
"flex h-64 w-full items-center justify-center"
>
<
Spinner
/>
</
div
>
)
:
(
<
div
className=
"container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10"
>
<
div
className=
"mb-8"
>
<
h1
className=
"text-3xl font-bold leading-tight text-[#111827] md:text-4xl"
>
{
category
.
name
}
</
h1
>
<
div
className=
"mt-2 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
</
div
>
<
div
className=
"flex flex-col gap-10 xl:flex-row xl:gap-14"
>
<
main
className=
"order-2 min-w-0 xl:order-1 xl:flex-1"
>
{
paginatedPosts
.
length
?
(
<
div
className=
"grid grid-cols-2 gap-5 sm:grid-cols-3 xl:grid-cols-4 xl:gap-6"
>
{
paginatedPosts
.
map
((
item
,
index
)
=>
{
return
(
<
Link
key=
{
item
.
id
}
href=
{
item
.
external_link
}
className=
"group block"
>
<
div
className=
"overflow-hidden bg-white shadow-[0_10px_24px_rgba(17,24,39,0.08)]"
>
<
div
className=
"relative aspect-[3/4] overflow-hidden bg-white"
>
<
ImageNext
src=
{
resolveDynamicPostImage
(
item
.
thumbnail
)
}
alt=
{
item
.
title
}
width=
{
520
}
height=
{
693
}
className=
{
`h-full w-full transition-transform duration-500 group-hover:scale-[1.03] ${getCatalogImageClassName(index)}`
}
/>
</
div
>
</
div
>
<
div
className=
"px-1 pt-3 text-center"
>
<
h2
className=
"line-clamp-2 text-[14px] leading-[1.45] text-[#1f2f57]"
>
{
item
.
title
}
</
h2
>
</
div
>
</
Link
>
);
})
}
</
div
>
)
:
(
<
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."
}
</
div
>
)
}
<
div
className=
"flex w-full justify-center pt-8"
>
<
Pagination
pageCount=
{
totalPages
}
page=
{
currentPage
}
onChangePage=
{
setPage
}
onGoToPreviousPage=
{
()
=>
setPage
(
Math
.
max
(
1
,
currentPage
-
1
))
}
onGoToNextPage=
{
()
=>
setPage
(
Math
.
min
(
totalPages
,
currentPage
+
1
))
}
/>
</
div
>
</
main
>
<
aside
className=
"order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0"
>
<
form
className=
"rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
onSubmit=
{
(
event
)
=>
{
event
.
preventDefault
();
setPage
(
1
);
setSubmitSearch
(
searchInput
);
}
}
>
<
h2
className=
"text-lg font-bold text-[#111827]"
>
Tìm kiếm
</
h2
>
<
Input
value=
{
searchInput
}
onChange=
{
(
event
)
=>
setSearchInput
(
event
.
target
.
value
)
}
placeholder=
"Tên bài viết ..."
className=
"mt-4 h-11 rounded-xl border-[#edf1f5] bg-[#f8fafc] text-sm placeholder:text-gray-700"
/>
<
div
className=
"mt-4 grid grid-cols-2 gap-3"
>
<
Button
type=
"submit"
className=
"h-11 rounded-xl bg-[#14519f] text-white hover:bg-[#0f4386]"
>
Tìm kiếm
</
Button
>
<
Button
type=
"button"
variant=
"outline"
className=
"h-11 rounded-xl border-[#edf1f5] bg-white text-[#4b5563]"
onClick=
{
()
=>
{
setSearchInput
(
""
);
setPage
(
1
);
setSubmitSearch
(
""
);
}
}
>
Bỏ tìm
</
Button
>
</
div
>
</
form
>
<
div
className=
"overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
>
<
div
className=
"relative min-h-[390px] bg-[#1f334f]"
>
<
ImageNext
src=
"/banner.webp"
alt=
"Đối tác quảng bá"
width=
{
640
}
height=
{
760
}
className=
"absolute inset-0 h-full w-full object-cover"
/>
<
div
className=
"absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent"
/>
<
div
className=
"absolute bottom-8 left-7 right-7 text-white"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-white/70"
>
Đối tác quảng bá
</
div
>
<
div
className=
"mt-3 text-2xl font-bold leading-tight"
>
Business Combo cho doanh nghiệp hội viên
</
div
>
</
div
>
</
div
>
</
div
>
</
aside
>
</
div
>
</
div
>
)
}
</
div
>
);
}
src/app/(main)/[...slug]/templates/EventDetailPage.tsx
View file @
80e61988
...
@@ -37,24 +37,24 @@ export default function EventDetailPage() {
...
@@ -37,24 +37,24 @@ export default function EventDetailPage() {
return
notFound
();
return
notFound
();
}
}
return
(
return
(
<
div
className=
'container
w-full flex justify-center items-center pb
-10'
>
<
div
className=
'container
flex w-full items-center justify-center px-4 pb-10 sm:px-6 lg:px
-10'
>
{
isLoading
?
(
{
isLoading
?
(
<
div
className=
"flex justify-center items-center w-full h-64"
>
<
div
className=
"flex justify-center items-center w-full h-64"
>
<
Spinner
/>
<
Spinner
/>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
'flex
flex-col gap-5 w-full
'
>
<
div
className=
'flex
w-full flex-col gap-5
'
>
<
ListCategory
categories=
{
category
?.
responseData
?.
children
}
/>
<
ListCategory
categories=
{
category
?.
responseData
?.
children
}
/>
<
div
className=
"grid grid-cols-1
lg:grid-cols-3 gap-5
"
>
<
div
className=
"grid grid-cols-1
gap-5 xl:grid-cols-[minmax(0,1fr)_340px]
"
>
<
main
className=
"
lg:col-span-2 bg-white border rounded-md py-10 px-5 md:px-2
0"
>
<
main
className=
"
min-w-0 rounded-md border bg-white px-4 py-6 sm:px-6 md:px-10 lg:px-14 lg:py-1
0"
>
<
div
className=
'pb-5 text-primary text-2xl leading-normal font-medium'
>
<
div
className=
'pb-5 text-primary text-2xl leading-normal font-medium'
>
{
eventsDetail
?.
responseData
?.
rows
[
0
]?.
name
}
{
eventsDetail
?.
responseData
?.
rows
[
0
]?.
name
}
</
div
>
</
div
>
<
hr
className=
"py-2"
/>
<
hr
className=
"py-2"
/>
{
/* Top summary with image + details */
}
{
/* Top summary with image + details */
}
<
div
className=
"
flex flex-col md:flex-row gap-6 my-6
"
>
<
div
className=
"
my-6 flex flex-col gap-6 md:flex-row
"
>
<
div
className=
"w-full
lg:w-1/2 bg-gray-50 rounded-md overflow-hidden
"
>
<
div
className=
"w-full
overflow-hidden rounded-md bg-gray-50 md:w-1/2
"
>
{
eventsDetail
?.
responseData
?.
rows
[
0
].
image
?
(
{
eventsDetail
?.
responseData
?.
rows
[
0
].
image
?
(
<
div
className=
"w-full h-52 relative "
>
<
div
className=
"w-full h-52 relative "
>
<
EventImage
<
EventImage
...
@@ -67,7 +67,7 @@ export default function EventDetailPage() {
...
@@ -67,7 +67,7 @@ export default function EventDetailPage() {
)
}
)
}
</
div
>
</
div
>
<
div
className=
"w-full
lg:w-1/2 bg-white border rounded-md p-3
md:p-6"
>
<
div
className=
"w-full
rounded-md border bg-white p-3 md:w-1/2
md:p-6"
>
<
div
className=
"flex flex-col gap-3"
>
<
div
className=
"flex flex-col gap-3"
>
<
div
className=
"text-sm text-gray-500 flex flex-row items-center gap-2"
>
<
div
className=
"text-sm text-gray-500 flex flex-row items-center gap-2"
>
<
Clock
className=
"h-5 w-5 text-yellow-500"
/>
<
Clock
className=
"h-5 w-5 text-yellow-500"
/>
...
@@ -112,13 +112,13 @@ export default function EventDetailPage() {
...
@@ -112,13 +112,13 @@ export default function EventDetailPage() {
</
div
>
</
div
>
{
/* Full description */
}
{
/* Full description */
}
<
div
className=
"prose tiptap overflow-hidden"
>
<
div
className=
"prose tiptap
max-w-none
overflow-hidden"
>
{
parse
(
eventsDetail
?.
responseData
?.
rows
[
0
]?.
description
??
""
)
}
{
parse
(
eventsDetail
?.
responseData
?.
rows
[
0
]?.
description
??
""
)
}
</
div
>
</
div
>
</
main
>
</
main
>
{
/* Sidebar */
}
{
/* Sidebar */
}
<
aside
className=
"space-y-6"
>
<
aside
className=
"
min-w-0
space-y-6"
>
<
EventCalendar
/>
<
EventCalendar
/>
<
div
className=
"bg-white border rounded-md overflow-hidden"
>
<
div
className=
"bg-white border rounded-md overflow-hidden"
>
<
div
className=
"w-full h-75 relative bg-gray-100"
>
<
div
className=
"w-full h-75 relative bg-gray-100"
>
...
@@ -159,4 +159,4 @@ function EventImage({ src, alt }: EventImageProps) {
...
@@ -159,4 +159,4 @@ function EventImage({ src, alt }: EventImageProps) {
}
}
}
}
/>
/>
);
);
}
}
\ No newline at end of file
src/app/(main)/[...slug]/templates/EventPage.tsx
View file @
80e61988
...
@@ -61,17 +61,17 @@ export default function EventPage() {
...
@@ -61,17 +61,17 @@ export default function EventPage() {
//template
//template
return
(
return
(
<>
<>
<
div
className=
"
min-h-screen container mx-auto
"
>
<
div
className=
"
container mx-auto min-h-screen px-4 py-6 sm:px-6 lg:px-10
"
>
{
eventsLoading
?
(
{
eventsLoading
?
(
<
div
className=
"flex justify-center items-center w-full h-64"
>
<
div
className=
"flex justify-center items-center w-full h-64"
>
<
Spinner
/>
<
Spinner
/>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"
w-full flex
flex-col gap-5"
>
<
div
className=
"
flex w-full
flex-col gap-5"
>
<
ListCategory
categories=
{
categoriesPage
?.
responseData
?.
children
}
/>
<
ListCategory
categories=
{
categoriesPage
?.
responseData
?.
children
}
/>
<
div
className=
"grid grid-cols-1
lg:grid-cols-3 gap-6
"
>
<
div
className=
"grid grid-cols-1
gap-6 xl:grid-cols-[minmax(0,1fr)_340px]
"
>
<
main
className=
"
lg:col-span-2
bg-background"
>
<
main
className=
"
min-w-0
bg-background"
>
<
div
className=
"
pb-5 overflow-hidden
"
>
<
div
className=
"
overflow-hidden pb-5
"
>
{
events
?.
responseData
?.
rows
?.
map
((
item
)
=>
(
{
events
?.
responseData
?.
rows
?.
map
((
item
)
=>
(
<
CardEvents
<
CardEvents
key=
{
item
.
id
}
key=
{
item
.
id
}
...
@@ -92,7 +92,7 @@ export default function EventPage() {
...
@@ -92,7 +92,7 @@ export default function EventPage() {
</
div
>
</
div
>
</
div
>
</
div
>
</
main
>
</
main
>
<
aside
className=
"space-y-6"
>
<
aside
className=
"
min-w-0
space-y-6"
>
<
ListFilter
onSearch=
{
setSubmitSearch
}
/>
<
ListFilter
onSearch=
{
setSubmitSearch
}
/>
<
EventCalendar
/>
<
EventCalendar
/>
</
aside
>
</
aside
>
...
...
src/app/(main)/[...slug]/templates/InformationPage.tsx
View file @
80e61988
'use client'
;
'use client'
;
import
dayjs
from
"dayjs"
;
import
parse
from
"html-react-parser"
;
import
parse
from
"html-react-parser"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
{
getDynamicPostBodyHtml
}
from
"./data"
;
import
{
buildDynamicCategoryMenu
,
getDynamicPostBodyHtml
,
}
from
"./data"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
import
type
{
DynamicCategoryRouteItem
,
DynamicPostItem
}
from
"./types"
;
type
InformationPageProps
=
{
type
InformationPageProps
=
{
...
@@ -17,24 +14,102 @@ type InformationPageProps = {
...
@@ -17,24 +14,102 @@ type InformationPageProps = {
export
default
function
InformationPage
({
export
default
function
InformationPage
({
post
,
post
,
category
,
category
,
allCategories
,
}:
InformationPageProps
)
{
}:
InformationPageProps
)
{
const
categoryMenu
=
buildDynamicCategoryMenu
(
category
,
allCategories
);
const
publishedDate
=
dayjs
(
post
.
release_at
??
post
.
published_at
??
post
.
created_at
,
).
format
(
"DD/MM/YYYY"
);
return
(
return
(
<
div
className=
"container w-full flex justify-center items-center pb-10"
>
<
div
className=
"min-h-screen bg-[#fbfbfa]"
>
<
div
className=
"flex flex-col gap-5 w-full"
>
<
div
className=
"container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10"
>
{
categoryMenu
.
length
>
0
?
<
ListCategory
categories=
{
categoryMenu
}
/>
:
<
br
/>
}
<
main
className=
"w-full"
>
<
main
className=
"bg-white border rounded-md py-10 px-5 md:px-20 lg:px-20"
>
<
div
className=
"mb-5 flex flex-wrap items-center gap-3 text-xs"
>
<
div
className=
"text-primary text-2xl leading-normal font-bold"
>
<
span
className=
"rounded-full bg-[#eaf0ff] px-2.5 py-1 font-semibold text-[#1f4fa3]"
>
{
post
.
title
}
{
category
.
name
}
</
span
>
<
span
className=
"text-[#9aa3ad]"
>
{
publishedDate
}
</
span
>
</
div
>
</
div
>
<
hr
className=
"my-5"
/>
<
div
className=
"flex-1 text-app-grey text-base overflow-hidden"
>
<
h1
className=
"max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]"
>
<
div
className=
"prose tiptap max-w-none overflow-hidden"
>
{
post
.
title
}
</
h1
>
<
div
className=
"mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
{
post
.
summary
?
(
<
p
className=
"mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"
>
{
post
.
summary
}
</
p
>
)
:
null
}
<
div
className=
"mt-7 rounded-[24px] 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"
>
{
parse
(
getDynamicPostBodyHtml
(
post
))
}
{
parse
(
getDynamicPostBodyHtml
(
post
))
}
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"page-detail-styles"
>
<
style
jsx
global
>
{
`
.page-detail-content {
color: #1f2937;
font-size: 16px;
line-height: 1.85;
}
.page-detail-content p,
.page-detail-content div {
margin: 0 0 18px;
}
.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 img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
margin: 24px auto 10px;
border-radius: 14px;
}
.page-detail-content figure {
margin: 28px 0;
}
.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)/_lib/layout/header.tsx
View file @
80e61988
...
@@ -143,12 +143,12 @@ function Header() {
...
@@ -143,12 +143,12 @@ function Header() {
{
"
\
u0110
\
u0103ng K
\
u00fd H
\
u1ed9i Vi
\
u00ean"
}
{
"
\
u0110
\
u0103ng K
\
u00fd H
\
u1ed9i Vi
\
u00ean"
}
</
Link
>
</
Link
>
</
div
>
</
div
>
<
Link
{
/*
<Link
className="px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
className="px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
href="/site-map"
href="/site-map"
>
>
Sitemap
Sitemap
</
Link
>
</Link>
*/
}
<
Link
<
Link
className=
"px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
className=
"px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
href=
"https://vccihcm.vn/lien-he"
href=
"https://vccihcm.vn/lien-he"
...
@@ -252,13 +252,15 @@ function Header() {
...
@@ -252,13 +252,15 @@ function Header() {
</
div
>
</
div
>
<
div
<
div
className=
{
`overflow-hidden border-t border-slate-200 bg-white transition-all duration-300 lg:hidden ${
className=
{
`fixed left-0 right-0 top-[80px] z-40 border-t border-slate-200 bg-white transition-all duration-300 lg:hidden ${
toggleMenu ? "max-h-[520px] opacity-100" : "max-h-0 opacity-0"
toggleMenu
? "pointer-events-auto h-[calc(100dvh-80px)] translate-y-0 opacity-100"
: "pointer-events-none h-[calc(100dvh-80px)] -translate-y-2 opacity-0"
}`
}
}`
}
>
>
<
div
className=
"px-4 py-3"
>
<
div
className=
"
flex h-full flex-col overflow-y-auto overscroll-contain
px-4 py-3"
>
<
input
<
input
className=
"h-11 w-full rounded-md border border-slate-200 px-4 text-sm outline-none placeholder:text-slate-400 focus:border-[#2f57ff]"
className=
"h-11 w-full
shrink-0
rounded-md border border-slate-200 px-4 text-sm outline-none placeholder:text-slate-400 focus:border-[#2f57ff]"
type=
"text"
type=
"text"
placeholder=
{
"T
\
u00ecm ki
\
u1ebfm"
}
placeholder=
{
"T
\
u00ecm ki
\
u1ebfm"
}
onKeyDown=
{
(
e
)
=>
{
onKeyDown=
{
(
e
)
=>
{
...
@@ -270,34 +272,34 @@ function Header() {
...
@@ -270,34 +272,34 @@ function Header() {
}
}
}
}
}
}
/>
/>
</
div
>
<
div
className=
"pb-3"
>
<
div
className=
"pb-6"
>
{
menuItems
.
map
((
category
)
=>
(
{
menuItems
.
map
((
category
)
=>
(
<
div
key=
{
category
.
id
}
className=
"border-t border-slate-100 first:border-t-0"
>
<
div
key=
{
category
.
id
}
className=
"border-t border-slate-100 first:border-t-0"
>
<
Link
<
Link
href=
{
category
.
url
||
"#"
}
href=
{
category
.
url
||
"#"
}
className=
"block px-5 py-3 text-[15px] font-medium text-slate-700 transition hover:bg-slate-50 hover:text-[#2f57ff]"
className=
"block px-5 py-3 text-[15px] font-medium text-slate-700 transition hover:bg-slate-50 hover:text-[#2f57ff]"
onClick=
{
()
=>
setToggleMenu
(
false
)
}
onClick=
{
()
=>
setToggleMenu
(
false
)
}
>
>
{
category
.
name
}
{
category
.
name
}
</
Link
>
</
Link
>
{
category
.
children
.
length
>
0
?
(
{
category
.
children
.
length
>
0
?
(
<
div
className=
"pb-2 pl-8 pr-5"
>
<
div
className=
"pb-2 pl-8 pr-5"
>
{
category
.
children
.
map
((
child
)
=>
(
{
category
.
children
.
map
((
child
)
=>
(
<
Link
<
Link
key=
{
child
.
id
}
key=
{
child
.
id
}
href=
{
child
.
url
||
"#"
}
href=
{
child
.
url
||
"#"
}
className=
"block py-2 text-sm text-slate-500 transition hover:text-[#2f57ff]"
className=
"block py-2 text-sm text-slate-500 transition hover:text-[#2f57ff]"
onClick=
{
()
=>
setToggleMenu
(
false
)
}
onClick=
{
()
=>
setToggleMenu
(
false
)
}
>
>
{
child
.
name
}
{
child
.
name
}
</
Link
>
</
Link
>
))
}
))
}
</
div
>
</
div
>
)
:
null
}
)
:
null
}
</
div
>
</
div
>
))
}
))
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
header
>
</
header
>
...
...
src/app/(main)/search/page.tsx
View file @
80e61988
"use client"
;
"use client"
;
import
React
,
{
useState
,
Suspense
,
useEffect
}
from
"react"
;
import
{
Suspense
,
useEffect
,
useState
}
from
"react"
;
import
ListCategory
from
"@/components/base/list-category"
;
import
Link
from
"next/link"
;
import
ListFilter
from
"@/components/base/list-filter"
;
import
{
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
CardNews
from
"@/components/base/card-news"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
ImageNext
from
"@/components/shared/image-next"
;
import
{
Pagination
}
from
"@components/base/pagination"
;
import
{
Pagination
}
from
"@components/base/pagination"
;
import
Image
from
"next/image"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
useGetNews
}
from
"@api/endpoints/news"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
GetNewsResponseType
}
from
"@api/types/news"
;
import
{
Spinner
}
from
"@components/ui/spinner"
;
import
{
Spinner
}
from
"@components/ui/spinner"
;
import
{
useSearchParams
,
useRouter
}
from
'next/navigation'
import
{
buildPostFilters
,
fetchDynamicPostList
,
resolveDynamicPostImage
,
stripHtml
,
}
from
"@/app/(main)/[...slug]/templates/data"
;
import
type
{
DynamicPostItem
}
from
"@/app/(main)/[...slug]/templates/types"
;
const
formatPostDate
=
(
value
?:
string
|
null
)
=>
{
if
(
!
value
)
return
""
;
const
date
=
new
Date
(
value
);
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
""
;
return
new
Intl
.
DateTimeFormat
(
"vi-VN"
,
{
day
:
"2-digit"
,
month
:
"2-digit"
,
year
:
"numeric"
,
}).
format
(
date
);
};
const
getTagClassName
=
(
index
:
number
)
=>
{
const
classes
=
[
"bg-[#eaf0ff] text-[#1f4fa3]"
,
"bg-[#e9f7ee] text-[#138040]"
,
"bg-[#fff0e3] text-[#d47a16]"
,
"bg-[#ffe9f0] text-[#d22f62]"
,
];
return
classes
[
index
%
classes
.
length
];
};
function
SearchResultItem
({
item
,
index
}:
{
item
:
DynamicPostItem
;
index
:
number
})
{
const
fallbackDescription
=
item
.
content_structure
?.
post_content
?.
map
((
section
)
=>
section
.
content
)
.
join
(
" "
);
const
description
=
item
.
summary
||
stripHtml
(
item
.
content
)
||
stripHtml
(
fallbackDescription
);
const
date
=
formatPostDate
(
item
.
release_at
||
item
.
published_at
||
item
.
created_at
);
const
categoryName
=
item
.
categories
[
0
]?.
name
||
"Tin tức"
;
return
(
<
article
className=
"border-b border-[#eceff3] pb-8 last:border-b-0"
>
<
Link
href=
{
item
.
external_link
||
"#"
}
className=
"group grid gap-5 sm:grid-cols-[250px_minmax(0,1fr)]"
>
<
div
className=
"overflow-hidden rounded-md bg-[#edf1f5]"
>
<
ImageNext
src=
{
resolveDynamicPostImage
(
item
.
thumbnail
)
}
alt=
{
item
.
title
}
width=
{
520
}
height=
{
360
}
className=
"h-[170px] w-full object-cover transition-transform duration-500 group-hover:scale-[1.03] sm:h-[150px]"
/>
</
div
>
<
div
className=
"min-w-0 pt-1"
>
<
div
className=
"flex flex-wrap items-center gap-3 text-xs"
>
<
span
className=
{
`rounded-full px-2.5 py-1 font-semibold ${getTagClassName(index)}`
}
>
{
categoryName
}
</
span
>
{
date
?
<
span
className=
"text-[#9aa3ad]"
>
{
date
}
</
span
>
:
null
}
</
div
>
<
h2
className=
"mt-3 line-clamp-2 text-[18px] font-bold leading-snug text-[#111827] transition-colors group-hover:text-[#144c9c]"
>
{
item
.
title
}
</
h2
>
{
description
?
(
<
p
className=
"mt-2 line-clamp-3 text-sm leading-6 text-[#5f6875]"
>
{
description
}
</
p
>
)
:
null
}
</
div
>
</
Link
>
</
article
>
);
}
function
SearchContent
()
{
function
SearchContent
()
{
const
router
=
useRouter
();
const
router
=
useRouter
();
const
searchParams
=
useSearchParams
();
const
searchParams
=
useSearchParams
();
const
query
=
searchParams
.
get
(
'q'
)
||
''
;
const
query
=
searchParams
.
get
(
"q"
)
||
""
;
const
pageFromUrl
=
searchParams
.
get
(
'page'
);
const
pageFromUrl
=
searchParams
.
get
(
"page"
);
const
[
page
,
setPage
]
=
useState
(
pageFromUrl
?
parseInt
(
pageFromUrl
)
:
1
);
const
[
page
,
setPage
]
=
useState
(
pageFromUrl
?
Number
(
pageFromUrl
)
:
1
);
const
[
searchInput
,
setSearchInput
]
=
useState
(
query
);
const
pageSize
=
5
;
const
{
data
:
allData
,
isLoading
}
=
useGetNews
<
GetNewsResponseType
>
({
pageSize
:
String
(
pageSize
),
currentPage
:
String
(
page
),
filters
:
query
?
`title @=
${
query
}
`
:
undefined
,
});
// Update URL when page changes
const
pageSize
=
10
;
useEffect
(()
=>
{
const
postsQuery
=
useQuery
({
const
params
=
new
URLSearchParams
(
searchParams
.
toString
());
queryKey
:
[
"search-posts"
,
page
,
pageSize
,
query
],
params
.
set
(
'page'
,
String
(
page
));
queryFn
:
()
=>
router
.
push
(
`/search?
${
params
.
toString
()}
`
,
{
scroll
:
false
});
fetchDynamicPostList
({
},
[
page
]);
page
,
pageSize
,
filters
:
buildPostFilters
([
"is_hidden==false"
,
"is_active==true"
,
"status==published"
,
"type==news"
,
query
?
`title@=
${
query
}
`
:
null
,
]),
}),
staleTime
:
60
*
1000
,
});
// Sync state with URL on mount/change
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
pageFromUrl
)
{
const
nextPage
=
pageFromUrl
?
Number
(
pageFromUrl
)
:
1
;
setPage
(
parseInt
(
pageFromUrl
));
if
(
Number
.
isFinite
(
nextPage
))
{
setPage
(
nextPage
);
}
}
},
[
pageFromUrl
]);
},
[
pageFromUrl
]);
useEffect
(()
=>
{
setSearchInput
(
query
);
},
[
query
]);
const
updateSearchUrl
=
(
nextQuery
:
string
,
nextPage
=
1
)
=>
{
const
params
=
new
URLSearchParams
();
const
trimmedQuery
=
nextQuery
.
trim
();
if
(
trimmedQuery
)
params
.
set
(
"q"
,
trimmedQuery
);
params
.
set
(
"page"
,
String
(
nextPage
));
router
.
push
(
`/search?
${
params
.
toString
()}
`
,
{
scroll
:
false
});
};
const
rows
=
postsQuery
.
data
?.
rows
??
[];
const
totalPages
=
Number
(
postsQuery
.
data
?.
totalPages
??
1
);
const
currentPage
=
Number
(
postsQuery
.
data
?.
page
??
page
);
return
(
return
(
<
div
className=
"min-h-screen container mx-auto p-4"
>
<
div
className=
"min-h-screen bg-[#fbfbfa]"
>
<
div
className=
"w-full flex flex-col gap-5"
>
<
div
className=
"container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10"
>
<
div
className=
"border-t border-gray-200 bg-white p-2.5"
>
<
div
className=
"mb-8"
>
<
div
className=
"w-full px-4 sm:px-6 lg:px-8"
>
<
h1
className=
"text-3xl font-bold leading-tight text-[#111827] md:text-4xl"
>
<
div
className=
"py-3"
>
Tìm kiếm
<
h1
className=
"text-md md:text-lg font-semibold leading-6 text-gray-900"
>
</
h1
>
Search Results for:
{
query
}
<
div
className=
"mt-2 h-[3px] w-16 rounded-full bg-[#f5a400]"
/>
</
h1
>
{
query
?
(
</
div
>
<
p
className=
"mt-4 text-sm text-[#5f6875]"
>
</
div
>
Kết quả tìm kiếm cho:
<
span
className=
"font-semibold text-[#111827]"
>
{
query
}
</
span
>
</
p
>
)
:
null
}
</
div
>
</
div
>
<
div
className=
"grid grid-cols-1 lg:grid-cols-3 gap-6"
>
<
div
className=
"flex flex-col gap-10 xl:flex-row xl:gap-14"
>
<
main
className=
"lg:col-span-2 bg-background "
>
<
main
className=
"order-2 min-w-0 xl:order-1 xl:flex-1"
>
<
div
className=
"pb-5 overflow-hidden"
>
{
postsQuery
.
isLoading
?
(
{
isLoading
?
(
<
div
className=
"flex items-center justify-center py-16"
>
<
div
className=
"flex justify-center items-center py-12"
>
<
Spinner
className=
"size-8"
/>
<
Spinner
className=
"size-8"
/>
<
span
className=
"ml-2 text-gray-600"
>
Đang tìm kiếm...
</
span
>
<
span
className=
"ml-2 text-gray-600"
>
Đang tìm kiếm...
</
span
>
</
div
>
</
div
>
)
:
(
)
:
(
<
div
className=
"space-y-9"
>
<>
{
rows
.
length
?
(
{
allData
?.
responseData
.
rows
.
map
((
news
)
=>
(
rows
.
map
((
item
,
index
)
=>
(
<
CardNews
<
SearchResultItem
key=
{
item
.
id
}
item=
{
item
}
index=
{
index
}
/>
key=
{
news
.
id
}
))
news=
{
news
}
)
:
(
link=
{
news
.
external_link
}
<
div
className=
"rounded-2xl border border-[#edf1f5] bg-white px-6 py-12 text-center text-gray-600"
>
/>
Không tìm thấy bài viết phù hợp.
))
}
<
div
className=
"w-full flex justify-center mt-4"
>
<
Pagination
pageCount=
{
Number
(
allData
?.
responseData
.
totalPages
??
1
)
}
page=
{
Number
(
allData
?.
responseData
.
currentPage
??
page
)
}
onChangePage=
{
(
p
)
=>
setPage
(
p
)
}
onGoToPreviousPage=
{
()
=>
setPage
(
Math
.
max
(
1
,
page
-
1
))
}
onGoToNextPage=
{
()
=>
setPage
(
Math
.
min
(
Number
(
allData
?.
responseData
.
totalPages
??
1
),
page
+
1
)
)
}
/>
</
div
>
</
div
>
</>
)
}
)
}
</
div
>
<
div
className=
"flex w-full justify-center pt-2"
>
<
Pagination
pageCount=
{
totalPages
}
page=
{
currentPage
}
onChangePage=
{
(
nextPage
)
=>
{
setPage
(
nextPage
);
updateSearchUrl
(
query
,
nextPage
);
}
}
onGoToPreviousPage=
{
()
=>
{
const
nextPage
=
Math
.
max
(
1
,
currentPage
-
1
);
setPage
(
nextPage
);
updateSearchUrl
(
query
,
nextPage
);
}
}
onGoToNextPage=
{
()
=>
{
const
nextPage
=
Math
.
min
(
totalPages
,
currentPage
+
1
);
setPage
(
nextPage
);
updateSearchUrl
(
query
,
nextPage
);
}
}
/>
</
div
>
</
div
>
)
}
</
main
>
</
main
>
<
aside
className=
"space-y-6 order-first lg:order-last"
>
<
aside
className=
"order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0"
>
<
div
className=
"bg-white border rounded-md overflow-hidden hidden lg:block"
>
<
form
<
div
className=
"w-full relative bg-gray-100"
>
className=
"rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
<
img
onSubmit=
{
(
event
)
=>
{
event
.
preventDefault
();
setPage
(
1
);
updateSearchUrl
(
searchInput
,
1
);
}
}
>
<
h2
className=
"text-lg font-bold text-[#111827]"
>
Tìm kiếm
</
h2
>
<
Input
value=
{
searchInput
}
onChange=
{
(
event
)
=>
setSearchInput
(
event
.
target
.
value
)
}
placeholder=
"Tên bài viết ..."
className=
"mt-4 h-11 rounded-xl border-[#edf1f5] bg-[#f8fafc] text-sm placeholder:text-gray-700"
/>
<
div
className=
"mt-4 grid grid-cols-2 gap-3"
>
<
Button
type=
"submit"
className=
"h-11 rounded-xl bg-[#14519f] text-white hover:bg-[#0f4386]"
>
Tìm kiếm
</
Button
>
<
Button
type=
"button"
variant=
"outline"
className=
"h-11 rounded-xl border-[#edf1f5] bg-white text-[#4b5563]"
onClick=
{
()
=>
{
setSearchInput
(
""
);
setPage
(
1
);
updateSearchUrl
(
""
,
1
);
}
}
>
Bỏ tìm
</
Button
>
</
div
>
</
form
>
<
div
className=
"overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]"
>
<
div
className=
"relative min-h-[390px] bg-[#1f334f]"
>
<
ImageNext
src=
"/banner.webp"
src=
"/banner.webp"
alt=
"Quảng cáo"
alt=
"Đối tác quảng bá"
className=
"object-cover"
width=
{
640
}
height=
{
760
}
className=
"absolute inset-0 h-full w-full object-cover"
/>
/>
<
div
className=
"absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent"
/>
<
div
className=
"absolute bottom-8 left-7 right-7 text-white"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-white/70"
>
Đối tác quảng bá
</
div
>
<
div
className=
"mt-3 text-2xl font-bold leading-tight"
>
Business Combo cho doanh nghiệp hội viên
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
aside
>
</
aside
>
...
@@ -110,14 +264,13 @@ function SearchContent() {
...
@@ -110,14 +264,13 @@ function SearchContent() {
export
default
function
Page
()
{
export
default
function
Page
()
{
return
(
return
(
<
Suspense
fallback=
{
<
Suspense
<
div
className=
"min-h-screen container mx-auto p-4 flex items-center justify-center"
>
fallback=
{
<
div
className=
"text-center"
>
<
div
className=
"flex min-h-screen items-center justify-center bg-[#fbfbfa]"
>
<
div
className=
"animate-spin rounded-full h-12 w-12 border-b-2 border-[#063e8e] mx-auto"
></
div
>
<
Spinner
className=
"size-8"
/>
<
p
className=
"mt-4 text-gray-600"
>
Loading search results...
</
p
>
</
div
>
</
div
>
</
div
>
}
}
>
>
<
SearchContent
/>
<
SearchContent
/>
</
Suspense
>
</
Suspense
>
);
);
...
...
src/app/admin/base-config/page.tsx
View file @
80e61988
...
@@ -556,7 +556,8 @@ export default function AdminBaseConfigPage() {
...
@@ -556,7 +556,8 @@ export default function AdminBaseConfigPage() {
return (
return (
<
div
className=
"space-y-8"
>
<
div
className=
"space-y-8"
>
<
Tabs
value=
{
activeTab
}
onValueChange=
{
setActiveTab
}
className=
"space-y-5"
>
<
Tabs
value=
{
activeTab
}
onValueChange=
{
setActiveTab
}
className=
"space-y-5"
>
<
TabsList
className=
"h-auto rounded-2xl bg-[#eaf2ff] p-1.5"
>
<
div
className=
"overflow-x-auto pb-1"
>
<
TabsList
className=
"h-auto min-w-max rounded-2xl bg-[#eaf2ff] p-1.5"
>
<
TabsTrigger
<
TabsTrigger
value=
"branding"
value=
"branding"
className=
"rounded-xl px-4 py-2.5 text-sm font-semibold text-slate-600 hover:text-[#063e8e] data-[state=active]:bg-white data-[state=active]:text-[#063e8e]"
className=
"rounded-xl px-4 py-2.5 text-sm font-semibold text-slate-600 hover:text-[#063e8e] data-[state=active]:bg-white data-[state=active]:text-[#063e8e]"
...
@@ -581,7 +582,8 @@ export default function AdminBaseConfigPage() {
...
@@ -581,7 +582,8 @@ export default function AdminBaseConfigPage() {
>
>
Mạng xã hội
Mạng xã hội
</
TabsTrigger
>
</
TabsTrigger
>
</
TabsList
>
</
TabsList
>
</
div
>
<
TabsContent
value=
"branding"
className=
"mt-0"
>
<
TabsContent
value=
"branding"
className=
"mt-0"
>
<
Card
className=
"rounded-[30px] border-[#063e8e]/10 shadow-sm"
>
<
Card
className=
"rounded-[30px] border-[#063e8e]/10 shadow-sm"
>
...
@@ -636,9 +638,9 @@ export default function AdminBaseConfigPage() {
...
@@ -636,9 +638,9 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
</
CardHeader
>
</
CardHeader
>
<
CardContent
className=
"space-y-6"
>
<
CardContent
className=
"space-y-6
px-4 sm:px-6
"
>
<
div
className=
"grid gap-6 lg:grid-cols-[minmax(0,1.3fr)_360px]"
>
<
div
className=
"grid gap-6 lg:grid-cols-[minmax(0,1.3fr)_360px]"
>
<
div
className=
"rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#f8fbff] to-white p-5"
>
<
div
className=
"rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#f8fbff] to-white p-
4 sm:p-
5"
>
<
div
className=
"relative flex min-h-[320px] items-center justify-center overflow-hidden rounded-[24px] border border-dashed border-[#063e8e]/18 bg-[#eef4ff]"
>
<
div
className=
"relative flex min-h-[320px] items-center justify-center overflow-hidden rounded-[24px] border border-dashed border-[#063e8e]/18 bg-[#eef4ff]"
>
{
currentLogoMedia
?
(
{
currentLogoMedia
?
(
<
div
className=
"relative h-[220px] w-[220px]"
>
<
div
className=
"relative h-[220px] w-[220px]"
>
...
@@ -658,7 +660,7 @@ export default function AdminBaseConfigPage() {
...
@@ -658,7 +660,7 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5"
>
<
div
className=
"space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-
4 sm:p-
5"
>
{
currentLogo
?
(
{
currentLogo
?
(
<
div
className=
"space-y-4 rounded-3xl border border-[#063e8e]/12 bg-white p-5 text-sm text-slate-600 shadow-sm"
>
<
div
className=
"space-y-4 rounded-3xl border border-[#063e8e]/12 bg-white p-5 text-sm text-slate-600 shadow-sm"
>
<
div
>
<
div
>
...
@@ -765,8 +767,8 @@ export default function AdminBaseConfigPage() {
...
@@ -765,8 +767,8 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
</
CardHeader
>
</
CardHeader
>
<
CardContent
className=
"space-y-6"
>
<
CardContent
className=
"space-y-6
px-4 sm:px-6
"
>
<
div
className=
"rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5"
>
<
div
className=
"rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-
4 sm:p-
5"
>
<
div
className=
"relative aspect-[16/6] overflow-hidden rounded-[24px] border border-[#063e8e]/12 bg-[#eef4ff]"
>
<
div
className=
"relative aspect-[16/6] overflow-hidden rounded-[24px] border border-[#063e8e]/12 bg-[#eef4ff]"
>
{
currentBannerMedia
?
(
{
currentBannerMedia
?
(
<
SafeNextImage
<
SafeNextImage
...
@@ -819,7 +821,7 @@ export default function AdminBaseConfigPage() {
...
@@ -819,7 +821,7 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"grid gap-4
md
:grid-cols-2 xl:grid-cols-3"
>
<
div
className=
"grid gap-4
sm
:grid-cols-2 xl:grid-cols-3"
>
{
sortedBanners
.
map
((
item
,
index
)
=>
(
{
sortedBanners
.
map
((
item
,
index
)
=>
(
<
ConfigItemPreview
<
ConfigItemPreview
key=
{
item
.
id
}
key=
{
item
.
id
}
...
@@ -833,7 +835,7 @@ export default function AdminBaseConfigPage() {
...
@@ -833,7 +835,7 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
{
currentBanner
?
(
{
currentBanner
?
(
<
div
className=
"grid gap-4
md
:grid-cols-4"
>
<
div
className=
"grid gap-4
sm:grid-cols-2 xl
:grid-cols-4"
>
<
div
className=
"rounded-2xl border border-[#063e8e]/10 bg-white px-4 py-4"
>
<
div
className=
"rounded-2xl border border-[#063e8e]/10 bg-white px-4 py-4"
>
<
div
className=
"text-xs uppercase tracking-[0.14em] text-gray-500"
>
Tên banner
</
div
>
<
div
className=
"text-xs uppercase tracking-[0.14em] text-gray-500"
>
Tên banner
</
div
>
<
div
className=
"mt-2 font-semibold text-[#163b73]"
>
{
currentBanner
.
name
}
</
div
>
<
div
className=
"mt-2 font-semibold text-[#163b73]"
>
{
currentBanner
.
name
}
</
div
>
...
@@ -899,8 +901,8 @@ export default function AdminBaseConfigPage() {
...
@@ -899,8 +901,8 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
</
CardHeader
>
</
CardHeader
>
<
CardContent
className=
"grid gap-6 lg:grid-cols-[360px_minmax(0,1fr)]"
>
<
CardContent
className=
"grid gap-6
px-4 sm:px-6
lg:grid-cols-[360px_minmax(0,1fr)]"
>
<
div
className=
"space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5"
>
<
div
className=
"space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-
4 sm:p-
5"
>
<
div
className=
"text-sm font-semibold uppercase tracking-[0.15em] text-[#4b74b8]"
>
<
div
className=
"text-sm font-semibold uppercase tracking-[0.15em] text-[#4b74b8]"
>
Danh sách chi nhánh
Danh sách chi nhánh
</
div
>
</
div
>
...
@@ -917,7 +919,7 @@ export default function AdminBaseConfigPage() {
...
@@ -917,7 +919,7 @@ export default function AdminBaseConfigPage() {
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"space-y-5 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5"
>
<
div
className=
"space-y-5 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-
4 sm:p-
5"
>
{
currentBranch
?
(
{
currentBranch
?
(
<>
<>
<
div
className=
"space-y-2"
>
<
div
className=
"space-y-2"
>
...
@@ -983,7 +985,7 @@ export default function AdminBaseConfigPage() {
...
@@ -983,7 +985,7 @@ export default function AdminBaseConfigPage() {
)
}
)
}
</
div
>
</
div
>
<
div
className=
"hidden rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#063e8e] to-[#0f4a9f] p-6 text-white shadow-[0_16px_30px_rgba(6,62,142,0.18)]"
>
<
div
className=
"hidden rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#063e8e] to-[#0f4a9f] p-6 text-white shadow-[0_16px_30px_rgba(6,62,142,0.18)]
lg:block
"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.18em] text-white/75"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.18em] text-white/75"
>
Preview chi nhánh
Preview chi nhánh
</
div
>
</
div
>
...
@@ -1034,16 +1036,16 @@ export default function AdminBaseConfigPage() {
...
@@ -1034,16 +1036,16 @@ export default function AdminBaseConfigPage() {
className=
"rounded-xl bg-[#163b73] text-white hover:bg-[#163b73]/90"
className=
"rounded-xl bg-[#163b73] text-white hover:bg-[#163b73]/90"
>
>
<
Save
className=
"mr-2 h-4 w-4"
/>
<
Save
className=
"mr-2 h-4 w-4"
/>
L
u
u cấu hình
L
ư
u cấu hình
</
Button
>
</
Button
>
</
div
>
</
div
>
</
CardHeader
>
</
CardHeader
>
<
CardContent
className=
"space-y-4"
>
<
CardContent
className=
"space-y-4
px-4 sm:px-6
"
>
{
sortedSocials
.
map
((
item
)
=>
(
{
sortedSocials
.
map
((
item
)
=>
(
<
div
<
div
key=
{
item
.
id
}
key=
{
item
.
id
}
className=
"rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5"
className=
"rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-
4 sm:p-
5"
>
>
<
div
className=
"grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)_180px] lg:items-end"
>
<
div
className=
"grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)_180px] lg:items-end"
>
<
div
className=
"flex items-center gap-3 rounded-2xl border border-[#063e8e]/10 bg-white px-4 py-4"
>
<
div
className=
"flex items-center gap-3 rounded-2xl border border-[#063e8e]/10 bg-white px-4 py-4"
>
...
...
src/app/admin/layout.tsx
View file @
80e61988
...
@@ -12,15 +12,35 @@ import { useSidebarStore } from '@/hooks/use-admin-sidebar';
...
@@ -12,15 +12,35 @@ import { useSidebarStore } from '@/hooks/use-admin-sidebar';
import
{
cn
}
from
'@/lib/utils'
;
import
{
cn
}
from
'@/lib/utils'
;
function
AdminShell
({
children
}:
{
children
:
React
.
ReactNode
})
{
function
AdminShell
({
children
}:
{
children
:
React
.
ReactNode
})
{
const
{
isOpen
}
=
useSidebarStore
();
const
{
close
,
isOpen
}
=
useSidebarStore
();
React
.
useEffect
(()
=>
{
const
mediaQuery
=
window
.
matchMedia
(
'(max-width: 1023px)'
);
const
syncSidebar
=
()
=>
{
if
(
mediaQuery
.
matches
)
close
();
};
syncSidebar
();
mediaQuery
.
addEventListener
(
'change'
,
syncSidebar
);
return
()
=>
mediaQuery
.
removeEventListener
(
'change'
,
syncSidebar
);
},
[
close
]);
return
(
return
(
<
div
className=
"min-h-screen bg-white"
>
<
div
className=
"min-h-screen
overflow-x-hidden
bg-white"
>
<
AdminSidebar
/>
<
AdminSidebar
/>
{
isOpen
?
(
<
button
type=
"button"
aria
-
label=
"Close sidebar"
className=
"fixed inset-0 z-30 bg-slate-950/35 backdrop-blur-[1px] lg:hidden"
onClick=
{
close
}
/>
)
:
null
}
<
div
<
div
className=
{
cn
(
className=
{
cn
(
'transition-all duration-300'
,
'
min-w-0
transition-all duration-300'
,
isOpen
?
'
pl-72'
:
'
pl-24'
,
isOpen
?
'
lg:pl-72'
:
'lg:
pl-24'
,
)
}
)
}
>
>
<
AdminHeader
/>
<
AdminHeader
/>
...
...
src/app/styles.css
View file @
80e61988
...
@@ -52,6 +52,19 @@
...
@@ -52,6 +52,19 @@
--tiptap-rose
:
oklch
(
0.72
0.17
13
);
--tiptap-rose
:
oklch
(
0.72
0.17
13
);
}
}
html
,
body
{
max-width
:
100%
;
overflow-x
:
hidden
;
}
img
,
video
,
canvas
,
svg
{
max-width
:
100%
;
}
@layer
components
{
@layer
components
{
.tiptap
{
.tiptap
{
@apply
border-gray-200;
@apply
border-gray-200;
...
...
src/components/admin/admin-stats-grid.tsx
View file @
80e61988
...
@@ -17,7 +17,7 @@ export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
...
@@ -17,7 +17,7 @@ export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
const
gridClassName
=
const
gridClassName
=
className
??
className
??
(
items
.
length
===
3
(
items
.
length
===
3
?
"grid grid-cols-1 gap-4
md
:grid-cols-3"
?
"grid grid-cols-1 gap-4
sm:grid-cols-2 xl
:grid-cols-3"
:
"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4"
);
:
"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4"
);
return
(
return
(
...
@@ -28,9 +28,9 @@ export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
...
@@ -28,9 +28,9 @@ export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
className=
"rounded-2xl border border-[#063e8e]/15 bg-white px-5 py-4 shadow-sm"
className=
"rounded-2xl border border-[#063e8e]/15 bg-white px-5 py-4 shadow-sm"
>
>
<
div
className=
"flex items-start justify-between gap-4"
>
<
div
className=
"flex items-start justify-between gap-4"
>
<
div
className=
"space-y-2"
>
<
div
className=
"
min-w-0
space-y-2"
>
<
p
className=
"text-sm font-medium text-gray-700"
>
{
item
.
label
}
</
p
>
<
p
className=
"text-sm font-medium text-gray-700"
>
{
item
.
label
}
</
p
>
<
div
className=
"
text-3xl font-semibold leading-none text-black
"
>
{
item
.
value
}
</
div
>
<
div
className=
"
break-words text-2xl font-semibold leading-none text-black sm:text-3xl
"
>
{
item
.
value
}
</
div
>
</
div
>
</
div
>
{
item
.
icon
?
(
{
item
.
icon
?
(
<
div
className=
"flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#063e8e]/10"
>
<
div
className=
"flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#063e8e]/10"
>
...
...
src/components/admin/admin-table-layout.tsx
View file @
80e61988
...
@@ -33,25 +33,25 @@ export function AdminTableLayout({
...
@@ -33,25 +33,25 @@ export function AdminTableLayout({
return
(
return
(
<
div
className=
"space-y-4"
>
<
div
className=
"space-y-4"
>
<
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<
div
className=
"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<
div
className=
"flex flex-1 flex-col gap-3 lg:flex-row lg:items-center"
>
<
div
className=
"flex
min-w-0
flex-1 flex-col gap-3 lg:flex-row lg:items-center"
>
<
Input
<
Input
value=
{
searchValue
}
value=
{
searchValue
}
placeholder=
{
searchPlaceholder
}
placeholder=
{
searchPlaceholder
}
onChange=
{
(
event
)
=>
onSearchChange
(
event
.
target
.
value
)
}
onChange=
{
(
event
)
=>
onSearchChange
(
event
.
target
.
value
)
}
className=
"
max-w-sm rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700
"
className=
"
w-full rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700 sm:max-w-sm
"
/>
/>
{
filters
}
{
filters
}
</
div
>
</
div
>
{
actionLabel
||
actionMeta
?
(
{
actionLabel
||
actionMeta
?
(
<
div
className=
"flex
items-center gap-3 self-start
sm:self-auto"
>
<
div
className=
"flex
w-full flex-wrap items-center gap-3 self-start sm:w-auto
sm:self-auto"
>
{
actionMeta
}
{
actionMeta
}
{
actionLabel
?
(
{
actionLabel
?
(
<
Button
<
Button
type=
"button"
type=
"button"
disabled=
{
actionDisabled
}
disabled=
{
actionDisabled
}
onClick=
{
onActionClick
}
onClick=
{
onActionClick
}
className=
"
bg-[#063e8e] text-white hover:bg-[#063e8e]/90
"
className=
"
w-full bg-[#063e8e] text-white hover:bg-[#063e8e]/90 sm:w-auto
"
>
>
{
actionIcon
??
<
Plus
className=
"mr-2 h-4 w-4"
/>
}
{
actionIcon
??
<
Plus
className=
"mr-2 h-4 w-4"
/>
}
{
actionLabel
}
{
actionLabel
}
...
@@ -61,9 +61,9 @@ export function AdminTableLayout({
...
@@ -61,9 +61,9 @@ export function AdminTableLayout({
)
:
null
}
)
:
null
}
</
div
>
</
div
>
<
div
className=
"overflow-
hidden rounded-xl border border-[#063e8e]/20 bg-white shadow-sm
[&_tbody_td:not(:last-child)]:border-r [&_tbody_td:not(:last-child)]:border-[#063e8e]/20 [&_thead_th:not(:last-child)]:border-r [&_thead_th:not(:last-child)]:border-white/15"
>
<
div
className=
"overflow-
x-auto rounded-xl border border-[#063e8e]/20 bg-white shadow-sm [&_table]:min-w-[760px]
[&_tbody_td:not(:last-child)]:border-r [&_tbody_td:not(:last-child)]:border-[#063e8e]/20 [&_thead_th:not(:last-child)]:border-r [&_thead_th:not(:last-child)]:border-white/15"
>
{
children
}
{
children
}
</
div
>
</
div
>
</
div
>
</
div
>
);
);
}
}
\ No newline at end of file
src/components/shared/admin-header.tsx
View file @
80e61988
...
@@ -67,8 +67,8 @@ export function AdminHeader() {
...
@@ -67,8 +67,8 @@ export function AdminHeader() {
return
(
return
(
<
header
className=
"sticky top-0 z-30 border-b border-[#063e8e]/15 bg-background/95 shadow-sm backdrop-blur supports-backdrop-filter:bg-background/80"
>
<
header
className=
"sticky top-0 z-30 border-b border-[#063e8e]/15 bg-background/95 shadow-sm backdrop-blur supports-backdrop-filter:bg-background/80"
>
<
div
className=
"flex
h-16 items-center justify-between
px-4 lg:px-6"
>
<
div
className=
"flex
min-h-16 items-center justify-between gap-3 px-3 py-2 sm:
px-4 lg:px-6"
>
<
div
className=
"flex
items-center
gap-4"
>
<
div
className=
"flex
min-w-0 items-center gap-2 sm:
gap-4"
>
<
Button
<
Button
variant=
"ghost"
variant=
"ghost"
size=
"icon"
size=
"icon"
...
@@ -78,11 +78,11 @@ export function AdminHeader() {
...
@@ -78,11 +78,11 @@ export function AdminHeader() {
>
>
<
Menu
className=
"h-5 w-5"
/>
<
Menu
className=
"h-5 w-5"
/>
</
Button
>
</
Button
>
<
h1
className=
"t
ext-xl font-bold text-[#063e8e]
"
>
{
title
}
</
h1
>
<
h1
className=
"t
runcate text-base font-bold text-[#063e8e] sm:text-xl
"
>
{
title
}
</
h1
>
</
div
>
</
div
>
<
div
className=
"flex
items-center
gap-3"
>
<
div
className=
"flex
shrink-0 items-center gap-2 sm:
gap-3"
>
<
div
className=
"
flex items-center gap-2 rounded-full border border-[#063e8e]/10 bg-[#f8fbff] px-3 py-1.5 text-sm font-medium text-[#163b73]
"
>
<
div
className=
"
hidden items-center gap-2 rounded-full border border-[#063e8e]/10 bg-[#f8fbff] px-3 py-1.5 text-sm font-medium text-[#163b73] sm:flex
"
>
<
ShieldCheck
className=
"h-4 w-4 text-[#063e8e]"
/>
<
ShieldCheck
className=
"h-4 w-4 text-[#063e8e]"
/>
<
span
>
{
formatRoles
(
currentUser
?.
roles
)
}
</
span
>
<
span
>
{
formatRoles
(
currentUser
?.
roles
)
}
</
span
>
</
div
>
</
div
>
...
@@ -93,7 +93,7 @@ export function AdminHeader() {
...
@@ -93,7 +93,7 @@ export function AdminHeader() {
className=
"border-[#063e8e]/15 text-[#063e8e]"
className=
"border-[#063e8e]/15 text-[#063e8e]"
>
>
<
LogOut
className=
"h-4 w-4"
/>
<
LogOut
className=
"h-4 w-4"
/>
Đăng xuất
<
span
className=
"hidden sm:inline"
>
Đăng xuất
</
span
>
</
Button
>
</
Button
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/components/shared/admin-sidebar.tsx
View file @
80e61988
...
@@ -70,7 +70,7 @@ const membersReservedSegments = new Set(['fields', 'regions']);
...
@@ -70,7 +70,7 @@ const membersReservedSegments = new Set(['fields', 'regions']);
export
function
AdminSidebar
()
{
export
function
AdminSidebar
()
{
const
pathname
=
usePathname
();
const
pathname
=
usePathname
();
const
{
isOpen
}
=
useSidebarStore
();
const
{
close
,
isOpen
}
=
useSidebarStore
();
const
[
expandedGroups
,
setExpandedGroups
]
=
React
.
useState
<
Record
<
string
,
boolean
>>
({});
const
[
expandedGroups
,
setExpandedGroups
]
=
React
.
useState
<
Record
<
string
,
boolean
>>
({});
const
isItemActive
=
React
.
useCallback
(
const
isItemActive
=
React
.
useCallback
(
...
@@ -93,17 +93,22 @@ export function AdminSidebar() {
...
@@ -93,17 +93,22 @@ export function AdminSidebar() {
const
toggleGroup
=
(
name
:
string
)
=>
const
toggleGroup
=
(
name
:
string
)
=>
setExpandedGroups
((
previous
)
=>
({
...
previous
,
[
name
]:
!
previous
[
name
]
}));
setExpandedGroups
((
previous
)
=>
({
...
previous
,
[
name
]:
!
previous
[
name
]
}));
const
handleMobileNavigate
=
()
=>
{
if
(
window
.
innerWidth
<
1024
)
close
();
};
return
(
return
(
<
aside
<
aside
className=
{
cn
(
className=
{
cn
(
'fixed left-0 top-0 z-40 h-
screen
border-r border-[#063e8e]/10 bg-gradient-to-b from-[#f6f9ff] via-[#edf4ff] to-[#f8fbff] shadow-[0_18px_45px_rgba(6,62,142,0.08)] transition-all duration-300'
,
'fixed left-0 top-0 z-40 h-
dvh
border-r border-[#063e8e]/10 bg-gradient-to-b from-[#f6f9ff] via-[#edf4ff] to-[#f8fbff] shadow-[0_18px_45px_rgba(6,62,142,0.08)] transition-all duration-300'
,
isOpen
?
'w-72
'
:
'w-24
'
,
isOpen
?
'w-72
translate-x-0 lg:w-72'
:
'-translate-x-full lg:w-24 lg:translate-x-0
'
,
)
}
)
}
>
>
<
div
className=
"flex h-full flex-col"
>
<
div
className=
"flex h-full flex-col"
>
<
div
className=
{
cn
(
'px-4 pb-4 pt-5'
,
!
isOpen
&&
'px-3'
)
}
>
<
div
className=
{
cn
(
'px-4 pb-4 pt-5'
,
!
isOpen
&&
'px-3'
)
}
>
<
Link
<
Link
href=
"/admin/base-config"
href=
"/admin/base-config"
onClick=
{
handleMobileNavigate
}
className=
{
cn
(
className=
{
cn
(
'flex items-center backdrop-blur-sm'
,
'flex items-center backdrop-blur-sm'
,
isOpen
isOpen
...
@@ -189,6 +194,7 @@ export function AdminSidebar() {
...
@@ -189,6 +194,7 @@ export function AdminSidebar() {
<
Link
<
Link
key=
{
child
.
name
}
key=
{
child
.
name
}
href=
{
child
.
href
}
href=
{
child
.
href
}
onClick=
{
handleMobileNavigate
}
className=
{
cn
(
className=
{
cn
(
'group relative flex rounded-2xl px-4 py-3 text-sm leading-6 transition-all'
,
'group relative flex rounded-2xl px-4 py-3 text-sm leading-6 transition-all'
,
childActive
childActive
...
@@ -212,6 +218,7 @@ export function AdminSidebar() {
...
@@ -212,6 +218,7 @@ export function AdminSidebar() {
<
Link
<
Link
key=
{
item
.
name
}
key=
{
item
.
name
}
href=
{
item
.
href
||
'#'
}
href=
{
item
.
href
||
'#'
}
onClick=
{
handleMobileNavigate
}
title=
{
!
isOpen
?
item
.
name
:
undefined
}
title=
{
!
isOpen
?
item
.
name
:
undefined
}
className=
{
cn
(
className=
{
cn
(
'flex items-center rounded-2xl text-sm font-medium transition-all duration-200'
,
'flex items-center rounded-2xl text-sm font-medium transition-all duration-200'
,
...
@@ -233,6 +240,7 @@ export function AdminSidebar() {
...
@@ -233,6 +240,7 @@ export function AdminSidebar() {
<
div
className=
"rounded-[28px] border border-white/80 bg-white/95 p-4 shadow-[0_14px_32px_rgba(6,62,142,0.08)]"
>
<
div
className=
"rounded-[28px] border border-white/80 bg-white/95 p-4 shadow-[0_14px_32px_rgba(6,62,142,0.08)]"
>
<
Link
<
Link
href=
"/"
href=
"/"
onClick=
{
handleMobileNavigate
}
className=
"flex items-center gap-3 text-sm font-semibold text-[#063e8e] transition hover:opacity-80"
className=
"flex items-center gap-3 text-sm font-semibold text-[#063e8e] transition hover:opacity-80"
>
>
<
div
className=
"flex h-10 w-10 items-center justify-center rounded-2xl bg-[#edf4ff] text-[#063e8e]"
>
<
div
className=
"flex h-10 w-10 items-center justify-center rounded-2xl bg-[#edf4ff] text-[#063e8e]"
>
...
@@ -250,6 +258,7 @@ export function AdminSidebar() {
...
@@ -250,6 +258,7 @@ export function AdminSidebar() {
)
:
(
)
:
(
<
Link
<
Link
href=
"/"
href=
"/"
onClick=
{
handleMobileNavigate
}
title=
"Về trang chủ"
title=
"Về trang chủ"
className=
"mx-auto flex h-14 w-14 items-center justify-center rounded-[22px] border border-white/80 bg-white/95 text-[#063e8e] shadow-sm transition hover:bg-white"
className=
"mx-auto flex h-14 w-14 items-center justify-center rounded-[22px] border border-white/80 bg-white/95 text-[#063e8e] shadow-sm transition hover:bg-white"
>
>
...
...
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