Commit 29e06f04 authored by Văn Hoàng's avatar Văn Hoàng

[tag]0.1-vcci

parents 28465113 dbcd6907
Pipeline #43394 passed with stages
in 6 minutes and 20 seconds
......@@ -15,6 +15,7 @@ import { useParams } from 'next/navigation'
import ListCategory from '@/components/base/list-category'
import { MEDIA_INFORMATION_CATEGORIES } from '@/constants/categories'
import EventCalendar from '@/components/base/event-calendar'
import parse from "html-react-parser";
// import { t } from 'i18next'
// Component
......@@ -52,7 +53,7 @@ const NewsDetailPage = () => {
<hr />
</div>
<div className='flex-1 text-app-grey text-base overflow-hidden'>
<AppEditorContent value={data?.responseData?.description ?? ''} />
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</div>
</main>
{/* Sidebar */}
......
import { NewsAdminItem } from '@/api/types/news'
import BASE_URL from '@/links'
import dayjs from 'dayjs';
import AppEditorContent from '@/components/shared/editor-content';
import { NewsAdminItem } from "@/api/types/news";
import BASE_URL from "@/links";
import dayjs from "dayjs";
import AppEditorContent from "@/components/shared/editor-content";
function CardNews({ news }: { news: NewsAdminItem }) {
return (
<a
href={`${news.id}`}
className='flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3 p-2 sm:p-3 border border-gray-200 bg-white rounded-md'
className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3"
>
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
......@@ -18,13 +18,12 @@ function CardNews({ news }: { news: NewsAdminItem }) {
e.currentTarget.src = "/fallback.png"
}}
/>
<div className='flex-1'>
<p className='text-[#0056b3] font-bold text-sm line-clamp-2'>
<div className="flex-1">
<p className="text-[#363636] font-bold text-sm line-clamp-2">
{news.title}
</p>
<p className='text-gray-500 text-sm my-1'>
{dayjs(news.release_at).format('DD/MM/YYYY')}
<p className="text-gray-500 text-sm my-1">
{dayjs(news.release_at).format("DD/MM/YYYY")}
</p>
{/* <AppEditorContent className='line-clamp-2' value={news.description} /> */}
</div>
......@@ -32,4 +31,4 @@ function CardNews({ news }: { news: NewsAdminItem }) {
);
}
export default CardNews;
\ No newline at end of file
export default CardNews;
"use client";
import React, { useState } from "react";
import { ArrowRight, ArrowLeft } from "lucide-react";
export default function EventCalendar() {
const mockCalendar = {
month: 10,
year: 2025,
highlighted: [6, 9, 12],
};
const [month, setMonth] = useState<number>(mockCalendar.month);
const [year, setYear] = useState<number>(mockCalendar.year);
return (
<div className="bg-white border rounded-md p-4">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-medium">
THÁNG {month}/{year}
</div>
<div className="flex items-center gap-2">
<div className="group">
<button
aria-label="Tháng trước"
onClick={() => {
// prev month
if (month === 1) {
setMonth(12);
setYear((y) => y - 1);
} else {
setMonth((m) => m - 1);
}
}}
className="group-hover:border-primary p-1 h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636] "
>
<ArrowLeft
size={24}
className="group-hover:text-muted-foreground text-[#363636]"
/>
</button>
</div>
<div className="group">
<button
aria-label="Tháng sau"
onClick={() => {
// next month
if (month === 12) {
setMonth(1);
setYear((y) => y + 1);
} else {
setMonth((m) => m + 1);
}
}}
className="p-1 group-hover:border-primary h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636]"
>
<ArrowRight
size={24}
className="group-hover:text-muted-foreground"
/>
</button>
</div>
</div>
</div>
<div className="grid grid-cols-7 gap-1 text-center text-xs">
{["CN", "T2", "T3", "T4", "T5", "T6", "T7"].map((d) => (
<div key={d} className="text-gray-400 py-1">
{d}
</div>
))}
{
(() => {
const totalDays = new Date(year, month, 0).getDate()
const firstDayIndex = new Date(year, month - 1, 1).getDay() // 0 (Sun) - 6 (Sat)
// previous month total days
const prevMonthTotalDays = new Date(year, month - 1, 0).getDate()
const totalCells = firstDayIndex + totalDays
const trailingCount = (7 - (totalCells % 7)) % 7
return (
<>
{Array.from({ length: firstDayIndex }).map((_, i) => {
const dayNum = prevMonthTotalDays - (firstDayIndex - 1) + i
return (
<div key={`prev-${i}`} className="py-2 text-sm text-gray-300">
{dayNum}
</div>
)
})}
{Array.from({ length: totalDays }, (_, i) => i + 1).map((day) => {
const isHighlighted = mockCalendar.highlighted.includes(day)
return (
<div
key={day}
className={`py-2 rounded-full w-10 h-10 flex flex-col justify-center items-center text-sm ${isHighlighted ? 'bg-yellow-500 text-white' : 'text-gray-700'
}`}
>
{day}
</div>
)
})}
{Array.from({ length: trailingCount }).map((_, i) => (
<div key={`next-${i}`} className="py-2 text-sm text-gray-300">
{i + 1}
</div>
))}
</>
)
})()
}
</div>
</div>
);
}
'use client'
"use client";
import Image from 'next/image'
import { useEffect, useRef, useState } from 'react'
import { Autoplay, Grid } 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 BASE_URL from '@/links/index'
import { Spinner } from '@/components/ui'
import CardNews from './components/card-news'
import CardEvent from './components/card-event'
import EventCalendar from './components/event-calendar'
import dayjs from 'dayjs'
import AppEditorContent from '@/components/shared/editor-content'
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { Autoplay, Grid } 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 BASE_URL from "@/links/index";
import { Spinner } from "@/components/ui";
import CardNews from "./components/card-news";
import CardEvent from "./components/card-event";
import EventCalendar from "@components/base/event-calendar";
import dayjs from "dayjs";
import AppEditorContent from "@/components/shared/editor-content";
// server
import { useGetEvents } from '@/api/endpoints/event'
import { useGetCategory } from '@/api/endpoints/category'
import { useGetNews } from '@/api/endpoints/news'
import { GetCategoryAdminResponseType } from '@/api/types/category'
import { GetNewsAdminResponseType, NewsAdminItem } from '@/api/types/news'
import { EventApiResponse, EventItem } from '@/api/types/event'
import { useGetEvents } from "@/api/endpoints/event";
import { useGetCategory } from "@/api/endpoints/category";
import { useGetNews } from "@/api/endpoints/news";
import { GetCategoryAdminResponseType } from "@/api/types/category";
import { GetNewsAdminResponseType, NewsAdminItem } from "@/api/types/news";
import { EventApiResponse, EventItem } from "@/api/types/event";
import { ChevronsRight, Link } from "lucide-react";
const Page = () => {
const [tab, setTab] = useState('all')
const [currentIndex, setCurrentIndex] = useState(0)
const swiperRef = useRef<SwiperType | null>(null)
const [tab, setTab] = useState("all");
const [currentIndex, setCurrentIndex] = useState(0);
const swiperRef = useRef<SwiperType | null>(null);
const { data: categoryData, isLoading: isLoadingCategory } = useGetCategory<GetCategoryAdminResponseType>()
const { data: categoryData, isLoading: isLoadingCategory } = useGetCategory<GetCategoryAdminResponseType>();
const { data: newsData, isLoading: isLoadingNews } = useGetNews<GetNewsAdminResponseType>(
{ pageSize: '999' },
)
const { data: eventData, isLoading: isLoadingEvent } = useGetEvents<EventApiResponse>()
{ pageSize: '999' }
);
const { data: eventData, isLoading: isLoadingEvent } = useGetEvents<EventApiResponse>();
// filter category
const rows = newsData?.responseData?.rows ?? []
const filteredRows = tab === 'all' ? rows : rows.filter((n) => n.category === tab)
const rows = newsData?.responseData?.rows ?? [];
const filteredRows =
tab === "all" ? rows : rows.filter((n) => n.category === tab);
const images = [
'/home/doi-tac/AMFORI-1.png.webp',
'/home/doi-tac/AUS4SKILLS-1.png.webp',
'/home/doi-tac/BetterWork-1.png.webp',
'/home/doi-tac/BOI-LOGO.jpg.webp',
'/home/doi-tac/DNV-logo-1-1.png.webp',
'/home/doi-tac/GERMAN-COOPERATION-1.png.webp',
'/home/doi-tac/GIZ.png.webp',
'/home/doi-tac/HBA.png.webp',
'/home/doi-tac/ILO-1.png.webp',
'/home/doi-tac/InvestHK.png.webp',
'/home/doi-tac/IOM-1.png.webp',
'/home/doi-tac/JICA-134x100-1-1.jpg.webp',
'/home/doi-tac/KIRBY.png.webp',
'/home/doi-tac/NHO-1.png.webp',
'/home/doi-tac/OXFAM-1.png.webp',
'/home/doi-tac/RECOTVET-1.png.webp',
'/home/doi-tac/SC-1.png.webp',
'/home/doi-tac/UNDP.png.webp',
]
"/home/doi-tac/AMFORI-1.png.webp",
"/home/doi-tac/AUS4SKILLS-1.png.webp",
"/home/doi-tac/BetterWork-1.png.webp",
"/home/doi-tac/BOI-LOGO.jpg.webp",
"/home/doi-tac/DNV-logo-1-1.png.webp",
"/home/doi-tac/GERMAN-COOPERATION-1.png.webp",
"/home/doi-tac/GIZ.png.webp",
"/home/doi-tac/HBA.png.webp",
"/home/doi-tac/ILO-1.png.webp",
"/home/doi-tac/InvestHK.png.webp",
"/home/doi-tac/IOM-1.png.webp",
"/home/doi-tac/JICA-134x100-1-1.jpg.webp",
"/home/doi-tac/KIRBY.png.webp",
"/home/doi-tac/NHO-1.png.webp",
"/home/doi-tac/OXFAM-1.png.webp",
"/home/doi-tac/RECOTVET-1.png.webp",
"/home/doi-tac/SC-1.png.webp",
"/home/doi-tac/UNDP.png.webp",
];
const hoivien = [
'/home/hoi-vien-tieu-bieu/logo-GTD-768x768.png.webp',
'/home/hoi-vien-tieu-bieu/Nhua-Long-Thanh_Logo.jpg.webp',
'/home/hoi-vien-tieu-bieu/Nova_Group_logo-1.png.webp',
'/home/hoi-vien-tieu-bieu/samngoclinh-1-768x768.png.webp',
'/home/hoi-vien-tieu-bieu/Screenshot-2022-12-26-144136-768x768.png.webp',
'/home/hoi-vien-tieu-bieu/UOB-logo_Vuong.jpeg.webp',
]
"/home/hoi-vien-tieu-bieu/logo-GTD-768x768.png.webp",
"/home/hoi-vien-tieu-bieu/Nhua-Long-Thanh_Logo.jpg.webp",
"/home/hoi-vien-tieu-bieu/Nova_Group_logo-1.png.webp",
"/home/hoi-vien-tieu-bieu/samngoclinh-1-768x768.png.webp",
"/home/hoi-vien-tieu-bieu/Screenshot-2022-12-26-144136-768x768.png.webp",
"/home/hoi-vien-tieu-bieu/UOB-logo_Vuong.jpeg.webp",
];
if (isLoadingNews || isLoadingCategory || isLoadingEvent)
return (
<div className="w-full h-[80vh] flex justify-center items-center">
return (
(isLoadingNews || isLoadingCategory || isLoadingEvent) ? (
<div className="container w-full h-[80vh] flex justify-center items-center">
<Spinner />
</div>
)
) : (
<>
{/* Banner */}
<Swiper
modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
slidesPerView={1}
onSwiper={(s) => (swiperRef.current = s)}
onSlideChange={(s) =>
setCurrentIndex(
typeof s.realIndex === "number" ? s.realIndex : s.activeIndex
)
}
>
<SwiperSlide>
<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 h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
</SwiperSlide>
<SwiperSlide>
<img
src="https://vcci-hcm.org.vn/wp-content/uploads/2022/07/Landscape-HCM_3-01.png"
alt="Banner"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
</SwiperSlide>
</Swiper>
return (
<>
{/* Banner */}
<Swiper
modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
slidesPerView={1}
onSwiper={(s) => (swiperRef.current = s)}
onSlideChange={(s) =>
setCurrentIndex(typeof s.realIndex === 'number' ? s.realIndex : s.activeIndex)
}
>
<SwiperSlide>
<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 h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
</SwiperSlide>
<SwiperSlide>
<img
src="https://vcci-hcm.org.vn/wp-content/uploads/2022/07/Landscape-HCM_3-01.png"
alt="Banner"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
</SwiperSlide>
</Swiper>
<div className="container mx-auto px-3 sm:px-6 lg:px-10 space-y-12">
{/* Featured News */}
<section>
<div className="flex items-center justify-center py-8 px-4">
<div className="flex items-center w-full max-w-4xl">
<div className="flex-1 h-[1px] bg-gradient-to-r from-transparent via-gray-300 to-gray-400"></div>
<h1 className="px-6 text-[20px] sm:text-[24px] md:text-[28px] uppercase font-bold text-[#063e8e] whitespace-nowrap">
Tin Nổi Bật
</h1>
<div className="flex-1 h-[1px] bg-gradient-to-l from-transparent via-gray-300 to-gray-400"></div>
</div>
</div>
<Swiper
modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
breakpoints={{
0: { slidesPerView: 1.1, spaceBetween: 10 },
640: { slidesPerView: 2, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="pb-5"
>
{rows.map((news) => (
<SwiperSlide key={news.id}>
<a
href={`/${news.id}`}
className="relative block bg-white shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
>
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full aspect-3/2 sm:h-56 md:h-64 object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 h-20 md:h-24 bg-linear-to-t from-black/80 to-transparent flex items-center justify-center p-3">
<p className="text-white text-center font-semibold line-clamp-2 text-sm sm:text-base leading-snug">
{news.title}
</p>
</div>
</a>
</SwiperSlide>
))}
</Swiper>
</section>
<div className="container mx-auto px-3 sm:px-6 lg:px-10 space-y-12">
{/* Featured News */}
<section>
<div className="flex flex-col items-center py-8 text-center">
<h1 className="text-app-blue text-[20px] sm:text-[24px] md:text-[28px] uppercase font-bold text-blue-900">
Tin Nổi Bật
</h1>
<div className="w-16 h-[3px] bg-blue-900 mt-2 rounded-full"></div>
<div>
<a href="https://hardwaretools.com.vn/">
<img src="/home/Standard-Banner-1-2024.png.webp" alt="banner" />
</a>
</div>
<Swiper
modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
breakpoints={{
0: { slidesPerView: 1.1, spaceBetween: 10 },
640: { slidesPerView: 2, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="pb-5"
>
{rows.map((news) => (
<SwiperSlide key={news.id}>
{/* Tin tức + Liên kết nhanh */}
<section className="flex flex-col lg:flex-row gap-5 pb-10 mb-0">
{/* Left */}
<div className="flex-1">
<div className="flex justify-between items-center">
<a
href={`/${news.id}`}
className="relative block bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
href="/thong-tin-truyen-thong/tin-vcci/"
className="text-[18px] sm:text-[20px] font-semibold uppercase text-[#063e8e]"
>
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full aspect-3/2 sm:h-56 md:h-64 object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 h-20 md:h-24 bg-linear-to-t from-black/80 to-transparent flex items-center justify-center p-3">
<p className="text-white text-center font-semibold line-clamp-2 text-sm sm:text-base leading-snug">
{news.title}
</p>
</div>
Tin tức
</a>
</SwiperSlide>
))}
</Swiper>
</section>
<div>
<a href="https://hardwaretools.com.vn/">
<img src="/home/Standard-Banner-1-2024.png.webp" alt="banner" />
</a>
</div>
{/* Tin tức + Liên kết nhanh */}
<section className="flex flex-col lg:flex-row gap-5 pb-10 mb-0">
{/* Left */}
<div className="flex-1">
<div className="flex justify-between items-center">
<a href="/thong-tin-truyen-thong/tin-vcci/" className="text-[18px] sm:text-[20px] font-bold uppercase text-blue-900">
Tin tức
</a>
<a href="/thong-tin-truyen-thong/tin-vcci/" className="text-blue-900 text-sm sm:text-base">
{'>>'}
</a>
</div>
<hr className="border-blue-900 mb-4" />
<div className="flex flex-col md:flex-row gap-5">
{newsData?.responseData.rows.slice(0, 1).map((news: NewsAdminItem) => (
<a
key={news.id}
href={`${news.id}`}
className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3"
href="/thong-tin-truyen-thong/tin-vcci/"
className="text-[#063e8e] text-sm sm:text-base"
>
<div className="w-full aspect-3/2 overflow-hidden">
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/>
</div>
<div className="flex-1">
<p className="text-[#0056b3] font-bold text-xl line-clamp-2">
{news.title}
</p>
<p className="text-gray-500 text-sm my-1">
{dayjs(news.release_at).format('DD/MM/YYYY')}
</p>
<AppEditorContent className="line-clamp-4" value={news.description} />
</div>
<ChevronsRight />
</a>
))}
</div>
<hr className="border-[#063e8e] mb-4" />
<div className="w-full md:w-1/2">
<div className="flex flex-wrap gap-2 sm:gap-3 mb-3">
<button
className={`flex-1 px-3 sm:px-4 py-2 rounded-md border text-sm transition-colors ${tab === 'all'
? 'border-blue-700 bg-blue-50 text-blue-800 font-semibold'
: 'border-gray-300 bg-white hover:bg-gray-50'
}`}
onClick={() => setTab('all')}
>
Tất cả
</button>
{categoryData?.responseData.rows.slice(0, 3).map((category) => (
<div className="flex flex-col md:flex-row gap-5">
{newsData?.responseData.rows
.slice(0, 1)
.map((news: NewsAdminItem) => (
<a
key={news.id}
href={`${news.id}`}
className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 bg-white"
>
<div className="w-full aspect-3/2 overflow-hidden">
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 p-5">
<p className="text-[#063E8E] font-bold text-xl line-clamp-2">
{news.title}
</p>
<p className="text-gray-500 text-sm my-1">
{dayjs(news.release_at).format("DD/MM/YYYY")}
</p>
<AppEditorContent
className="line-clamp-4"
value={news.description}
/>
</div>
</a>
))}
<div className="w-full md:w-1/2">
<div className="flex flex-wrap gap-2 sm:gap-3 mb-5">
<button
key={category.id}
className={`flex-1 px-3 sm:px-4 py-2 rounded-md border text-sm transition-colors ${category.name === tab
? 'border-blue-700 bg-blue-50 text-blue-800 font-semibold'
: 'border-gray-300 bg-white hover:bg-gray-50'
className={`flex-1 py-[3px] text-sm transition-colors cursor-pointer ${tab === "all"
? " bg-[#d3d3d3] text-[#063e8e] font-semibold"
: "border-gray-300 text-[#363636] bg-[#e8e8e8] hover:bg-[#e8e8e8] hover:text-[#063e8e] font-semibold"
}`}
onClick={() => setTab(category.name)}
onClick={() => setTab("all")}
>
{category.name}
Tất cả
</button>
{categoryData?.responseData.rows
.slice(0, 3)
.map((category) => (
<button
key={category.id}
className={`flex-1 py-[3px] text-[14px] transition-colors cursor-pointer ${category.name === tab
? "bg-[#d3d3d3] text-[#063e8e] font-semibold"
: "border-gray-300 text-[#363636] bg-[#e8e8e8] hover:bg-[#e8e8e8] hover:text-[#063e8e] font-semibold"
}`}
onClick={() => setTab(category.name)}
>
{category.name}
</button>
))}
</div>
{filteredRows.slice(0, 4).map((news) => (
<CardNews key={news.id} news={news} />
))}
</div>
{filteredRows.slice(0, 4).map((news) => (
<CardNews key={news.id} news={news} />
))}
</div>
</div>
</div>
{/* Right */}
<aside className="w-full lg:w-[30%]">
<div className="flex justify-between items-center">
<h2 className="text-[18px] sm:text-[20px] font-bold uppercase text-blue-900">
Liên kết nhanh
</h2>
<a href="#" className="text-blue-900 text-sm sm:text-base">
{'>>'}
</a>
</div>
<hr className="border-blue-900 mb-4" />
<div className="space-y-2 text-blue-900 text-sm md:text-base pb-10">
<div>
<a href="https://vcci-hcm.org.vn/lien-ket-nhanh/cam-nang-huong-dan-dau-tu-kinh-doanh-tai-viet-nam-2023/">
🔗 Cẩm nang hướng dẫn đầu tư kinh doanh tại Việt Nam
{/* Right */}
<aside className="w-full lg:w-[30%]">
<div className="flex justify-between items-center">
<h2 className="text-[18px] sm:text-[20px] font-semibold uppercase text-[#063e8e]">
Liên kết nhanh
</h2>
<a href="#" className="text-[#063e8e] text-sm sm:text-base">
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-4" />
<div className="space-y-2 text-[#063e8e] text-sm md:text-base pb-10">
<div>
<a
className="text-[#363636]"
href="https://vcci-hcm.org.vn/lien-ket-nhanh/cam-nang-huong-dan-dau-tu-kinh-doanh-tai-viet-nam-2023/"
>
<Link
size={16}
className="inline mr-2 font-semibold text-[#063e8e]"
style={{ verticalAlign: "middle", marginTop: "-2px" }}
/>
Cẩm nang hướng dẫn đầu tư kinh doanh tại Việt Nam
</a>
</div>
<div>
<a
className="text-[#363636]"
href="https://vcci-hcm.org.vn/lien-ket-nhanh/doanh-nghiep-kien-nghi-ve-chinh-sach-va-phap-luat/"
>
<Link
size={16}
className="inline mr-2 font-semibold text-[#063e8e]"
style={{ verticalAlign: "middle", marginTop: "-2px" }}
/>
Doanh nghiệp kiến nghị về chính sách và pháp luật
</a>
</div>
</div>
<div>
<a href='https://vcci-hcm.org.vn/lien-ket-nhanh/doanh-nghiep-kien-nghi-ve-chinh-sach-va-phap-luat/'>
🔗 Doanh nghiệp kiến nghị về chính sách và pháp luật
<a href="https://hardwaretools.com.vn/">
<img src="/home/20-2048x1365.webp" alt="banner" />
</a>
</div>
</div>
<div>
<a href="https://hardwaretools.com.vn/">
<img src="/home/20-2048x1365.webp" alt="banner" />
</a>
</div>
</aside>
</section>
{/* Sự kiện */}
<section className="flex flex-col lg:flex-row gap-5 pb-10 mb-0">
<div className="flex-1 bg-blue-900 p-5">
<div className="flex justify-between items-center">
<h2 className="text-[18px] sm:text-[20px] font-bold uppercase text-[#e8c518]">
Sự kiện sắp diễn ra
</h2>
<a href="#" className="text-[#e8c518] text-sm sm:text-base">
{'>>'}
</a>
</div>
<hr className="border-[#e8c518] mb-4" />
<div className="flex flex-col md:flex-row gap-5">
{eventData?.responseData.rows.slice(0, 1).map((event: EventItem) => (
<a
key={event.id}
href={`${event.id}`}
className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3"
>
<div className="w-full aspect-3/2 overflow-hidden">
<img
src={`${BASE_URL.imageEndpoint}${event.image}`}
alt={event.name}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}} />
</div>
</aside>
</section >
<div className="flex-1">
<p className="text-[#0056b3] font-bold text-xl line-clamp-2">
{event.name}
</p>
<p className="text-gray-500 text-sm my-1">
{dayjs(event.start_time).format('DD/MM/YYYY')}
</p>
<AppEditorContent className="line-clamp-3" value={event.description} />
</div>
</a>
))}
<div className="w-full md:w-1/2">
{eventData?.responseData.rows.slice(0, 4).map((event) => (
<CardEvent key={event.id} event={event} />
))}
</div>
</div>
</div>
<div className='bg-blue-900 w-full lg:w-[30%] p-5'>
<aside>
{/* Sự kiện */}
< section className="flex flex-col lg:flex-row gap-5 pb-10 mb-0" >
<div className="flex-1 bg-[#063e8e] p-5">
<div className="flex justify-between items-center">
<h2 className="text-[18px] sm:text-[20px] font-bold uppercase text-[#e8c518]">
Lịch sự kiện
Sự kiện sắp diễn ra
</h2>
<a href="#" className="text-[#e8c518] hover:underline text-sm sm:text-base">
{'>>'}
<a href="#" className="text-[#e8c518] text-sm sm:text-base">
<ChevronsRight />
</a>
</div>
<hr className="border-[#e8c518] mb-4" />
<EventCalendar />
</aside>
</div>
</section>
{/* Cơ hội kinh doanh + Chính sách */}
<div className='flex flex-col lg:flex-row gap-5 pb-10 mb-0'>
<div className='flex flex-col flex-1'>
<div>
<a href="https://vcci-hcm.org.vn/wp-content/uploads/2022/11/MEDIA-KIT_VCCI-HCM-2022-Final.pdf">
<img src="/home/Standard-Banner-1-2024.png.webp" alt="banner" />
</a>
</div>
<section className="flex flex-col md:flex-row gap-5 pt-8">
<div className="flex-1">
<div className="flex justify-between items-center">
<a href="/xuc-tien-thuong-mai/co-hoi-kinh-doanh/" className="text-[18px] sm:text-[20px] font-bold uppercase text-blue-900">
Cơ hội kinh doanh
</a>
<a href="/xuc-tien-thuong-mai/co-hoi-kinh-doanh/" className="text-blue-900 text-sm sm:text-base">
{'>>'}
</a>
</div>
<hr className="border-blue-900 mb-4" />
<div className="pt-2">
{newsData?.responseData.rows.slice(0, 1).map((news: NewsAdminItem) => (
<div className="flex flex-col md:flex-row gap-5">
{eventData?.responseData.rows
.slice(0, 1)
.map((event: EventItem) => (
<a
key={news.id}
href={`${news.id}`}
key={event.id}
href={`${event.id}`}
className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3"
>
<div className="w-full aspect-3/2 relative overflow-hidden mb-5">
<div className="w-full aspect-3/2 overflow-hidden">
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
src={`${BASE_URL.imageEndpoint}${event.image}`}
alt={event.name}
className="w-full h-full object-cover"
/>
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
<p className="text-blue-900 font-semibold text-sm sm:text-base z-10 line-clamp-3">
{news.title}
</p>
</div>
</div>
<div className="flex-1">
<p className="text-[#0056b3] font-bold text-xl line-clamp-2">
{event.name}
</p>
<p className="text-gray-500 text-sm my-1">
{dayjs(event.start_time).format("DD/MM/YYYY")}
</p>
<AppEditorContent
className="line-clamp-3"
value={event.description}
/>
</div>
</a>
))}
{rows.slice(0, 3).map((news) => (
<CardNews key={news.id} news={news} />
<div className="w-full md:w-1/2">
{eventData?.responseData.rows.slice(0, 4).map((event) => (
<CardEvent key={event.id} event={event} />
))}
</div>
</div>
<div className="flex-1">
</div>
<div className="bg-[#063e8e] w-full lg:w-[30%] p-5">
<aside>
<div className="flex justify-between items-center">
<a href="/thong-tin-truyen-thong/thong-tin-chinh-sach-va-phap-luat" className="text-[18px] sm:text-[20px] font-bold uppercase text-blue-900">
Chính sách & pháp luật
</a>
<a href="/thong-tin-truyen-thong/thong-tin-chinh-sach-va-phap-luat" className="text-blue-900 text-sm sm:text-base">
{'>>'}
<h2 className="text-[18px] sm:text-[20px] font-bold uppercase text-[#e8c518]">
Lịch sự kiện
</h2>
<a
href="#"
className="text-[#e8c518] hover:underline text-sm sm:text-base"
>
<ChevronsRight />
</a>
</div>
<hr className="border-blue-900 mb-4" />
<div className="pt-2">
{newsData?.responseData.rows.slice(0, 1).map((news: NewsAdminItem) => (
<hr className="border-[#e8c518] mb-4" />
<EventCalendar />
</aside>
</div>
</section >
{/* Cơ hội kinh doanh + Chính sách */}
< div className="flex flex-col lg:flex-row gap-5 pb-10 mb-0" >
<div className="flex flex-col flex-1">
<div>
<a href="https://vcci-hcm.org.vn/wp-content/uploads/2022/11/MEDIA-KIT_VCCI-HCM-2022-Final.pdf">
<img src="/home/Standard-Banner-1-2024.png.webp" alt="banner" />
</a>
</div>
<section className="flex flex-col md:flex-row gap-5 pt-8">
<div className="flex-1">
<div className="flex justify-between items-center">
<a
key={news.id}
href={`${news.id}`}
href="/xuc-tien-thuong-mai/co-hoi-kinh-doanh/"
className="text-[18px] sm:text-[20px] font-bold uppercase text-[#063e8e]"
>
<div className="w-full aspect-3/2 relative overflow-hidden mb-5">
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full h-full object-cover"
/>
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
<p className="text-blue-900 font-semibold text-sm sm:text-base z-10 line-clamp-3">
{news.title}
</p>
</div>
</div>
Cơ hội kinh doanh
</a>
))}
<a
href="/xuc-tien-thuong-mai/co-hoi-kinh-doanh/"
className="text-[#063e8e] text-sm sm:text-base"
>
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-4" />
<div className="pt-2">
{newsData?.responseData.rows
.slice(0, 1)
.map((news: NewsAdminItem) => (
<a key={news.id} href={`${news.id}`}>
<div className="w-full aspect-3/2 relative overflow-hidden mb-5">
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full h-full object-cover"
/>
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
<p className="text-[#063e8e] font-semibold text-sm sm:text-base z-10 line-clamp-3">
{news.title}
</p>
</div>
</div>
</a>
))}
{rows.slice(0, 3).map((news) => (
<CardNews key={news.id} news={news} />
))}
{rows.slice(0, 3).map((news) => (
<CardNews key={news.id} news={news} />
))}
</div>
</div>
</div>
</section>
</div>
<div className='w-full lg:w-[30%] justify-center items-start flex'>
<a href="https://smartgara.ecaraid.com/">
<img src="/home/eCarAid_web_banner_600x400.webp" alt="banner" />
</a>
</div>
</div >
{/* Hội viên tiêu biểu */}
< section className="flex flex-col lg:flex-row gap-5 pb-10 mb-0" >
{/* left */}
< aside className="w-full lg:w-1/3 flex-1 bg-[#e8c518] p-5" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-blue-900">Hội viên tiêu biểu</h2>
<a
href="#"
className="text-blue-900 hover:underline text-sm font-medium"
>
{'>>'}
</a>
</div>
<hr className="border-blue-900 mb-5" />
<div>
<Swiper
modules={[Autoplay, Grid]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
grid={{ rows: 1, fill: 'row' }}
slidesPerGroup={3}
breakpoints={{
0: { slidesPerView: 2, spaceBetween: 10 },
640: { slidesPerView: 3, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="partner-swiper"
>
{hoivien.map((src, i) => (
<SwiperSlide key={i}>
<div className="aspect-square flex justify-center items-center bg-white rounded-lg shadow">
<img
src={src}
alt={`partner-${i}`}
className="w-3/4 h-3/4 object-contain"
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</aside >
{/* right */}
< aside className="w-full lg:w-[30%] py-5" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-blue-900">Kết nối hội viên</h2>
<a
href="#"
className="text-blue-900 hover:underline text-sm font-medium"
>
{'>>'}
</a>
</div>
<hr className="border-blue-900 mb-5" />
<div className="pb-10">
<Swiper
modules={[Autoplay, Grid]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
grid={{ rows: 2, fill: 'row' }}
slidesPerGroup={3}
breakpoints={{
0: { slidesPerView: 2, spaceBetween: 10 },
640: { slidesPerView: 3, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="partner-swiper"
>
{images.map((src, i) => (
<SwiperSlide key={i}>
<div className="aspect-square flex justify-center items-center bg-white rounded-lg shadow">
<img
src={src}
alt={`partner-${i}`}
className="w-3/4 h-3/4 object-contain"
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</aside >
</section >
<div className="flex-1">
<div className="flex justify-between items-center">
<a
href="/thong-tin-truyen-thong/thong-tin-chinh-sach-va-phap-luat"
className="text-[18px] sm:text-[20px] font-bold uppercase text-[#063e8e]"
>
Chính sách & pháp luật
</a>
<a
href="/thong-tin-truyen-thong/thong-tin-chinh-sach-va-phap-luat"
className="text-[#063e8e] text-sm sm:text-base"
>
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-4" />
<div className="pt-2">
{newsData?.responseData.rows
.slice(0, 1)
.map((news: NewsAdminItem) => (
<a key={news.id} href={`${news.id}`}>
<div className="w-full aspect-3/2 relative overflow-hidden mb-5">
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full h-full object-cover"
/>
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
<p className="text-[#063e8e] font-semibold text-sm sm:text-base z-10 line-clamp-3">
{news.title}
</p>
</div>
</div>
</a>
))}
{/* Video + đối tác */}
< section className="flex flex-col lg:flex-row gap-5 pb-10" >
{/* left */}
< div className="flex flex-col flex-1" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-blue-900">Video</h2>
<a
href="#"
className="text-blue-900 hover:underline text-sm font-medium"
>
{'>>'}
</a>
</div>
<hr className="border-blue-900 mb-5" />
<div className="flex flex-col md:flex-row gap-4 md:gap-6">
{[
{
src: 'https://www.youtube.com/embed/J0Iz0iGuAXY',
title: 'VCCI-HCM 2024 IN REVIEW (ENGLISH VERSION)',
},
{
src: 'https://www.youtube.com/embed/_OnnGWv2ehM',
title: 'Hội nghị Hội viên VCCI - Gala Mừng Xuân Ất Tỵ 2025',
},
].map((video, i) => (
<div key={i} className="w-full md:w-1/2">
<div className="aspect-video rounded-lg overflow-hidden shadow">
<iframe
className="w-full h-full font-bold"
src={video.src}
title={video.title}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
{rows.slice(0, 3).map((news) => (
<CardNews key={news.id} news={news} />
))}
</div>
<p className="mt-2 text-sm text-gray-700 font-medium">{video.title}</p>
</div>
))}
</section>
</div>
</div >
{/* right */}
< aside className="w-full lg:w-[30%]" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-blue-900">Đối tác</h2>
<a
href="#"
className="text-blue-900 hover:underline text-sm font-medium"
>
{'>>'}
<div className="w-full lg:w-[30%] justify-center items-start flex">
<a href="https://smartgara.ecaraid.com/">
<img src="/home/eCarAid_web_banner_600x400.webp" alt="banner" />
</a>
</div>
<hr className="border-blue-900 mb-5" />
<div className="pb-10">
<Swiper
modules={[Autoplay, Grid]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
grid={{ rows: 2, fill: 'row' }}
slidesPerGroup={3}
breakpoints={{
0: { slidesPerView: 2, spaceBetween: 10 },
640: { slidesPerView: 3, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="partner-swiper"
>
{images.map((src, i) => (
<SwiperSlide key={i}>
<div className="aspect-square flex justify-center items-center bg-white rounded-lg shadow">
<img
src={src}
alt={`partner-${i}`}
className="w-3/4 h-3/4 object-contain"
</div >
{/* Hội viên tiêu biểu */}
< section className="flex flex-col lg:flex-row gap-5 pb-10 mb-0" >
{/* left */}
< aside className="w-full lg:w-1/3 flex-1 bg-[#e8c518] p-5" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-[#063e8e]">
Hội viên tiêu biểu
</h2>
<a
href="#"
className="text-[#063e8e] hover:underline text-sm font-medium"
>
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-5" />
<div>
<Swiper
modules={[Autoplay, Grid]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
grid={{ rows: 1, fill: "row" }}
slidesPerGroup={3}
breakpoints={{
0: { slidesPerView: 2, spaceBetween: 10 },
640: { slidesPerView: 3, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="partner-swiper"
>
{hoivien.map((src, i) => (
<SwiperSlide key={i}>
<div className="aspect-square flex justify-center items-center bg-white rounded-lg shadow">
<img
src={src}
alt={`partner-${i}`}
className="w-3/4 h-3/4 object-contain"
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</aside >
{/* right */}
< aside className="w-full lg:w-[30%] py-5" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-[#063e8e]">
Kết nối hội viên
</h2>
<a
href="#"
className="text-[#063e8e] hover:underline text-sm font-medium"
>
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-5" />
<div className="pb-10">
<Swiper
modules={[Autoplay, Grid]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
grid={{ rows: 2, fill: "row" }}
slidesPerGroup={3}
breakpoints={{
0: { slidesPerView: 2, spaceBetween: 10 },
640: { slidesPerView: 3, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="partner-swiper"
>
{images.map((src, i) => (
<SwiperSlide key={i}>
<div className="aspect-square flex justify-center items-center bg-white rounded-lg shadow">
<img
src={src}
alt={`partner-${i}`}
className="w-3/4 h-3/4 object-contain"
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</aside >
</section >
{/* Video + đối tác */}
< section className="flex flex-col lg:flex-row gap-5 pb-10" >
{/* left */}
< div className="flex flex-col flex-1" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-[#063e8e]">
Video
</h2>
<a
href="#"
className="text-[#063e8e] hover:underline text-sm font-medium"
>
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-5" />
<div className="flex flex-col md:flex-row gap-4 md:gap-6">
{[
{
src: "https://www.youtube.com/embed/J0Iz0iGuAXY",
title: "VCCI-HCM 2024 IN REVIEW (ENGLISH VERSION)",
},
{
src: "https://www.youtube.com/embed/_OnnGWv2ehM",
title: "Hội nghị Hội viên VCCI - Gala Mừng Xuân Ất Tỵ 2025",
},
].map((video, i) => (
<div key={i} className="w-full md:w-1/2">
<div className="aspect-video rounded-lg overflow-hidden shadow">
<iframe
className="w-full h-full font-bold"
src={video.src}
title={video.title}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
</div>
</SwiperSlide>
<p className="mt-2 text-sm text-gray-700 font-medium">
{video.title}
</p>
</div>
))}
</Swiper>
</div>
</aside >
</section >
</div >
</>
)
}
</div>
</div >
{/* right */}
< aside className="w-full lg:w-[30%]" >
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-bold uppercase text-[#063e8e]">
Đối tác
</h2>
<a
href="#"
className="text-[#063e8e] hover:underline text-sm font-medium"
>
<ChevronsRight />
</a>
</div>
<hr className="border-[#063e8e] mb-5" />
<div className="pb-10">
<Swiper
modules={[Autoplay, Grid]}
autoplay={{ delay: 4000, disableOnInteraction: false }}
loop
grid={{ rows: 2, fill: "row" }}
slidesPerGroup={3}
breakpoints={{
0: { slidesPerView: 2, spaceBetween: 10 },
640: { slidesPerView: 3, spaceBetween: 16 },
1024: { slidesPerView: 3, spaceBetween: 24 },
}}
className="partner-swiper"
>
{images.map((src, i) => (
<SwiperSlide key={i}>
<div className="aspect-square flex justify-center items-center bg-white rounded-lg shadow">
<img
src={src}
alt={`partner-${i}`}
className="w-3/4 h-3/4 object-contain"
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</aside >
</section >
</div >
</>
)
);
};
export default Page
export default Page;
"use client";
import { useState, useEffect } from "react";
import { ChevronsUp } from "lucide-react";
export default function ScrollToTopButton() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const toggleVisibility = () => {
setVisible(window.scrollY > 300);
};
window.addEventListener("scroll", toggleVisibility);
return () => window.removeEventListener("scroll", toggleVisibility);
}, []);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<button
onClick={scrollToTop}
className={`fixed bottom-25 right-6 bg-[#e8c518] hover:text-[#063e8e] text-white p-3 rounded-lg shadow-lg transition-all duration-500 cursor-pointer ${
visible
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-3 pointer-events-none"
}`}
>
<ChevronsUp size={24} />
</button>
);
}
......@@ -170,7 +170,7 @@ function Footer() {
</div>
</div>
<div className="bg-[#032248] h-[80px] flex items-center justify-center">
<div className="max-w-[1200px] w-full p-5">
<div className="container w-full p-5">
<p className="text-[14px] text-white">
© Bản quyền VCCI-HCM | All rights reserved
</p>
......
......@@ -7,12 +7,7 @@ import { Button } from '@/components/ui/button'
type Category = { id: string; title: string }
const MOCK_CATEGORIES: Category[] = [
{ id: 'topic', title: 'Chuyên đề' },
{ id: 'training', title: 'Tập huấn NSDLĐ' },
{ id: 'law', title: 'Xây dựng và Phổ biến pháp luật' },
{ id: 'trade', title: 'Xúc tiến thương mại và Đầu tư' }
]
type FilterPayload = {
upcoming: boolean
......@@ -22,14 +17,20 @@ type FilterPayload = {
fromDate: string
toDate: string
}
export const EventFilter: React.FC<{ onFilter?: (payload: FilterPayload) => void; onReset?: () => void }> = ({ onFilter, onReset }) => {
export const EventFilter: React.FC<{
categories?: Category[]
onFilter?: (payload: FilterPayload) => void
onReset?: () => void
}> = ({ categories: categoriesProp, onFilter, onReset }) => {
const [upcoming, setUpcoming] = useState(false)
const [past, setPast] = useState(false)
const [query, setQuery] = useState('')
// Use categories passed via props if available; otherwise use an empty array
const categoriesList = categoriesProp ?? []
const [categories, setCategories] = useState<Record<string, boolean>>(() => {
const map: Record<string, boolean> = {}
MOCK_CATEGORIES.forEach((c) => (map[c.id] = false))
categoriesList.forEach((c) => (map[c.id] = false))
return map
})
const [fromDate, setFromDate] = useState('')
......@@ -85,14 +86,16 @@ export const EventFilter: React.FC<{ onFilter?: (payload: FilterPayload) => void
/>
</div>
<div className="mb-4">
{MOCK_CATEGORIES.map((c) => (
<label key={c.id} className="flex items-center gap-2 mb-2">
<Checkbox checked={!!categories[c.id]} onCheckedChange={() => toggleCategory(c.id)} />
<span className="text-sm">{c.title}</span>
</label>
))}
</div>
{categoriesList && categoriesList.length > 0 && (
<div className="mb-4">
{categoriesList.map((c) => (
<label key={c.id} className="flex items-center gap-2 mb-2">
<Checkbox checked={!!categories[c.id]} onCheckedChange={() => toggleCategory(c.id)} />
<span className="text-sm">{c.title}</span>
</label>
))}
</div>
)}
<div className="mb-4">
<Label className="block text-sm mb-1">Từ ngày:</Label>
......
'use client'
import React from "react";
import ListCategory from "../components/list-category";
import EventCalendar from "../components/event-calendar";
import EventCalendar from '@/components/base/event-calendar';
const Page = () => {
return (
......
......@@ -4,7 +4,6 @@ import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
// ...existing code...
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import { useGetEvents } from '@api/endpoints/event'
......@@ -14,8 +13,8 @@ import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
const [filtersString, setFiltersString] = useState<string | undefined>('')
const pageSize = 5;
const { data: allData, isLoading } = useGetEvents<EventApiResponse>({
......@@ -23,7 +22,7 @@ export default function Page() {
currentPage: String(page),
sortField: 'start_time',
sortOrder: 'ASC',
filters: submitSearch ? `title @=${submitSearch},start_time>${new Date()}` : `start_time>${new Date()}`,
filters: filtersString ?? undefined,
});
return (
<div className="min-h-screen container mx-auto p-4">
......@@ -61,7 +60,37 @@ export default function Page() {
{/* Sidebar */}
<aside className="space-y-6">
<EventFilter />
<EventFilter
onFilter={(payload) => {
const parts: string[] = []
// query
if (payload.query) parts.push(`title @=${payload.query}`)
const nowIso = new Date().toISOString()
// upcoming / past
if (payload.upcoming && !payload.past) {
parts.push(`start_time>${nowIso}`)
} else if (payload.past && !payload.upcoming) {
parts.push(`start_time<=${nowIso}`)
}
if (payload.fromDate) {
const fromIso = new Date(payload.fromDate).toISOString()
parts.push(`created_at>=${fromIso}`)
}
if (payload.toDate) {
const toIso = new Date(payload.toDate).toISOString()
parts.push(`created_at<=${toIso}`)
}
const filters = parts.length > 0 ? parts.join(',') : undefined
setFiltersString(filters)
setPage(1)
}}
onReset={() => {
setFiltersString(undefined)
setPage(1)
}}
/>
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
......
'use client'
import React from "react";
import ListCategory from "./components/list-category";
import EventCalendar from "./components/event-calendar";
import EventCalendar from "@/components/base/event-calendar";
const Page = () => {
return (
......
import Header from "@/app/(main)/_lib/layout/header"
import Footer from "@/app/(main)/_lib/layout/footer"
import Header from "@/app/(main)/_lib/layout/header";
import Footer from "@/app/(main)/_lib/layout/footer";
import React from "react";
import ScrollToTopButton from "./_lib/layout/ScrollToTopButton";
export default function Layout({
children,
......@@ -10,10 +9,11 @@ export default function Layout({
children: React.ReactNode;
}>) {
return (
<main className="bg-background">
<Header />
{children}
<Footer />
</main>
<main className="flex flex-col min-h-screen bg-background">
<Header />
<div className="flex-1">{children}</div>
<ScrollToTopButton />
<Footer />
</main>
);
}
......@@ -4,7 +4,7 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@/constants/categories";
import Image from "next/image";
import Link from "next/link";
import { notFound, useParams } from "next/navigation";
import Calendar from "../components/calendar";
import EventCalendar from "@/components/base/event-calendar";
const publications = [
{
......@@ -68,7 +68,7 @@ export default function PublicationDetail() {
return (
<div className="bg-[#f6f6f6] min-h-screen">
<div className="max-w-[1200px] mx-auto flex flex-col gap-5 mb-[50px]">
<div className="container mx-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
</div>
......@@ -106,7 +106,7 @@ export default function PublicationDetail() {
</div>
<div className="lg:w-[calc(35%-10px)] w-full">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
"use client";
import React, { useState, useMemo } from "react";
import {
format,
startOfMonth,
endOfMonth,
eachDayOfInterval,
isSameMonth,
isSameDay,
addMonths,
subMonths,
} from "date-fns";
import { ArrowLeft, ArrowRight, ChevronLeft, ChevronRight } from "lucide-react";
import { vi } from "date-fns/locale";
interface Event {
date: Date;
title: string;
type: "event" | "training";
description?: string;
}
export default function Calendar() {
const [currentMonth, setCurrentMonth] = useState(new Date());
const today = new Date();
// Dữ liệu mẫu
const events: Event[] = [
{
date: new Date(2025, 10, 1),
title: "Đào tạo nội bộ",
type: "training",
description: "Khóa học kỹ năng mềm",
},
{
date: new Date(2025, 10, 3),
title: "Họp cổ đông",
type: "event",
description: "Báo cáo Q3",
},
{
date: new Date(2025, 10, 13),
title: "Đào tạo kỹ thuật",
type: "training",
description: "React Advanced",
},
{
date: new Date(2025, 10, 14),
title: "Đào tạo an toàn",
type: "training",
description: "An toàn lao động",
},
{
date: new Date(2025, 10, 20),
title: "Hội thảo thuế",
type: "event",
description:
"Cập nhật luật thuế thu nhập doanh nghiệp số 67/2025/QH15...",
},
{
date: new Date(2025, 10, 28),
title: "Sự kiện nội bộ",
type: "event",
description: "Team building",
},
];
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const monthDays = eachDayOfInterval({ start: monthStart, end: monthEnd });
const firstDayOfWeek = monthStart.getDay(); // 0 = CN, 1 = T2...
const startDate = new Date(monthStart);
startDate.setDate(startDate.getDate() - firstDayOfWeek);
const days = [];
for (let i = 0; i < 42; i++) {
const day = new Date(startDate);
day.setDate(startDate.getDate() + i);
days.push(day);
}
const getEventForDay = (date: Date) =>
events.filter((e) => isSameDay(e.date, date));
const formatMonthTitle = () => {
return `THÁNG ${format(currentMonth, "M/yyyy")}`.toUpperCase();
};
return (
<>
<div className="w-full mx-auto bg-white rounded-lg p-4 ">
{/* Header */}
<div className="flex items-center justify-between mb-4 px-3">
<h2 className="text-[15px] font-bold text-[#063E8E]">
{formatMonthTitle()}
</h2>
<div className="flex gap-3">
<button
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
className="p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<ArrowLeft className="group-hover:text-[#e8c518] text-[#363636] w-5 h-5" />
</button>
<button
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
className="p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<ArrowRight className="group-hover:text-[#e8c518] text-[#363636] w-5 h-5" />
</button>
</div>
</div>
{/* Days of week */}
<div className="grid grid-cols-7 text-center text-sm font-medium text-gray-600 mb-1">
{["CN", "T2", "T3", "T4", "T5", "T6", "T7"].map((day) => (
<div key={day} className="py-2 text-[15px] text-[#063E8E]">
{day}
</div>
))}
</div>
{/* Calendar grid */}
<div className="grid grid-cols-7 gap-1 text-sm">
{days.map((day, idx) => {
const dayEvents = getEventForDay(day);
const isCurrentMonth = isSameMonth(day, currentMonth);
const isToday = isSameDay(day, today);
const hasEvent = dayEvents.some((e) => e.type === "event");
const hasTraining = dayEvents.some((e) => e.type === "training");
return (
<div
key={idx}
className={`
relative group aspect-square flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-[#A4A4A4]" : "text-[#333333]"}
${hasEvent || hasTraining ? "text-white" : "text-[#333333]"}
${isToday ? "text-red-600 font-bold" : ""}
hover:bg-gray-50 transition
`}
>
<span className="relative z-10">{format(day, "d")}</span>
{/* Event/Training dots */}
{(hasEvent || hasTraining) && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="flex">
{hasEvent && (
<div className="w-10 h-10 bg-blue-600 rounded-full"></div>
)}
{hasTraining && (
<div className="w-10 h-10 bg-yellow-500 rounded-full"></div>
)}
</div>
</div>
)}
{/* Tooltip on hover */}
{dayEvents.length > 0 && (
<div
className="absolute top-full left-1/2 -translate-x-1/2 mt-2
w-64 p-3 bg-gray-900 text-white text-xs rounded-lg
shadow-xl opacity-0 pointer-events-none
group-hover:opacity-90 transition-opacity z-50"
>
<div className="space-y-2">
{dayEvents.map((event, i) => (
<div
key={i}
className="border-b border-gray-700 border-opacity-20 last:border-0 pb-2 last:pb-0"
>
<div className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded-full ${
event.type === "event"
? "bg-blue-400"
: "bg-yellow-400"
}`}
></div>
<span className="font-medium">{event.title}</span>
</div>
{event.description && (
<p className="text-gray-300 mt-1 text-xs">
{event.description}
</p>
)}
</div>
))}
</div>
{/* Mũi tên nhọn HƯỚNG LÊN (chỉ vào ngày) */}
<div
className="absolute bottom-full left-1/2 -translate-x-1/2 -mb-1
w-0 h-0
border-l-8 border-l-transparent
border-r-8 border-r-transparent
border-b-8 border-b-gray-900"
></div>
</div>
)}
</div>
);
})}
</div>
{/* Legend */}
<div className="flex justify-center gap-6 mt-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
<span>Sự kiện</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<span>Đào tạo</span>
</div>
</div>
</div>
</>
);
}
......@@ -7,7 +7,7 @@ import PublicationList from "./components/publicationList";
export default function Page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
</div>
......
......@@ -5,7 +5,7 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import EventCalendar from "@app/dai-dien-gioi-chu/components/event-calendar";
import EventCalendar from "@/components/base/event-calendar";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import { useGetNews } from "@api/endpoints/news";
......
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -122,7 +122,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
"use client";
import React, { useState, useMemo } from "react";
import {
format,
startOfMonth,
endOfMonth,
eachDayOfInterval,
isSameMonth,
isSameDay,
addMonths,
subMonths,
} from "date-fns";
import { ArrowLeft, ArrowRight, ChevronLeft, ChevronRight } from "lucide-react";
import { vi } from "date-fns/locale";
interface Event {
date: Date;
title: string;
type: "event" | "training";
description?: string;
}
export default function Calendar() {
const [currentMonth, setCurrentMonth] = useState(new Date());
const today = new Date();
// Dữ liệu mẫu
const events: Event[] = [
{
date: new Date(2025, 10, 1),
title: "Đào tạo nội bộ",
type: "training",
description: "Khóa học kỹ năng mềm",
},
{
date: new Date(2025, 10, 3),
title: "Họp cổ đông",
type: "event",
description: "Báo cáo Q3",
},
{
date: new Date(2025, 10, 13),
title: "Đào tạo kỹ thuật",
type: "training",
description: "React Advanced",
},
{
date: new Date(2025, 10, 14),
title: "Đào tạo an toàn",
type: "training",
description: "An toàn lao động",
},
{
date: new Date(2025, 10, 20),
title: "Hội thảo thuế",
type: "event",
description:
"Cập nhật luật thuế thu nhập doanh nghiệp số 67/2025/QH15...",
},
{
date: new Date(2025, 10, 28),
title: "Sự kiện nội bộ",
type: "event",
description: "Team building",
},
];
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const monthDays = eachDayOfInterval({ start: monthStart, end: monthEnd });
const firstDayOfWeek = monthStart.getDay(); // 0 = CN, 1 = T2...
const startDate = new Date(monthStart);
startDate.setDate(startDate.getDate() - firstDayOfWeek);
const days = [];
for (let i = 0; i < 42; i++) {
const day = new Date(startDate);
day.setDate(startDate.getDate() + i);
days.push(day);
}
const getEventForDay = (date: Date) =>
events.filter((e) => isSameDay(e.date, date));
const formatMonthTitle = () => {
return `THÁNG ${format(currentMonth, "M/yyyy")}`.toUpperCase();
};
return (
<>
<div className="w-full mx-auto bg-white rounded-lg p-4 ">
{/* Header */}
<div className="flex items-center justify-between mb-4 px-3">
<h2 className="text-[15px] font-bold text-[#063E8E]">
{formatMonthTitle()}
</h2>
<div className="flex gap-3">
<button
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
className="p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<ArrowLeft className="group-hover:text-[#e8c518] text-[#363636] w-5 h-5" />
</button>
<button
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
className="p-2 cursor-pointer rounded-full group border-3 border-[#363636] hover:border-[#063e8e] transition"
>
<ArrowRight className="group-hover:text-[#e8c518] text-[#363636] w-5 h-5" />
</button>
</div>
</div>
{/* Days of week */}
<div className="grid grid-cols-7 text-center text-sm font-medium text-gray-600 mb-1">
{["CN", "T2", "T3", "T4", "T5", "T6", "T7"].map((day) => (
<div key={day} className="py-2 text-[15px] text-[#063E8E]">
{day}
</div>
))}
</div>
{/* Calendar grid */}
<div className="grid grid-cols-7 gap-1 text-sm">
{days.map((day, idx) => {
const dayEvents = getEventForDay(day);
const isCurrentMonth = isSameMonth(day, currentMonth);
const isToday = isSameDay(day, today);
const hasEvent = dayEvents.some((e) => e.type === "event");
const hasTraining = dayEvents.some((e) => e.type === "training");
return (
<div
key={idx}
className={`
relative group aspect-square flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-[#A4A4A4]" : "text-[#333333]"}
${hasEvent || hasTraining ? "text-white" : "text-[#333333]"}
${isToday ? "text-red-600 font-bold" : ""}
hover:bg-gray-50 transition
`}
>
<span className="relative z-10">{format(day, "d")}</span>
{/* Event/Training dots */}
{(hasEvent || hasTraining) && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="flex">
{hasEvent && (
<div className="w-10 h-10 bg-blue-600 rounded-full"></div>
)}
{hasTraining && (
<div className="w-10 h-10 bg-yellow-500 rounded-full"></div>
)}
</div>
</div>
)}
{/* Tooltip on hover */}
{dayEvents.length > 0 && (
<div
className="absolute top-full left-1/2 -translate-x-1/2 mt-2
w-64 p-3 bg-gray-900 text-white text-xs rounded-lg
shadow-xl opacity-0 pointer-events-none
group-hover:opacity-90 transition-opacity z-50"
>
<div className="space-y-2">
{dayEvents.map((event, i) => (
<div
key={i}
className="border-b border-gray-700 border-opacity-20 last:border-0 pb-2 last:pb-0"
>
<div className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded-full ${
event.type === "event"
? "bg-blue-400"
: "bg-yellow-400"
}`}
></div>
<span className="font-medium">{event.title}</span>
</div>
{event.description && (
<p className="text-gray-300 mt-1 text-xs">
{event.description}
</p>
)}
</div>
))}
</div>
{/* Mũi tên nhọn HƯỚNG LÊN (chỉ vào ngày) */}
<div
className="absolute bottom-full left-1/2 -translate-x-1/2 -mb-1
w-0 h-0
border-l-8 border-l-transparent
border-r-8 border-r-transparent
border-b-8 border-b-gray-900"
></div>
</div>
)}
</div>
);
})}
</div>
{/* Legend */}
<div className="flex justify-center gap-6 mt-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
<span>Sự kiện</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<span>Đào tạo</span>
</div>
</div>
</div>
</>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -137,7 +137,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -269,7 +269,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -98,7 +98,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
import React from "react";
import ListCategory from "./components/list-category";
import Calendar from "./components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
import Image from "next/image";
export default function page() {
......@@ -202,7 +202,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -57,7 +57,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -154,7 +154,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import Calendar from "../components/Calendar";
import EventCalendar from "@/components/base/event-calendar";
export default function page() {
return (
<div className="bg-[#f6f6f6]">
<div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<div className="container m-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory />
</div>
......@@ -313,7 +313,7 @@ export default function page() {
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full ">
<Calendar />
<EventCalendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
......
......@@ -3,7 +3,7 @@ import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import EventCalendar from "@app/dai-dien-gioi-chu/components/event-calendar";
import EventCalendar from "@/components/base/event-calendar";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import { useGetNews } from "@api/endpoints/news";
......@@ -19,12 +19,12 @@ export default function Page() {
pageSize: String(pageSize),
currentPage: String(page),
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters:'category@=Cơ hội kinh doanh'
filters: 'category@=Cơ hội kinh doanh'
});
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
......
......@@ -5,7 +5,7 @@ import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import EventCalendar from "@app/dai-dien-gioi-chu/components/event-calendar";
import EventCalendar from "@/components/base/event-calendar";
import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
......@@ -19,7 +19,7 @@ export default function Page() {
pageSize: String(pageSize),
currentPage: String(page),
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters:'category@=Môi trường kinh doanh'
filters: 'category@=Môi trường kinh doanh'
});
return (
<div className="min-h-screen container mx-auto p-4">
......
"use client";
import React, { useState } from "react";
import { ArrowRight, ArrowLeft } from "lucide-react";
import { useState, useMemo } from "react";
import {
format,
startOfMonth,
endOfMonth,
eachDayOfInterval,
isSameMonth,
isSameDay,
addMonths,
subMonths,
} from "date-fns";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { useGetEvents } from "@/api/endpoints/event";
import { EventApiResponse } from "@/api/types/event";
export default function EventCalendar() {
const mockCalendar = {
month: 10,
year: 2025,
highlighted: [6, 9, 12],
};
const [month, setMonth] = useState<number>(mockCalendar.month);
const [year, setYear] = useState<number>(mockCalendar.year);
const [currentMonth, setCurrentMonth] = useState(new Date());
const today = new Date();
// Fetch event data from API
const { data } = useGetEvents<EventApiResponse>();
const events = data?.responseData.rows ?? [];
// Calculate the days to display in the current month grid
const days = useMemo(() => {
const start = startOfMonth(currentMonth);
const end = endOfMonth(currentMonth);
const firstDayOfWeek = start.getDay(); // Sunday = 0
const gridStart = new Date(start);
gridStart.setDate(start.getDate() - firstDayOfWeek);
const result: Date[] = [];
for (let i = 0; i < 42; i++) {
const day = new Date(gridStart);
day.setDate(gridStart.getDate() + i);
result.push(day);
}
return result;
}, [currentMonth]);
// Helper: get all events for a specific day
const getEventForDay = (date: Date) =>
events.filter((e) => isSameDay(new Date(e.start_time), date));
// Month title formatting (Vietnamese)
const formatMonthTitle = () => `THÁNG ${format(currentMonth, "M/yyyy")}`.toUpperCase();
return (
<div className="bg-white border rounded-md p-4">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-medium">
THÁNG {month}/{year}
</div>
<div className="flex items-center gap-2">
<div className="group">
<button
aria-label="Tháng trước"
onClick={() => {
// prev month
if (month === 1) {
setMonth(12);
setYear((y) => y - 1);
} else {
setMonth((m) => m - 1);
}
}}
className="group-hover:border-primary p-1 h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636] "
>
<ArrowLeft
size={24}
className="group-hover:text-muted-foreground text-[#363636]"
/>
</button>
</div>
<div className="group">
<button
aria-label="Tháng sau"
onClick={() => {
// next month
if (month === 12) {
setMonth(1);
setYear((y) => y + 1);
} else {
setMonth((m) => m + 1);
}
}}
className="p-1 group-hover:border-primary h-10 w-10 flex items-center justify-center rounded-full border-2 border-[#363636]"
>
<ArrowRight
size={24}
className="group-hover:text-muted-foreground"
/>
</button>
</div>
<div className="w-full mx-auto bg-white rounded-lg p-4">
{/* ===== HEADER ===== */}
<div className="flex items-center justify-between mb-4 px-3">
<h2 className="text-[15px] font-bold text-[#063E8E]">{formatMonthTitle()}</h2>
{/* Month navigation buttons */}
<div className="flex gap-3">
<button
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
className="p-2 rounded-full group border border-[#363636] hover:border-[#063e8e] transition"
>
<ArrowLeft className="group-hover:text-[#e8c518] text-[#363636] w-5 h-5" />
</button>
<button
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
className="p-2 rounded-full group border border-[#363636] hover:border-[#063e8e] transition"
>
<ArrowRight className="group-hover:text-[#e8c518] text-[#363636] w-5 h-5" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-1 text-center text-xs">
{["CN", "T2", "T3", "T4", "T5", "T6", "T7"].map((d) => (
<div key={d} className="text-gray-400 py-1">
{d}
{/* ===== DAYS OF WEEK ===== */}
<div className="grid grid-cols-7 text-center text-sm font-medium mb-1">
{["CN", "T2", "T3", "T4", "T5", "T6", "T7"].map((day) => (
<div key={day} className="py-2 text-[15px] text-[#063E8E]">
{day}
</div>
))}
{
(() => {
const totalDays = new Date(year, month, 0).getDate()
const firstDayIndex = new Date(year, month - 1, 1).getDay() // 0 (Sun) - 6 (Sat)
// previous month total days
const prevMonthTotalDays = new Date(year, month - 1, 0).getDate()
const totalCells = firstDayIndex + totalDays
const trailingCount = (7 - (totalCells % 7)) % 7
return (
<>
{Array.from({ length: firstDayIndex }).map((_, i) => {
const dayNum = prevMonthTotalDays - (firstDayIndex - 1) + i
return (
<div key={`prev-${i}`} className="py-2 text-sm text-gray-300">
{dayNum}
</div>
)
})}
{Array.from({ length: totalDays }, (_, i) => i + 1).map((day) => {
const isHighlighted = mockCalendar.highlighted.includes(day)
return (
<div
key={day}
className={`py-2 rounded-full w-10 h-10 flex flex-col justify-center items-center text-sm ${
isHighlighted ? 'bg-yellow-500 text-white' : 'text-gray-700'
}`}
>
{day}
</div>
)
})}
{Array.from({ length: trailingCount }).map((_, i) => (
<div key={`next-${i}`} className="py-2 text-sm text-gray-300">
{i + 1}
</div>
{/* ===== CALENDAR GRID ===== */}
<div className="grid grid-cols-7 gap-1 text-sm">
{days.map((day, idx) => {
const dayEvents = getEventForDay(day);
const isCurrentMonth = isSameMonth(day, currentMonth);
const isToday = isSameDay(day, today);
// Example condition: color days that have "B2B" events
const hasB2BEvent = dayEvents.some((e) => e.type === "B2B");
return (
<div
key={idx}
className={`
relative group aspect-square flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-gray-400" : ""}
${hasB2BEvent ? "bg-blue-600 text-white" : "text-[#333333]"}
${isToday ? "text-red-600 font-bold" : ""}
transition
`}
>
{/* Day number */}
<span className="relative z-10">{format(day, "d")}</span>
{/* Tooltip showing event details on hover */}
{dayEvents.length > 0 && (
<div
className="absolute top-full left-1/2 -translate-x-1/2 mt-2
w-64 p-3 bg-gray-900 text-white text-xs rounded-lg
shadow-xl opacity-0 pointer-events-none
group-hover:opacity-90 transition-opacity z-50"
>
<div className="space-y-2">
{dayEvents.map((event, i) => (
<div
key={i}
className="border-b border-gray-700 border-opacity-20 last:border-0 pb-2 last:pb-0"
>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-400" />
<span className="font-medium">{event.name}</span>
</div>
</div>
))}
</div>
))}
</>
)
})()
}
{/* Tooltip arrow */}
<div
className="absolute bottom-full left-1/2 -translate-x-1/2 -mb-1
w-0 h-0
border-l-8 border-l-transparent
border-r-8 border-r-transparent
border-b-8 border-b-gray-900"
/>
</div>
)}
</div>
);
})}
</div>
{/* ===== LEGEND ===== */}
<div className="flex justify-center gap-6 mt-4 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-600 rounded-full" />
<span>Sự kiện</span>
</div>
</div>
</div>
);
......
import { FC, JSX } from 'react';
import htmlParse, { DOMNode, Element, Text } from 'html-react-parser';
import { AppEditorContentProps } from './AppEditorContent.type';
import './AppEditorContent.css';
import { FC, JSX } from "react";
import htmlParse, { DOMNode, Element, Text } from "html-react-parser";
import { AppEditorContentProps } from "./AppEditorContent.type";
import "./AppEditorContent.css";
const AppEditorContent: FC<AppEditorContentProps> = ({ value = '', className = '' }) => {
const transform = (node: DOMNode): JSX.Element | string | undefined | null => {
......@@ -49,7 +49,9 @@ const AppEditorContent: FC<AppEditorContentProps> = ({ value = '', className = '
return (
<div className="jodit-container app-editor-container">
<div className={`jodit-wysiwyg ${className}`}>{htmlParse(value, { replace: transform })}</div>
<div className={`jodit-wysiwyg ${className}`}>
{htmlParse(value, { replace: transform })}
</div>
</div>
);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment