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
f2c48aff
Commit
f2c48aff
authored
Oct 31, 2025
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update/home_page
parent
a9a4088f
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
280 additions
and
40 deletions
+280
-40
layout.tsx
src/app/(main)/layout.tsx
+0
-0
page.tsx
src/app/(main)/page.tsx
+0
-0
page.tsx
src/app/page.tsx
+263
-40
index.ts
src/components/ui/index.ts
+1
-0
spinner.tsx
src/components/ui/spinner.tsx
+16
-0
No files found.
src/app/(main)/layout.tsx
0 → 100644
View file @
f2c48aff
src/app/(main)/page.tsx
0 → 100644
View file @
f2c48aff
src/app/page.tsx
View file @
f2c48aff
'use client'
import
Image
from
'next/image'
import
{
Button
,
Card
}
from
'@/components/ui'
import
{
Button
,
Card
,
Spinner
}
from
'@/components/ui'
import
{
useEffect
,
useRef
,
useState
}
from
'react'
import
{
Autoplay
}
from
'swiper/modules'
import
{
Swiper
,
SwiperSlide
}
from
'swiper/react'
import
{
Swiper
as
SwiperType
}
from
'swiper/types'
import
'swiper/css'
import
'swiper/css/navigation'
import
'swiper/css/pagination'
import
{
is
}
from
'zod/v4/locales'
export
default
function
Home
()
{
return
(
<
div
className=
"flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"
>
<
main
className=
"flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"
>
<
Image
className=
"dark:invert"
src=
"/next.svg"
alt=
"Next.js logo"
width=
{
100
}
height=
{
20
}
priority
/>
// states
const
[
tab
,
setTab
]
=
useState
(
'all'
)
const
[
search
,
setSearch
]
=
useState
(
''
)
const
[
submitSearch
,
setSubmitSearch
]
=
useState
(
''
)
const
[
currentIndex
,
setCurrentIndex
]
=
useState
(
0
)
// responsive slidesPerView used to compute pagination pages
const
[
slidesPerView
,
setSlidesPerView
]
=
useState
<
number
>
(
3
)
// Refs
const
swiperRef
=
useRef
<
SwiperType
|
null
>
(
null
)
// server
// const { data: categoryData } = useGetCategory<GetCategoryAdminResponseType>();
// const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
// pageSize: '999',
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
// })
//tab filter
// let data
// if (tab === 'all') {
// data = allData
// } else {
// // fillter by category
// const filteredRows = allData?.responseData?.rows?.filter(
// (news) => news.category === tab
// )
// data = {
// ...allData,
// responseData: {
// ...allData?.responseData,
// rows: filteredRows ?? []
// }
// }
// }
// update slidesPerView on resize to match the Swiper breakpoints
useEffect
(()
=>
{
const
getSlides
=
(
w
:
number
)
=>
{
if
(
w
>=
1024
)
return
3
if
(
w
>=
640
)
return
2
return
1
}
<
div
className=
"flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"
>
<
h1
className=
"max-w-xs text-3xl font-semibold leading-10 tracking-tight text-primary dark:text-zinc-50"
>
To get started, edit the page.tsx file.
const
update
=
()
=>
setSlidesPerView
(
getSlides
(
window
.
innerWidth
))
// run once on mount
update
()
window
.
addEventListener
(
'resize'
,
update
)
return
()
=>
window
.
removeEventListener
(
'resize'
,
update
)
},
[])
// demo
const
isLoading
=
false
//template
return
(
isLoading
?
(
<
div
className=
'w-full h-[80vh] flex justify-center items-center'
>
<
Spinner
/>
</
div
>
)
:
(
<>
{
/* Banner */
}
<
img
src=
"https://vcci-hcm.org.vn/wp-content/uploads/2025/10/1.1.-Hero-Banner-CEO-2025-Bi-Sai-Nam-2025-Nhe-2560x720-Px.jpg"
alt=
"Banner"
className=
'w-full'
/>
<
div
className=
'app-container'
>
{
/* Featured News */
}
<
div
className=
'pt-10'
>
<
div
className=
'flex justify-center items-center w-full text-center'
>
<
hr
className=
'border-blue-900 w-full'
/>
<
h1
className=
'text-app-blue text-[28px] leading-normal uppercase font-bold w-full text-blue-900'
>
Tin Nổi Bật
</
h1
>
<
p
className=
"max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"
>
Looking for a starting point or more instructions? Head over to
{
' '
}
<
a
href=
"https://vercel.com/templates?framework=next.js"
className=
"font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</
a
>
{
' '
}
or the
{
' '
}
<
a
href=
"https://nextjs.org/learn?utm_source=create-next-app"
className=
"font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</
a
>
{
' '
}
center.
</
p
>
</
div
>
<
div
className=
"flex w-full gap-4 text-base font-medium sm:flex-row"
>
<
Card
className=
"flex-1 md:w-[158px]"
>
<
a
href=
"https://vercel.com/new?utm_source=create-next-app"
target=
"_blank"
rel=
"noopener noreferrer"
>
<
Button
className=
"w-full"
variant=
"secondary"
>
<
Image
className=
"dark:invert"
src=
"/vercel.svg"
alt=
"Vercel logomark"
width=
{
16
}
height=
{
16
}
/>
<
span
className=
"ml-2"
>
Deploy Now
</
span
>
</
Button
>
<
hr
className=
'border-blue-900 w-full'
/>
</
div
>
{
/* slider */
}
<
div
className=
'py-10'
>
<
Swiper
modules=
{
[
Autoplay
]
}
// navigation
// pagination={{ clickable: true }}
autoplay=
{
{
delay
:
4000
,
disableOnInteraction
:
false
}
}
loop
spaceBetween=
{
16
}
breakpoints=
{
{
0
:
{
slidesPerView
:
1
},
640
:
{
slidesPerView
:
2
},
1024
:
{
slidesPerView
:
3
}
}
}
onSwiper=
{
(
swiper
)
=>
{
swiperRef
.
current
=
swiper
}
}
onSlideChange=
{
(
swiper
)
=>
{
setCurrentIndex
(
typeof
swiper
.
realIndex
===
'number'
?
swiper
.
realIndex
:
swiper
.
activeIndex
)
}
}
>
{
/* {allData?.responseData.rows.map((news) => (
<SwiperSlide key={news.id}>
<a href={`/tin-tuc/${news.id}`} className='block bg-white shadow-md overflow-hidden relative'>
<AppImage
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className='w-full h-48 sm:h-56 md:h-64 object-cover'
/>
<div className='absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white px-4 font-semibold text-base rounded-b-xl flex items-center justify-center text-center h-16 md:h-20'>
<div
className='w-full overflow-hidden'
style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical'
}}
>
{news.title}
</div>
</div>
</a>
</
Card
>
</SwiperSlide>
))} */
}
<
Card
className=
"flex-1 md:w-[158px]"
>
<
a
href=
"https://nextjs.org/docs?utm_source=create-next-app"
target=
"_blank"
rel=
"noopener noreferrer"
>
<
Button
className=
"w-full"
variant=
"outline"
>
Documentation
</
Button
>
<
SwiperSlide
>
<
a
href=
{
`#`
}
className=
'block bg-white shadow-md overflow-hidden relative'
>
<
img
src=
{
`https://media.istockphoto.com/id/814423752/vi/anh/con-m%E1%BA%AFt-c%E1%BB%A7a-ng%C6%B0%E1%BB%9Di-m%E1%BA%ABu-v%E1%BB%9Bi-trang-%C4%91i%E1%BB%83m-ngh%E1%BB%87-thu%E1%BA%ADt-%C4%91%E1%BA%A7y-m%C3%A0u-s%E1%BA%AFc-c%E1%BA%ADn-c%E1%BA%A3nh.jpg?s=1024x1024&w=is&k=20&c=g9JektNW0igwf2u2mT9uqSLkhzR91ZYviuVLXuhy2JQ=`
}
alt=
{
'image'
}
className=
'w-full h-48 sm:h-56 md:h-64 object-cover'
/>
<
div
className=
'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white px-4 font-semibold text-base rounded-b-xl flex items-center justify-center text-center h-16 md:h-20'
>
<
div
className=
'w-full overflow-hidden'
style=
{
{
display
:
'-webkit-box'
,
WebkitLineClamp
:
2
,
WebkitBoxOrient
:
'vertical'
}
}
>
title
</
div
>
</
div
>
</
a
>
</
Card
>
</
SwiperSlide
>
</
Swiper
>
</
div
>
</
main
>
</
div
>
{
/* news and quick links section */
}
<
div
className=
'flex flex-row gap-5'
>
<
div
className=
'w-[67%]'
>
<
div
>
<
div
className=
'flex justify-between items-center'
>
<
a
href=
'#'
className=
'text-[20px] font-bold uppercase text-blue-900'
>
Tin Tức
</
a
>
<
a
href=
'#'
className=
'text-blue-900'
>
{
'>>'
}
</
a
>
</
div
>
<
hr
className=
' border-blue-900'
/>
</
div
>
<
div
className=
'flex flex-row justify-center gap-5 pt-5'
>
{
/* special news section */
}
<
div
className=
'bg-gray-500 w-[50%] flex items-center justify-center rounded-lg'
>
<
p
className=
'text-white'
>
khung tin tức vip
</
p
>
</
div
>
{
/* news list section */
}
<
div
className=
'w-[50%]'
>
{
/* category tabs */
}
<
div
className=
'flex gap-2 mb-5 flex-wrap'
>
<
button
className=
{
`px-4 py-1 rounded-md border ${'all' === tab ? 'border-blue-700 bg-blue-50' : 'border-gray-300 bg-white'}`
}
onClick=
{
()
=>
setTab
(
'all'
)
}
>
Tất cả
</
button
>
{
/* {categoryData?.responseData.rows.slice(0, 3).map((category) => (
<button
key={category.id}
className={`px-4 py-1 rounded-md border ${category.name === tab ? 'border-blue-700 bg-blue-50' : 'border-gray-300 bg-white'}`}
onClick={() => setTab(category.name)}
>
{category.name}
</button>
))} */
}
</
div
>
{
/* News list */
}
<
div
className=
'pb-5 overflow-hidden'
>
{
/* {data?.responseData.rows.slice(0, 5).map((news) => (
<NewsContent key={news.id} news={news} />
))} */
}
{
/* <div className='w-full flex justify-center mt-4'>
<AppPagination
page={Math.floor(currentIndex / Math.max(slidesPerView, 1)) + 1}
count={Math.ceil((data?.responseData.rows?.length ?? 0) / Math.max(slidesPerView, 1))}
onChange={(_event, value) => {
const toIndex = (value - 1) * Math.max(slidesPerView, 1)
swiperRef.current?.slideTo(toIndex)
}}
/>
</div> */
}
</
div
>
</
div
>
</
div
>
</
div
>
{
/* quick links section */
}
<
div
className=
'w-[33%]'
>
<
div
>
<
div
className=
'flex justify-between items-center'
>
<
a
href=
'#'
className=
'text-[20px] font-bold uppercase text-blue-900'
>
Liên kết nhanh
</
a
>
<
a
href=
'#'
className=
'text-blue-900'
>
{
'>>'
}
</
a
>
</
div
>
<
hr
className=
' border-blue-900'
/>
</
div
>
<
div
className=
'pt-5'
>
<
p
>
🔗 Cẩm nang Hướng dẫn đầu tư kinh doanh tại Việt Nam
</
p
>
<
p
>
🔗 Doanh nghiệp kiến nghị về chính sách và pháp luật
</
p
>
</
div
>
</
div
>
</
div
>
{
/* Sidebar */
}
{
/* <div className='lg:flex-1 w-full'>
<div className='bg-white rounded-lg p-4 mb-6 shadow-sm'>
<div className='font-semibold mb-2'>Tìm kiếm</div>
<input
type='text'
placeholder='Tên bài viết...'
value={search}
onChange={(e) => setSearch(e.target.value)}
className='w-full p-2 border border-gray-300 rounded mb-2 focus:outline-none focus:ring-1 focus:ring-blue-500'
/>
<div className='flex gap-2'>
<button
onClick={() => setSubmitSearch(search)}
className='flex-1 bg-[#0056b3] text-white rounded p-2 font-semibold hover:bg-[#004999] transition'
>
Tìm kiếm
</button>
<button
onClick={() => setSearch('')}
className='flex-1 bg-gray-100 text-gray-700 rounded p-2 font-semibold hover:bg-gray-200 transition'
>
Bỏ tìm
</button>
</div>
</div>
</div> */
}
</
div
>
</>
)
)
}
src/components/ui/index.ts
View file @
f2c48aff
...
...
@@ -5,3 +5,4 @@ export { Badge } from './badge'
export
{
Card
}
from
'./card'
export
{
Icons
}
from
'./icons'
export
{
cn
}
from
'./utils'
export
{
Spinner
}
from
'./spinner'
src/components/ui/spinner.tsx
0 → 100644
View file @
f2c48aff
import
{
LoaderIcon
}
from
"lucide-react"
import
{
cn
}
from
"@/lib/utils"
function
Spinner
({
className
,
...
props
}:
React
.
ComponentProps
<
"svg"
>
)
{
return
(
<
LoaderIcon
role=
"status"
aria
-
label=
"Loading"
className=
{
cn
(
"size-4 animate-spin"
,
className
)
}
{
...
props
}
/>
)
}
export
{
Spinner
}
\ No newline at end of file
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