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

[tag]0.1-vcci

parents 5e763d67 5ca290c8
Pipeline #43367 passed with stages
in 47 minutes and 8 seconds
...@@ -79,7 +79,8 @@ const orvalConfig = async () => { ...@@ -79,7 +79,8 @@ const orvalConfig = async () => {
'UserHistory', 'UserHistory',
'Approvals', 'Approvals',
'News', 'News',
'Category' 'Category',
'NewsPageConfig',
] ]
} }
} }
......
...@@ -134,6 +134,9 @@ importers: ...@@ -134,6 +134,9 @@ importers:
react: react:
specifier: 19.2.0 specifier: 19.2.0
version: 19.2.0 version: 19.2.0
react-country-flag:
specifier: ^3.1.0
version: 3.1.0(react@19.2.0)
react-day-picker: react-day-picker:
specifier: ^9.11.1 specifier: ^9.11.1
version: 9.11.1(react@19.2.0) version: 9.11.1(react@19.2.0)
...@@ -3270,6 +3273,12 @@ packages: ...@@ -3270,6 +3273,12 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-country-flag@3.1.0:
resolution: {integrity: sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16'
react-day-picker@9.11.1: react-day-picker@9.11.1:
resolution: {integrity: sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==} resolution: {integrity: sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==}
engines: {node: '>=18'} engines: {node: '>=18'}
...@@ -7246,6 +7255,10 @@ snapshots: ...@@ -7246,6 +7255,10 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-country-flag@3.1.0(react@19.2.0):
dependencies:
react: 19.2.0
react-day-picker@9.11.1(react@19.2.0): react-day-picker@9.11.1(react@19.2.0):
dependencies: dependencies:
'@date-fns/tz': 1.4.1 '@date-fns/tz': 1.4.1
......
This diff is collapsed.
...@@ -140,6 +140,7 @@ export * from './putEventsLinkParams'; ...@@ -140,6 +140,7 @@ export * from './putEventsLinkParams';
export * from './putEventsParams'; export * from './putEventsParams';
export * from './putFooterParams'; export * from './putFooterParams';
export * from './putMembershipFeeParams'; export * from './putMembershipFeeParams';
export * from './putNewsPageConfigCategoryIdBody';
export * from './putNotificationsMarkAsReadParams'; export * from './putNotificationsMarkAsReadParams';
export * from './putNotificationsParams'; export * from './putNotificationsParams';
export * from './putOrderPaymentParams'; export * from './putOrderPaymentParams';
......
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VCCI
* Coded by Meu TEAM
* OpenAPI spec version: 1.0.0
*/
export type PutNewsPageConfigCategoryIdBody = {
category_ids?: string[];
};
import { ResponseType } from '@lib/types/common'
export type EventStatus = {
id: string;
name: string;
name_en: string;
code: string;
};
export type EventOrganization = {
id: string;
organization_id: string | null;
event_id: string;
status: string | null;
created_at: string;
updated_at: string | null;
created_by: string;
updated_by: string | null;
role: string;
guest_name: string;
guest_image: string;
org_table_count: number | null;
org_counter_count: number | null;
add_info: unknown | null;
organization: unknown | null;
};
// useGetEventsId
export type GetEventsIdQueryResponseType = ResponseType<{
accept_entries: boolean | null
counter_cost: number
counter_count: number
created_at: string
description: string
end_time: string
event_organizations: Array<{
add_info: string | null
created_at: string
guest_image: string | null
guest_name: string | null
id: string
org_counter_count: number | null
org_table_count: number | null
organization: {
address: string
avatar: string | null
club_link: string | null
club_name: string | null
id: string
name: string
org_categories: string[]
org_link: string | null
org_status_id: string | null
organization_products: Array<{
id: string
images: string[]
}>
province: string[]
tax_code: string
users: Array<{
id: string
}>
website: string
} | null
role: 'PARTAKER' | 'MAIN' | 'SUPPORT' | 'SUPPORT_1' | 'SUPPORT_2' | 'SUPPORT_3' | 'GUEST'
status: string | null
}>
host_club: string | null
id: string
image: string
introduction: string | null
location: string
name: string
org_support_titles: string[] | null
province: string
seo_text: string
seo_text_en: string | null
start_time: string
status: string
status_status: {
code: string
id: string
name: string
name_en: string
}
table_cost: number
table_count: number
updated_at: string | null
}>
export type EventItem = {
id: string;
name: string;
start_time: string;
end_time: string;
created_at: string;
created_by: string;
updated_at: string | null;
updated_by: string | null;
status: string;
image: string;
description: string;
location: string;
province: string;
table_count: number;
counter_count: number;
seo_text: string;
seo_text_en: string | null;
table_cost: number;
counter_cost: number;
table_min_pick: number | null;
counter_min_pick: number | null;
table_max_pick: number | null;
counter_max_pick: number | null;
org_support_titles: string[];
accept_entries: boolean;
type: string;
introduction: string;
host_club: string | null;
config: unknown | null;
event_organizations: EventOrganization[];
status_status: EventStatus;
};
export type EventResponseData = {
count: number;
rows: EventItem[];
totalPages: number;
currentPage: number;
};
export type EventApiResponse = {
message: string;
message_en: string;
responseData: EventResponseData;
status: string;
timeStamp: string;
violations: null | unknown;
};
...@@ -12,6 +12,9 @@ import { GetNewsDetailResponseType } from './page.type'; ...@@ -12,6 +12,9 @@ import { GetNewsDetailResponseType } from './page.type';
import { Link, CalendarFold, Book } from 'lucide-react'; import { Link, CalendarFold, Book } from 'lucide-react';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useParams } from 'next/navigation' 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 { t } from 'i18next' // import { t } from 'i18next'
// Component // Component
...@@ -33,82 +36,34 @@ const NewsDetailPage = () => { ...@@ -33,82 +36,34 @@ const NewsDetailPage = () => {
<Spinner /> <Spinner />
) : ( ) : (
<div> <div>
{/* Banner */} <div className='container flex flex-col gap-5'>
<img className='w-full h-100' src={`${BASE_URLS.imageEndpoint}${data?.responseData?.thumbnail}`} alt='Banner' /> <ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
{/* Breadcrumb */} <div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div className='container py-10'> {/* Main content */}
<div className='flex gap-4 items-stretch'> <main className="lg:col-span-2 bg-white border rounded-md p-7">
{/* <Breadcrumbs aria-label='breadcrumb'> <div className='pb-5 text-primary text-2xl leading-normal font-medium'>
<Link {data?.responseData?.title}
underline='hover'
color='inherit'
href='/'
className='!text-app-blue-secondary !text-sm !leading-normal'
>
{t('breadcrumbHomePage')}
<p>Trang chủ</p>
</Link>
<Link
underline='hover'
color='inherit'
href='/tin-tuc'
className='!text-app-blue-secondary !text-sm !leading-normal'
>
{t('breadcrumbNewsPage')}
<p>Tin tức</p>
</Link>
<Typography className='!text-sm !text-black !leading-normal'>
{t('breadcrumbNewsDetailPage')}
<p>Chi tiết</p>
</Typography>
</Breadcrumbs> */}
</div>
</div>
<div className='container bg-gray-100 rounded p-10 flex flex-col gap-10'>
{/* Heading */}
<div className='text-blue-900 text-[32px] leading-normal font-medium text-center'>{data?.responseData?.title}</div>
{/* body */}
<div className='flex items-start gap-8 flex-col lg:flex-row'>
{/* Info */}
<div className='lg:w-[332px] bg-white p-4 rounded shadow-app-quaternary flex flex-col gap-2'>
<div className='text-base text-app-grey font-semibold leading-normal text-center'>
{/* {t('information')} */}
</div> </div>
<div className='flex items-center gap-2 text-sm mb-4'>
{/* date */}
<div className='flex items-center gap-2 text-app-grey'>
<CalendarFold /> <CalendarFold />
<span className='text-base'>{dayjs(data?.responseData?.created_at).format('DD/MM/YYYY')}</span> <span className='text-base text-blue-700'>{dayjs(data?.responseData?.created_at).format('DD/MM/YYYY')}</span>
</div> </div>
<div className='py-5' >
{/* author */} <hr />
{/* <div className='flex items-center gap-2 text-app-grey'>
<PersonIcon />
<span className='text-base'>{data?.responseData?.created_by}</span>
</div> */}
{/* Category */}
<div className='flex items-center gap-2 text-app-grey'>
<Book />
<span className='text-base'>{data?.responseData?.category}</span>
</div> </div>
</div> <div className='flex-1 text-app-grey text-base overflow-hidden'>
<AppEditorContent value={data?.responseData?.description ?? ''} />
{/* Content */} </div>
<div className='flex-1 text-app-grey text-base overflow-hidden'> </main>
<AppEditorContent value={data?.responseData?.description ?? ''} /> {/* Sidebar */}
</div> <aside className="space-y-6">
<EventCalendar />
</aside>
</div> </div>
</div> </div>
{/* Related News */}
{/* <RelatedNews newsQuery={getRelatedNewsQuery} lang={lang} newsId={infoNews.id} /> */}
</div> </div>
)} )}
</div> </div >
) )
} }
......
import { EventItem } from '@/api/types/event'
import BASE_URL from '@/links'
import dayjs from 'dayjs';
import AppEditorContent from '@/components/shared/editor-content';
function CardEvent({ event }: { event: EventItem }) {
return (
<a
href={`${event.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'
>
<img
src={`${BASE_URL.imageEndpoint}${event.image}`}
alt={event.name}
className='w-[100px] md:w-[130px] aspect-3/2 object-cover'
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/>
<div className='flex-1'>
<p className='text-[#0056b3] font-bold text-sm 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-2' value={event.description} /> */}
</div>
</a>
);
}
export default CardEvent;
\ No newline at end of file
...@@ -7,13 +7,18 @@ function CardNews({ news }: { news: NewsAdminItem }) { ...@@ -7,13 +7,18 @@ function CardNews({ news }: { news: NewsAdminItem }) {
return ( return (
<a <a
href={`${news.id}`} href={`${news.id}`}
className='flex flex-row gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3' 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'
> >
<img <img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title} alt={news.title}
className='w-[120px] h-20 object-cover rounded-sm' className="w-[100px] md:w-[130px] aspect-3/2 object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/> />
<div className='flex-1'> <div className='flex-1'>
<p className='text-[#0056b3] font-bold text-sm line-clamp-2'> <p className='text-[#0056b3] font-bold text-sm line-clamp-2'>
{news.title} {news.title}
......
This diff is collapsed.
const MenuItem = ({ title, items, link }: { title: string; items: string[]; link?: string }) => ( import Link from "next/link";
<div className="group relative">
<a type MenuItemProps = {
className="px-3 py-5 text-[16px] font-[600] text-[#124588] hover:text-[#E8C518] transition block" title: string;
href={`${link}`} link?: string;
> items: { title: string; link: string }[];
{title} };
</a>
<div className="absolute left-0 top-full hidden group-hover:block bg-[#124588]/98 text-white text-[14px] font-[500] min-w-[220px] shadow-lg"> const MenuItem = ({ title, link, items }: MenuItemProps) => {
{items.map((item, i) => ( return (
<div <div className="group relative">
key={i} <Link
className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer whitespace-nowrap" href={`/${link || ""}`}
> className="px-3 py-5 text-[16px] font-[600] text-[#124588] hover:text-[#E8C518] transition block"
{item} >
</div> {title}
))} </Link>
{/* Dropdown */}
<div className="absolute left-0 top-full hidden group-hover:block bg-[#124588]/98 text-white text-[14px] font-[500] min-w-[220px] shadow-lg">
{items.map((item, i) => (
<Link
key={i}
href={`/${link}/${item.link}`}
className="block px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer whitespace-nowrap transition"
>
{item.title}
</Link>
))}
</div>
</div> </div>
</div> );
); };
export default MenuItem; export default MenuItem;
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
import Image from "next/image"; import Image from "next/image";
import vietnamMap from "@/assets/vietnam-map-white.png.webp"; import vietnamMap from "@/assets/vietnam-map-white.png.webp";
function footer() { function Footer() {
const emailRef = useRef<HTMLInputElement>(null); const emailRef = useRef<HTMLInputElement>(null);
const checkBoxRef = useRef<HTMLInputElement>(null); const checkBoxRef = useRef<HTMLInputElement>(null);
const [emailError, setEmailError] = useState(false); const [emailError, setEmailError] = useState(false);
...@@ -212,4 +212,4 @@ function footer() { ...@@ -212,4 +212,4 @@ function footer() {
); );
} }
export default footer; export default Footer;
This diff is collapsed.
// Core
"use client";
import Image from "next/image";
import ListCategory from "../../components/list-category";
import { OWNER_REPRESENTATIVES_CATEGORIES } from "@constants/categories";
import ListFilter from "../../components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -8,15 +8,18 @@ import { Pagination } from "@components/base/pagination"; ...@@ -8,15 +8,18 @@ import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData,isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, // filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters:'category@=Chủ đề'
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
...@@ -27,26 +30,35 @@ export default function Page() { ...@@ -27,26 +30,35 @@ export default function Page() {
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải dữ liệu...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} link={`${PATHS.ownerRepresentatives}/chu-de/${news.id}`}/>
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => onGoToNextPage={() =>
setPage( setPage(
Math.min( Math.min(
Number(allData?.responseData.totalPages ?? 1), Number(allData?.responseData.totalPages ?? 1),
page + 1 page + 1
) )
) )
} }
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
...@@ -2,17 +2,33 @@ ...@@ -2,17 +2,33 @@
import { NewsItem } from '@app/dai-dien-gioi-chu/lib/types/NewsPage.type'; import { NewsItem } from '@app/dai-dien-gioi-chu/lib/types/NewsPage.type';
import Links from '@links/index' import Links from '@links/index'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import parse from 'html-react-parser' import {EventItem} from "@api/types/event";
function NewsContent({ news }: { news: NewsItem }) { // Helper: remove <img> tags and extract plain text from HTML
const stripImagesAndHtml = (html?: string) => {
if (!html) return ''
// remove img tags first
const withoutImgs = html.replace(/<img[^>]*>/gi, '')
// use DOMParser on client for robust extraction
if (typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
try {
const doc = new DOMParser().parseFromString(withoutImgs, 'text/html')
return doc.body.textContent || ''
} catch {
// fallback to regex
}
}
return withoutImgs.replace(/<[^>]*>/g, '')
}
function NewsContent({ news ,link,event}: { news?: NewsItem ,link:string,event?:EventItem}) {
return ( return (
<a <a
href={`/tin-tuc/${news.id}`} href={`${link}`}
className="flex flex-col hover:no-underline sm:flex-row gap-2 mb-6 bg-white rounded-lg shadow-sm p-4 border items-start min-w-0" className="flex flex-col hover:no-underline sm:flex-row gap-2 mb-6 bg-white rounded-lg shadow-sm p-4 border items-start min-w-0"
> >
<img <img
src={`${Links.imageEndpoint}${news.thumbnail}`} src={`${Links.imageEndpoint}${news?news.thumbnail:event?.image}`}
alt={news.title} alt={news?news.title:event?.name}
className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0" className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0"
onError={(e) => { onError={(e) => {
e.currentTarget.src = "/img-error.png" e.currentTarget.src = "/img-error.png"
...@@ -21,14 +37,14 @@ function NewsContent({ news }: { news: NewsItem }) { ...@@ -21,14 +37,14 @@ function NewsContent({ news }: { news: NewsItem }) {
/> />
<div className="flex-1 min-w-0 pl-0 sm:pl-4"> <div className="flex-1 min-w-0 pl-0 sm:pl-4">
<p className="text-primary font-semibold text-base md:text-lg hover:underline line-clamp-2 wrap-break-word hover:no-underline"> <p className="text-primary font-semibold text-base md:text-lg hover:underline line-clamp-2 wrap-break-word">
{news.title} {news?.title}{event?.name}
</p> </p>
<div className="text-sm my-2 text-[#00AED5]">{dayjs(news.release_at).format('DD/MM/YYYY')}</div> <div className="text-sm my-2 text-[#00AED5]">{dayjs(news?news?.created_at:event?.created_at).format('DD/MM/YYYY')}</div>
<div className="text-sm text-[#777] line-clamp-3"> <div className="text-sm text-[#777] line-clamp-3">
<div className="text-sm prose tiptap">{parse(news.description)}</div> <div className="text-sm prose tiptap">{stripImagesAndHtml(news?news.description:event?.description)}</div>
</div> </div>
</div> </div>
</a> </a>
......
export const SAMPLE_HTML = ` export const SAMPLE_HTML = `
<div class="document"> <div class="document">
<h1 style="font-size:18px; font-weight:700; margin-bottom:8px;">Chức năng Đại diện Người sử dụng lao động</h1> <h1 class="text-primary" style="font-size:20px; font-weight:700; margin-bottom:12px;">Chức năng Đại diện Người sử dụng lao động</h1>
<p>Chức năng Đại diện Người sử dụng lao động (NSDLĐ):</p> <p>Chức năng Đại diện Người sử dụng lao động (NSDLĐ):</p>
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "../../components/list-category";
import { OWNER_REPRESENTATIVES_CATEGORIES } from "@constants/categories";
import ListFilter from "../../components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -8,12 +8,14 @@ import { Pagination } from "@components/base/pagination"; ...@@ -8,12 +8,14 @@ import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch}` : undefined,
...@@ -21,32 +23,41 @@ export default function Page() { ...@@ -21,32 +23,41 @@ export default function Page() {
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} /> <ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải tập huấn NSDLĐ...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} link={`${PATHS.ownerRepresentatives}/tap-huan-nsdld/${news.id}`} />
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => onGoToNextPage={() =>
setPage( setPage(
Math.min( Math.min(
Number(allData?.responseData.totalPages ?? 1), Number(allData?.responseData.totalPages ?? 1),
page + 1 page + 1
) )
) )
} }
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "../../components/list-category";
import { OWNER_REPRESENTATIVES_CATEGORIES } from "@constants/categories";
import ListFilter from "../../components/list-filter";
import { useGetNewsId } from "@/api/endpoints/news";
import parse from "html-react-parser";
import { useParams } from "next/navigation";
import { GetNewsDetailResponseType } from "@lib/types/news-detail-response-data";
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams();
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(
id as string
);
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className="pb-5 text-primary text-2xl leading-normal font-medium">
{data?.responseData?.title}
</div>
<hr className="py-2" />
<div className="p-7.5 prose tiptap overflow-hidden">
{parse(data?.responseData?.description ?? "")}
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -8,15 +8,17 @@ import { Pagination} from "@components/base/pagination"; ...@@ -8,15 +8,17 @@ import { Pagination} from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch,setsubmitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch},category@=Tin liên quan` : 'category@=Tin liên quan',
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
...@@ -27,25 +29,34 @@ export default function Page() { ...@@ -27,25 +29,34 @@ export default function Page() {
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải tin liên quan...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} link={`${PATHS.ownerRepresentatives}/tin-lien-quan/${news.id}`} />
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))} onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))}
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
{/* Sidebar */} {/* Sidebar */}
<aside className="space-y-6 order-first lg:order-last"> <aside className="space-y-6 order-first lg:order-last">
<ListFilter /> <ListFilter onSearch={setsubmitSearch}/>
<div className="bg-white border rounded-md overflow-hidden hidden lg:block"> <div className="bg-white border rounded-md overflow-hidden hidden lg:block">
<div className="w-full h-56 relative bg-gray-100"> <div className="w-full h-56 relative bg-gray-100">
......
This diff is collapsed.
"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'
import { useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import 'swiper/css'
export default function ImageGallery({ images }: { images: string[] }) {
const [activeIndex, setActiveIndex] = useState(0)
const [lightboxOpen, setLightboxOpen] = useState(false)
return (
<div className="w-full max-w-4xl mx-auto">
{/* Ảnh lớn */}
<div
className="w-full mb-4 overflow-hidden rounded-lg shadow-md cursor-zoom-in"
onClick={() => setLightboxOpen(true)}
>
<img
src={images[activeIndex]}
alt={`Image ${activeIndex + 1}`}
className="w-full h-full object-cover transition-transform duration-300"
/>
</div>
{/* Slider ảnh nhỏ */}
<Swiper spaceBetween={10} slidesPerView={4} className="cursor-pointer">
{images.map((img, index) => (
<SwiperSlide key={index} onClick={() => setActiveIndex(index)}>
<img
src={img}
alt={`Thumbnail ${index + 1}`}
className={`w-full object-cover rounded-lg border-2 transition-all duration-300
${activeIndex === index
? 'border-blue-500 filter brightness-100'
: 'border-transparent filter brightness-50 hover:brightness-75'
}`}
/>
</SwiperSlide>
))}
</Swiper>
{/* Lightbox */}
{lightboxOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 cursor-zoom-out"
onClick={() => setLightboxOpen(false)}
>
<img
src={images[activeIndex]}
alt={`Image ${activeIndex + 1}`}
className="max-h-full max-w-full object-contain"
/>
</div>
)}
</div>
)
}
"use client" "use client"
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import React from "react" import React from "react"
type Category = { type Category = {
title: string title: string
href: string href: string
} }
const CATEGORIES: Category[] = [ const CATEGORIES: Category[] = [
{ title: "Về VCCI-HCM", href: "/gioi-thieu" }, { title: "Về VCCI-HCM", href: "/gioi-thieu" },
{ title: "Chức năng và Nhiệm vụ", href: "/gioi-thieu/chuc-nang" }, { title: "Chức năng và nhiệm vụ", href: "/gioi-thieu/chuc-nang-nhiem-vu" },
{ title: "Sơ đồ Tổ chức", href: "/gioi-thieu/so-do" }, { title: "Sơ đồ tổ chức", href: "/gioi-thieu/so-do-to-chuc" },
{ title: "Dịch vụ cung cấp", href: "/gioi-thieu/dich-vu" }, { title: "Dịch vụ cung cấp", href: "/gioi-thieu/dich-vu-cung-cap" },
] ]
const ListCategory: React.FC = () => { const ListCategory: React.FC = () => {
const pathname = usePathname() || "" const pathname = usePathname() || ""
const router = useRouter()
const isActive = (href: string) => {
// treat the base path as active for nested routes as well const isActive = (href: string) => {
if (href === "/gioi-thieu") return pathname === href || pathname.startsWith(href + "/") if (href === "/gioi-thieu") return pathname === "/gioi-thieu"
return pathname === href return pathname === href
} }
return ( const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
<div className="border-t border-gray-200 bg-white p-2.5"> router.push(e.target.value)
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> }
<nav aria-label="Danh mục" className="py-3">
<ul className="flex gap-8 items-center"> return (
{CATEGORIES.map((c) => { <div className="border-t border-gray-200 bg-white p-2.5">
const active = isActive(c.href) <div className="max-w-7xl mx-auto">
return ( <nav aria-label="Danh mục" className="py-3">
<li key={c.href}> {/* --- Desktop view --- */}
<Link <ul className="hidden sm:flex items-center">
href={c.href} {CATEGORIES.map((c) => {
className={ const active = isActive(c.href)
"text-sm font-medium py-3.5 px-5 transition-colors duration-150 " + return (
(active <li key={c.href}>
? "text-yellow-500 font-semibold decoration-yellow-300 " <Link
: "text-gray-600 hover:text-gray-800 hover:underline") href={c.href}
} className={
> "text-sm font-bold py-3.5 px-5 transition-colors duration-150 " +
{c.title} (active
</Link> ? "text-yellow-500 font-semibold decoration-yellow-300"
</li> : "text-gray-600 hover:text-yellow-500")
) }
})} >
</ul> {c.title}
</nav> </Link>
</div> </li>
</div> )
) })}
</ul>
{/* --- Mobile view (Dropdown) --- */}
<div className="sm:hidden">
<select
value={pathname}
onChange={handleChange}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400"
>
{CATEGORIES.map((c) => (
<option key={c.href} value={c.href}>
{c.title}
</option>
))}
</select>
</div>
</nav>
</div>
</div>
)
} }
export default ListCategory export default ListCategory
\ No newline at end of file
"use client"
import React, { useState } from 'react'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
type Category = { id: string; title: string; count: number }
const DEFAULT_CATEGORIES: Category[] = [
{ id: 'ceo', title: 'CEO', count: 4 },
{ id: 'policy', title: 'Hỏi đáp về chính sách', count: 0 },
{ id: 'biz', title: 'Tin Doanh Nghiệp', count: 9 },
{ id: 'member', title: 'Tin Hội Viên', count: 17 },
{ id: 'law', title: 'Văn bản Pháp luật sắp có hiệu lực', count: 30 }
]
export const ListFilter: React.FC<{
categories?: Category[]
onSearch?: (q: string) => void
onReset?: () => void
}> = ({ categories = DEFAULT_CATEGORIES, onSearch, onReset }) => {
const [query, setQuery] = useState('')
const [selected, setSelected] = useState<Record<string, boolean>>(() => {
const map: Record<string, boolean> = {}
categories.forEach((c) => (map[c.id] = false))
return map
})
const toggle = (id: string) => setSelected((s) => ({ ...s, [id]: !s[id] }))
return (
<aside className="p-6 bg-white border rounded-md">
<h3 className="text-lg font-semibold mb-3">Tìm kiếm</h3>
<div className="mb-4">
<Input
placeholder="Tên văn bản ..."
value={query}
className='text-black placeholder:text-gray-400 rounded-none py-2.5 px-2'
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onSearch?.(query)
}
}}
/>
</div>
{/* <div className="flex flex-col gap-3 mb-6">
{categories.map((c) => (
<label key={c.id} className="flex items-center gap-3">
<Checkbox checked={!!selected[c.id]} onCheckedChange={() => toggle(c.id)} />
<div className="flex justify-between w-full items-center">
<span className="text-sm">{c.title}</span>
<span className="text-sm text-gray-400">({c.count})</span>
</div>
</label>
))}
</div> */}
<div className="flex gap-3">
<Button className="flex-1 rounded-none font-medium text-lg text-white hover:bg-muted-foreground hover:outline-1 outline-primary hover:text-primary" onClick={() => onSearch?.(query)}>
Tìm kiếm
</Button>
<Button
className="flex-1 rounded-none font-medium text-lg text-white hover:bg-muted-foreground hover:outline-1 outline-primary hover:text-primary"
onClick={() => {
setQuery('')
// restore initial map
const map: Record<string, boolean> = {}
categories.forEach((c) => (map[c.id] = false))
setSelected(map)
onReset?.()
}}
>
Bỏ tìm
</Button>
</div>
</aside>
)
}
export default ListFilter
'use client'
import React from "react";
import ListCategory from "../components/list-category";
import EventCalendar from "../components/event-calendar";
const Page = () => {
return (
<div className="min-h-screen container mx-auto pb-4">
<div className="w-full flex flex-col gap-5">
<ListCategory />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-7">
<h1 className="text-2xl font-bold text-[#153e8e]">Dịch vụ cung cấp</h1>
<hr className="my-5" />
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/1-7.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Tổ chức sự kiện, hội nghị, hội thảo, giao lưu thương mại, hội chợ, triển lãm</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/3-4-150x145.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Đào tạo nâng cao năng lực quản trị doanh nghiệp</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/5-1.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Khảo sát thị trường nước ngoài</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/7-150x147.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Cho thuê văn phòng, hội trường</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/8.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Quảng cáo, truyền thông</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/2-6.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Tư vấn về pháp lý, quan hệ lao động, môi trường kinh doanh</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/4-1.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Cấp C/O và xác nhận các chứng từ thương mại</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/6-5.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Cung cấp thông tin thị trường và hồ sơ doanh nghiệp</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/9.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Thu xếp visa nhập cảnh</p>
</div>
<div className="flex items-center">
<img src="/gioi-thieu/dich-vu-cung-cap/10.webp" alt="Thông tin" className="w-12 px-2 py-2" />
<p>Biên phiên dịch</p>
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
<img src="/home/eCarAid_web_banner_600x400.webp" alt="banner" />
</aside>
</div >
</div >
</div >
);
};
export default Page;
\ No newline at end of file
'use client'
import React from "react";
import ListCategory from "./components/list-category";
import EventCalendar from "./components/event-calendar";
import ImageGallery from "./components/image-gallery";
const Page = () => {
const images = [
'/gioi-thieu/VCCI-HCM-BROCHURE-2020_Tieng-Viet-1-scaled.webp',
'/gioi-thieu/VCCI-HCM-BROCHURE-2020_Tieng-Viet-2-scaled.webp',
'/gioi-thieu/VCCI-HCM-BROCHURE-2020_Tieng-Viet-3-scaled.webp',
'/gioi-thieu/VCCI-HCM-BROCHURE-2020_Tieng-Viet-4-scaled.webp',
]
return (
<div className="min-h-screen container mx-auto pb-4">
<div className="w-full flex flex-col gap-5">
<ListCategory />
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md py-10 px-5 md:px-10 xl:px-50 text-justify">
<h1 className="text-2xl font-bold text-[#153e8e]">Về VCCI-HCM</h1>
<hr className="my-5" />
<div className="flex flex-col justify-center items-center">
<p className="text-center text-[#063e8e] text-[14pt] font-bold pb-5">
GIỚI THIỆU CHUNG
</p>
<p className="pb-5">Liên đoàn Thương mại và Công nghiệp Việt Nam (VCCI) là tổ chức quốc gia tập hợp và đại diện cho cộng đồng doanh nghiệp, doanh nhân, người sử dụng lao động và các hiệp hội doanh nghiệp ở Việt Nam nhằm mục đích phát triển, bảo vệ và hỗ trợ cộng đồng doanh nghiệp, góp phần phát triển kinh tế - xã hội của đất nước, thúc đẩy các quan hệ hợp tác kinh tế, thương mại và khoa học - công nghệ với nước ngoài trên cơ sở bình đẳng và cùng có lợi, theo quy định của pháp luật.</p>
<p>Chi nhánh VCCI khu vực Thành phố Hồ Chí Minh (VCCI-HCM) là Chi nhánh lớn nhất, hoạt động trên địa bàn TP.HCM và 5 tỉnh thành phía Nam: Bình Dương, Bình Phước, Đồng Nai, Lâm Đồng, Tây Ninh.</p>
<img src="/gioi-thieu/MAPS_VCCI-HCM-Upload-1259x1536.jpg.webp" alt="map" />
<img src="/gioi-thieu/tam-nhin-1024x470.jpg.webp" alt="map" />
<p className="text-[14pt] pb-5">
<strong className="text-[#063e8e] font-sans">
BROCHURE VCCI-HCM
</strong>
</p>
<div className="pb-5">
<ImageGallery images={images} />
</div>
<p className="text-[14pt] pb-5">
<strong className="text-[#063e8e] font-sans">
VIDEO VỀ VCCI-HCM
</strong>
</p>
<iframe
width="808"
height="455"
src="https://www.youtube.com/embed/j9ao-9b6Jf0"
title="VCCI-HCM 2024 IN REVIEW"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen>
</iframe>
</div>
</main>
</div>
</div>
);
};
export default Page;
\ No newline at end of file
'use client'
import React from "react";
import ListCategory from "../components/list-category";
const Page = () => {
return (
<div className="min-h-screen container mx-auto pb-4">
<div className="w-full flex flex-col gap-5">
<ListCategory />
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md py-10 px-5 md:px-10 xl:px-50 text-justify">
<h1 className="text-2xl font-bold text-[#153e8e]">Về VCCI-HCM</h1>
<hr className="my-5" />
<img src="/gioi-thieu/so-do-to-chuc/2025-SO-DO-TO-CHUC-01-VN.jpg.webp" alt="Sơ đồ tổ chức VCCI-HCM" />
</main>
</div >
</div >
);
};
export default Page;
\ No newline at end of file
...@@ -11,7 +11,7 @@ export default function Page() { ...@@ -11,7 +11,7 @@ export default function Page() {
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <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"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -4,42 +4,53 @@ import ListCategory from "@app/dai-dien-gioi-chu/components/list-category"; ...@@ -4,42 +4,53 @@ import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories"; import { EVENT_CATEGORIES } from "@constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter"; import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news"; import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination} from "@components/base/pagination"; import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch}` : 'category @=Đào tạo',
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} /> <ListCategory categories={EVENT_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải khóa đào tạo...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} link={`${PATHS.event}/dao-tao/${news.id}`} />
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))} onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))}
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
...@@ -15,7 +15,7 @@ export default function Page() { ...@@ -15,7 +15,7 @@ export default function Page() {
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} /> <ListCategory categories={EVENT_CATEGORIES} />
</div> </div>
</div> </div>
); );
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import {useGetEventsId} from '@/api/endpoints/event';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
import {GetEventsIdQueryResponseType} from '@api/types/event';
import { Spinner } from "@components/ui/spinner";
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetEventsId<GetEventsIdQueryResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
{isLoading ? (
<div className="flex justify-center items-center py-12">
<Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải chi tiết sự kiện...</span>
</div>
) : (
<>
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.name}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</>
)}
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -5,43 +5,57 @@ import { EVENT_CATEGORIES } from "@constants/categories"; ...@@ -5,43 +5,57 @@ import { EVENT_CATEGORIES } from "@constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter"; import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news"; import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
// ...existing code... // ...existing code...
import { Pagination} from "@components/base/pagination"; import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetEvents } from '@api/endpoints/event'
import { EventApiResponse } from '@api/types/event'
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetEvents<EventApiResponse>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, sortField: 'start_time',
sortOrder: 'ASC',
filters: submitSearch ? `title @=${submitSearch},start_time>${new Date()}` : `start_time>${new Date()}`,
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} /> <ListCategory categories={EVENT_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải sự kiện...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((event) => (
<NewsContent key={event.id} event={event} link={`${PATHS.event}/su-kien/${event.id}`} />
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))} onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))}
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
...@@ -7,6 +7,7 @@ import { Pagination } from "@components/base/pagination"; ...@@ -7,6 +7,7 @@ import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
...@@ -16,7 +17,7 @@ export default function Page() { ...@@ -16,7 +17,7 @@ export default function Page() {
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch}` : `category @=Kết nối hội viên`,
}); });
return ( return (
<div className="min-h-screen container mx-auto pb-4"> <div className="min-h-screen container mx-auto pb-4">
...@@ -26,28 +27,37 @@ export default function Page() { ...@@ -26,28 +27,37 @@ export default function Page() {
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background"> <main className="lg:col-span-2 bg-background">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<CardNews key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải dữ liệu kết nối hội viên...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<CardNews key={news.id} news={news} />
))}
<div className='w-full flex justify-center mt-4'> <div className='w-full flex justify-center mt-4'>
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => onGoToNextPage={() =>
setPage( setPage(
Math.min( Math.min(
Number(allData?.responseData.totalPages ?? 1), Number(allData?.responseData.totalPages ?? 1),
page + 1 page + 1
) )
) )
} }
/> />
</div> </div>
</div> </>
</main> )}
</div>
</main>
{/* Sidebar */} {/* Sidebar */}
<aside className="space-y-6"> <aside className="space-y-6">
......
...@@ -7,6 +7,7 @@ import { Pagination } from '@components/base/pagination' ...@@ -7,6 +7,7 @@ import { Pagination } from '@components/base/pagination'
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from '@api/endpoints/news' import { useGetNews } from '@api/endpoints/news'
import { GetNewsResponseType } from '@api/types/NewsPage.type' import { GetNewsResponseType } from '@api/types/NewsPage.type'
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState('') const [submitSearch] = useState('')
...@@ -16,7 +17,7 @@ export default function Page() { ...@@ -16,7 +17,7 @@ export default function Page() {
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch}` : 'category @=Tin hội viên',
}) })
return ( return (
<div className="min-h-screen container mx-auto pb-4"> <div className="min-h-screen container mx-auto pb-4">
...@@ -26,26 +27,35 @@ export default function Page() { ...@@ -26,26 +27,35 @@ export default function Page() {
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background"> <main className="lg:col-span-2 bg-background">
<div className='pb-5 overflow-hidden'> <div className='pb-5 overflow-hidden'>
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<CardNews key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải tin hội viên...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<CardNews key={news.id} news={news} />
))}
<div className='w-full flex justify-center mt-4'> <div className='w-full flex justify-center mt-4'>
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => onGoToNextPage={() =>
setPage( setPage(
Math.min( Math.min(
Number(allData?.responseData.totalPages ?? 1), Number(allData?.responseData.totalPages ?? 1),
page + 1 page + 1
) )
) )
} }
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
"use client";
import React, { useState, Suspense } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { OWNER_REPRESENTATIVES_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { Spinner } from "@components/ui/spinner";
import { PATHS } from "@constants/paths";
import { useSearchParams } from 'next/navigation'
function SearchContent() {
const [page, setPage] = useState(1);
const searchParams = useSearchParams()
const query = searchParams.get('q') //
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: query ? `title @=${query}` : undefined,
});
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<div className="border-t border-gray-200 bg-white p-2.5">
<div className="w-full px-4 sm:px-6 lg:px-8">
<div className="py-3">
<h1 className="text-md md:text-lg font-semibold leading-6 text-gray-900">
{" "}
Search Results for: {query}
</h1>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{isLoading ? (
<div className="flex justify-center items-center py-12">
<Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tìm kiếm...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent
key={news.id}
news={news}
link={`${PATHS.mediaInformation}/tin-vcci/${news.id}`}
/>
))}
<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>
</main>
{/* Sidebar */}
<aside className="space-y-6 order-first lg:order-last">
<div className="bg-white border rounded-md overflow-hidden hidden lg:block">
<div className="w-full h-62 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
}
export default function Page() {
return (
<Suspense fallback={
<div className="min-h-screen container mx-auto p-4 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#063e8e] mx-auto"></div>
<p className="mt-4 text-gray-600">Loading search results...</p>
</div>
</div>
}>
<SearchContent />
</Suspense>
);
}
export type SiteMapItem = {
label: string;
url: string;
children?: SiteMapItem[];
};
export const MOCK_SITEMAP: SiteMapItem[] = [
{ label: "Trang chủ", url: "/homepage", children: [] },
{
label: "Giới thiệu",
url: "/gioi-thieu",
children: [
{ label: "Về VCCI-HCM", url: "/gioi-thieu/ve-vcci-hcm", children: [] },
{ label: "Chức năng và nhiệm vụ", url: "/gioi-thieu/chuc-nang-nhiem-vu", children: [] },
{ label: "Sơ đồ tổ chức", url: "/gioi-thieu/so-do-to-chuc", children: [] },
{ label: "Dịch vụ cung cấp", url: "/gioi-thieu/dich-vu-cung-cap", children: [] },
],
},
{
label: "Hội viên",
url: "/hoi-vien",
children: [
{ label: "Lợi ích của hội viên VCCI", url: "/hoi-vien/loi-ich-hoi-vien", children: [] },
{ label: "Đăng ký hội viên", url: "/hoi-vien/dang-ky-hoi-vien", children: [] },
{ label: "Kết nối hội viên", url: "/hoi-vien/ket-noi-hoi-vien", children: [] },
{ label: "Tin hội viên", url: "/hoi-vien/tin-hoi-vien", children: [] },
],
},
{
label: "Hoạt động",
url: "/hoat-dong",
children: [
{ label: "Sự kiện", url: "/hoat-dong/su-kien", children: [] },
{ label: "Đào tạo", url: "/hoat-dong/dao-tao", children: [] },
],
},
{
label: "Xuất xứ hàng hóa",
url: "/xuat-xu-hang-hoa",
children: [
{ label: "Định nghĩa chung", url: "/xuat-xu-hang-hoa", children: [] },
{ label: "Mục đích của C/O", url: "/xuat-xu-hang-hoa/muc-dich-co", children: [] },
{ label: "Luật áp dụng về C/O", url: "/xuat-xu-hang-hoa/luat-ap-dung-co", children: [] },
{ label: "Thủ tục cấp C/O", url: "/xuat-xu-hang-hoa/thu-tuc-cap-co", children: [] },
{ label: "Biểu mẫu C/O và cách khai", url: "/xuat-xu-hang-hoa/bieu-mau-co", children: [] },
{ label: "Phí và lệ phí cấp C/O", url: "/xuat-xu-hang-hoa/phi-le-phi-cap-co", children: [] },
{ label: "Điểm cấp và thời gian cấp C/O", url: "/xuat-xu-hang-hoa/diem-cap-thoi-gian", children: [] },
{ label: "Thông tin liên hệ", url: "/xuat-xu-hang-hoa/lien-he", children: [] },
],
},
{
label: "Đại diện giới chủ",
url: "/dai-dien-gioi-chu",
children: [
{ label: "Chức năng đại diện người sử dụng lao động", url: "/dai-dien-gioi-chu/chuc-nang", children: [] },
{ label: "Sự kiện - tập huấn NSDLĐ", url: "/dai-dien-gioi-chu/su-kien-tap-huan", children: [] },
{ label: "Tin liên quan", url: "/dai-dien-gioi-chu/tin-lien-quan", children: [] },
{ label: "Chủ đề", url: "/dai-dien-gioi-chu/chu-de", children: [] },
],
},
{
label: "Xúc tiến thương mại",
url: "/xuc-tien-thuong-mai",
children: [
{ label: "Hồ sơ thị trường", url: "/xuc-tien-thuong-mai/ho-so-thi-truong", children: [] },
{ label: "Môi trường kinh doanh", url: "/xuc-tien-thuong-mai/doi-song-kinh-doanh", children: [] },
{ label: "Cơ hội kinh doanh", url: "/xuc-tien-thuong-mai/co-hoi-kinh-doanh", children: [] },
{ label: "Hỗ trợ kinh doanh", url: "/xuc-tien-thuong-mai/ho-tro-kinh-doanh", children: [] },
],
},
{
label: "Thông tin truyền thông",
url: "/thong-tin-truyen-thong",
children: [
{ label: "Tin VCCI", url: "/thong-tin-truyen-thong/tin-vcci", children: [] },
{ label: "Tin kinh tế", url: "/thong-tin-truyen-thong/tin-kinh-te", children: [] },
{ label: "Tin doanh nghiệp", url: "/thong-tin-truyen-thong/tin-doanh-nghiep", children: [] },
{ label: "Chuyên đề", url: "/thong-tin-truyen-thong/chuyen-de", children: [] },
{ label: "Thông tin chính sách và pháp luật", url: "/thong-tin-truyen-thong/chinh-sach-phap-luat", children: [] },
{ label: "Ấn phẩm", url: "/thong-tin-truyen-thong/an-pham", children: [] },
{ label: "Thư viện tài liệu", url: "/thong-tin-truyen-thong/thu-vien-tai-lieu", children: [] },
],
},
];
export default MOCK_SITEMAP;
"use client";
import React from "react";
import { MOCK_SITEMAP } from "./_lib/mock-data";
import Link from "next/link";
function SiteMapPage() {
const homepage = MOCK_SITEMAP[0];
const sections = MOCK_SITEMAP.slice(1);
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold text-center mb-12 text-[#063e8e]">
SƠ ĐỒ TRANG WEB
</h1>
{/* Sitemap Structure */}
<div className="relative flex flex-col items-center">
{/* Homepage - Top Level */}
<div className="relative mb-20">
<Link
href={homepage.url}
className="block bg-[#063e8e] text-white px-8 py-4 rounded-lg font-semibold text-center hover:bg-[#0a4fb5] transition shadow-lg min-w-[200px]"
>
{homepage.label.toUpperCase()}
</Link>
{/* Vertical line from homepage down */}
<div className="absolute left-1/2 -translate-x-1/2 top-full h-16 w-0.5 bg-gray-600"></div>
</div>
{/* Main Sections - Second Level */}
<div className="relative w-full max-w-[1400px]">
{/* Horizontal line connecting all sections */}
<div className="absolute top-0 left-[7%] right-[7%] h-0.5 bg-gray-600 z-0"></div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-6 relative pt-4">
{sections.map((section, idx) => (
<div key={idx} className="relative flex flex-col items-center">
{/* Vertical line from horizontal bar down to section */}
<div className="absolute -top-4 left-1/2 -translate-x-1/2 h-4 w-0.5 bg-gray-600 z-10"></div>
{/* Section Box */}
<div className="relative z-20">
<Link
href={section.url}
className="flex bg-[#063e8e] text-white px-4 py-3 rounded-md font-medium text-center hover:bg-[#0a4fb5] transition shadow-md w-full text-sm min-h-20 items-center justify-center"
>
<span className="leading-tight">{section.label.toUpperCase()}</span>
</Link>
{/* Vertical line from section down to children */}
{section.children && section.children.length > 0 && (
<div className="absolute left-1/2 -translate-x-1/2 top-full h-6 w-0.5 bg-gray-600 z-10"></div>
)}
</div>
{/* Children - Third Level */}
{section.children && section.children.length > 0 && (
<div className="mt-6 flex flex-col gap-3 w-full relative z-20">
{section.children.map((child, childIdx) => (
<div key={childIdx} className="relative">
{/* Vertical line connecting child to parent */}
{childIdx === 0 ? (
// First child connects to the line from parent
<div className="absolute left-1/2 -translate-x-1/2 -top-6 h-6 w-0.5 bg-gray-600"></div>
) : (
// Other children connect to the vertical spine
<div className="absolute left-1/2 -translate-x-1/2 -top-[calc(1.5rem+0.375rem)] bottom-1/2 w-0.5 bg-gray-600"></div>
)}
{/* Horizontal line from spine to child box */}
<div className="absolute left-1/2 top-1/2 -translate-y-1/2 w-1/2 h-0.5 bg-gray-600 -translate-x-full"></div>
<Link
href={child.url}
className="block bg-gray-400 text-white px-3 py-2.5 rounded text-xs font-medium text-center hover:bg-gray-500 transition shadow-sm leading-tight relative z-10"
>
{child.label.toUpperCase()}
</Link>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
<style jsx>{`
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
`}</style>
</div>
);
}
export default SiteMapPage;
"use client";
import ListCategory from "@/components/base/list-category";
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";
const publications = [
{
id: "huong-dan-dau-tu-2024",
title: "Cẩm nang Hướng dẫn đầu tư kinh doanh tại Việt Nam",
date: "18/10/2023",
link: "https://vcci-hcm.org.vn/wp-content/uploads/2023/10/Doing-Business-in-Vietnam-2023_Upload.pdf",
title_link: "“DOING BUSINESS IN VIETNAM 2023”",
img: "/an-pham/A-Guide-2023_Cover-725x1024.webp",
},
{
id: "connections-2022-2023",
title: "Danh bạ Hội viên CONNECTIONS 2022-2023",
date: "19/01/2023",
link: "https://vcci-hcm.org.vn/wp-content/uploads/2022/12/Danh-ba-HV-Connections_2022-2023.pdf",
title_link: "“DANH BẠ HỘI VIÊN CONNECTIONS 2022-2023”",
img: "/an-pham/Trang-bia_Connections_2022-2023-725x1024.jpg.webp",
},
{
id: "chuyen-doi-so",
title: "Chuyển đổi số – Động lực phục hồi và phát triển kinh tế",
date: "19/01/2023", // ← Sửa: 119 → 19
link: "https://vcci-hcm.org.vn/wp-content/uploads/2022/12/CHUYEN-DOI-SO-2022_Final_19.12.2022.pdf",
title_link: "“CHUYỂN ĐỔI SỐ – ĐỘNG LỰC PHỤC HỒI VÀ PHÁT TRIỂN KINH TẾ”",
img: "/an-pham/Trang-bia_Chuyen-doi-so_2022-750x1024.webp",
},
{
id: "huong-dan-dau-tu-2021",
title: "Cẩm nang Hướng dẫn đầu tư kinh doanh tại Việt Nam 2021",
date: "14/03/2022",
link: "https://vcci-hcm.org.vn/wp-content/uploads/2023/10/Doing-Business-in-Vietnam-2023_Upload.pdf",
title_link: "“DOING BUSINESS IN VIETNAM 2021”",
img: "/an-pham/doing-in-business-cover-1-1.webp",
},
{
id: "ban-tin-quy-4-2020",
title: "Bản tin Quý IV năm 2020",
date: "04/01/2021",
link: "https://vcci-hcm.org.vn/wp-content/uploads/2021/01/No3-092020_VCCI-NEWS-FINAL.pdf",
title_link: "Bản Tin Quý IV năm 2020",
img: "/an-pham/bia-ban-tin-quy-4-1.webp",
},
{
id: "ban-tin-quy-1-2020",
title: "Bản tin Quý I năm 2020",
date: "16/07/2020",
link: "https://vcci-hcm.org.vn/wp-content/uploads/2020/07/VCCI-NEWS-012020_XUAN.pdf",
title_link: "Bản tin Quý I năm 2020",
img: "/an-pham/bantintet-1.webp", // ← Sửa: iimg → img
},
];
// ĐÚNG: Không async, params là object
export default function PublicationDetail() {
const params = useParams(); // Dùng hook
const id = params.id as string; // Ép kiểu an toàn
const publication = publications.find((p) => p.id === id);
if (!publication) return notFound();
return (
<div className="bg-[#f6f6f6] min-h-screen">
<div className="max-w-[1200px] mx-auto flex flex-col gap-5 mb-[50px]">
<div className="border-[#e5e7f2] border-[1px]">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
</div>
<div className="w-full flex gap-5 flex-wrap">
<div className="lg:w-[calc(65%-10px)] w-full border-[#e5e7f2] border-[1px] bg-white p-[30px] flex flex-col gap-[15px]">
<h1 className="text-[22px] font-semibold text-[#003366]">
{publication.title}
</h1>
<p className="text-[#00AED5] text-sm">{publication.date}</p>
<hr />
<p className="text-[16px] text-[#363636]">
Tải về ấn phẩm:{" "}
<Link
href={publication.link}
target="_blank"
className="text-[#0073e6] hover:text-[#e8c518]"
>
{publication.title_link}
</Link>
</p>
<div className="flex justify-center">
<Link href={publication.link} target="_blank">
<Image
src={publication.img}
alt={publication.title}
width={416}
height={566}
className="rounded-lg transition-all duration-300"
/>
</Link>
</div>
</div>
<div className="lg:w-[calc(35%-10px)] w-full">
<Calendar />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-contain"
/>
</div>
</div>
</div>
</div>
</div>
);
}
"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>
</>
);
}
"use client";
import React, { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { Pagination } from "@components/base/pagination";
import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
const publications = [
{
id: "huong-dan-dau-tu-2024",
title: "Cẩm nang Hướng dẫn đầu tư kinh doanh tại Việt Nam",
img: "/an-pham/A-Guide-2023_Cover-725x1024.webp",
},
{
id: "connections-2022-2023",
title: "Danh bạ Hội viên CONNECTIONS 2022-2023",
img: "/an-pham/Trang-bia_Connections_2022-2023-725x1024.jpg.webp",
},
{
id: "chuyen-doi-so",
title: "Chuyển đổi số – Động lực phục hồi và phát triển kinh tế",
img: "/an-pham/Trang-bia_Chuyen-doi-so_2022-750x1024.webp",
},
{
id: "huong-dan-dau-tu-2021",
title: "Cẩm nang Hướng dẫn đầu tư kinh doanh tại Việt Nam 2021",
img: "/an-pham/doing-in-business-cover-1-1.webp",
},
{
id: "ban-tin-quy-4-2020",
title: "Bản tin Quý IV năm 2020",
img: "/an-pham/bia-ban-tin-quy-4-1.webp",
},
{
id: "ban-tin-quy-1-2020",
title: "Bản tin Quý I năm 2020",
img: "/an-pham/bantintet-1.webp",
},
];
export default function PublicationList() {
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
});
return (
<div className="lg:w-[calc(65%-10px)] w-full flex flex-col gap-[15px]">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
{publications.map((pub) => (
<Link
href={`/thong-tin-truyen-thong/an-pham/${pub.id}`}
key={pub.id}
className="flex flex-col items-center text-center h-full max-h-[342px] bg-white group"
>
<div className="w-full max-w-[260px] aspect-[3/4] overflow-hidden rounded-lg">
<Image
src={pub.img}
alt={pub.title}
width={300}
height={400}
className="object-contain w-full h-full transition-transform duration-300 group-hover:scale-105"
/>
</div>
<h3 className="mt-3 text-[15px] font-semibold text-[#124588] group-hover:text-[#E8C518] leading-snug">
{pub.title}
</h3>
</Link>
))}
</div>
<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>
);
}
"use client"; import ListCategory from "@/components/base/list-category";
import React, { useState } from "react"; import { MEDIA_INFORMATION_CATEGORIES } from "@/constants/categories";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter"; import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination} from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import PublicationList from "./components/publicationList";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5; export default function Page() {
const { data: allData } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined,
});
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="bg-[#f6f6f6]">
<div className="w-full flex flex-col gap-5"> <div className="max-w-[1200px] m-auto flex flex-col gap-5 mb-[50px]">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} /> <div className="border-[#e5e7f2] border-[1px]">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> </div>
{/* Main content */} <div className="w-full flex gap-5 flex-wrap">
<main className="lg:col-span-2 bg-background "> <PublicationList />
<div className="pb-5 overflow-hidden"> <div className="lg:w-[calc(35%-10px)] w-full">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
))}
<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>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventFilter /> <EventFilter />
<div className="relative w-full mt-4 h-[300px] aspect-video rounded-lg overflow-hidden">
<div className="bg-white border rounded-md overflow-hidden"> <Image
<div className="w-full h-56 relative bg-gray-100"> src="/banner.webp"
<Image alt="Quảng cáo"
src="/banner.webp" fill
alt="Quảng cáo" className="object-contain"
fill />
className="object-cover"
/>
</div>
</div> </div>
</aside> </div>
</div> </div>
</div> </div>
</div> </div>
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -5,42 +5,58 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories"; ...@@ -5,42 +5,58 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code... // ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news"; import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter"; import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { Pagination} from "@components/base/pagination"; import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch},category @=Chuyên đề` : 'category @=Chuyên đề',
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} /> <ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải chuyên đề...</span>
</div>
) : allData?.responseData.rows.length === 0 ? (
<p className="text-center py-4">Không có dữ liệu</p>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent
key={news.id}
news={news}
link={`${PATHS.mediaInformation}/chuyen-de/${news.id}`}
/>
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))} onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))}
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
...@@ -15,7 +15,7 @@ export default function Page() { ...@@ -15,7 +15,7 @@ export default function Page() {
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} /> <ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
</div> </div>
</div> </div>
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -5,32 +5,40 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories"; ...@@ -5,32 +5,40 @@ import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code... // ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news"; import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter"; import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { Pagination} from "@components/base/pagination"; import { Pagination } from "@components/base/pagination";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData,isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch},category @=Thông tin chính sách và pháp luật` : 'category @=Thông tin chính sách và pháp luật',
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} /> <ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {allData?.responseData.rows.length === 0 ? (
<NewsContent key={news.id} news={news} /> <p className="text-center py-4">Không có dữ liệu</p>
))} ) : (
allData?.responseData.rows.map((news) => (
<NewsContent
key={news.id}
news={news}
link={`${PATHS.mediaInformation}/thong-tin-chinh-sach-va-phap-luat/${news.id}`}
/>
)))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
......
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
...@@ -4,43 +4,60 @@ import ListCategory from "@app/dai-dien-gioi-chu/components/list-category"; ...@@ -4,43 +4,60 @@ import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories"; import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code... // ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news"; import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination} from "@components/base/pagination"; import { Pagination } from "@components/base/pagination";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter"; import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import Image from "next/image"; import Image from "next/image";
import { useGetNews } from "@api/endpoints/news"; import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type"; import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() { export default function Page() {
const [submitSearch] = useState(""); const [submitSearch] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 5; const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({ const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize), pageSize: String(pageSize),
currentPage: String(page), currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined, filters: submitSearch ? `title @=${submitSearch},category @=Thư viện tài liệu'` : 'category @=Thư viện tài liệu',
}); });
return ( return (
<div className="min-h-screen container mx-auto p-4"> <div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5"> <div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} /> <ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */} {/* Main content */}
<main className="lg:col-span-2 bg-background "> <main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => ( {isLoading ? (
<NewsContent key={news.id} news={news} /> <div className="flex justify-center items-center py-12">
))} <Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải thư viện tài liệu...</span>
</div>
) : allData?.responseData.rows.length === 0 ? (
<p className="text-center py-4">Không có dữ liệu</p>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent
key={news.id}
news={news}
link={`${PATHS.mediaInformation}/thu-vien-tai-lieu/${news.id}`}
/>
))}
<div className="w-full flex justify-center mt-4"> <div className="w-full flex justify-center mt-4">
<Pagination <Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)} pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)} page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)} onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))} onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))} onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))}
/> />
</div> </div>
</>
)}
</div> </div>
</main> </main>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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