Commit 714019d8 authored by Vũ Đình Nguyên's avatar Vũ Đình Nguyên

Merge branch 'develop' into 'feat/publication_page'

# Conflicts:
#   src/app/(main)/_lib/layout/MenuItem.tsx
#   src/app/(main)/thong-tin-truyen-thong/an-pham/page.tsx
#   src/components/base/list-category/index.tsx
parents 9625caf9 227363b8
......@@ -134,6 +134,9 @@ importers:
react:
specifier: 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:
specifier: ^9.11.1
version: 9.11.1(react@19.2.0)
......@@ -3270,6 +3273,12 @@ packages:
queue-microtask@1.2.3:
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:
resolution: {integrity: sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==}
engines: {node: '>=18'}
......@@ -7246,6 +7255,10 @@ snapshots:
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):
dependencies:
'@date-fns/tz': 1.4.1
......
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;
};
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;
};
\ No newline at end of file
......@@ -12,6 +12,9 @@ import { GetNewsDetailResponseType } from './page.type';
import { Link, CalendarFold, Book } from 'lucide-react';
import { useEffect, useMemo } from 'react';
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'
// Component
......@@ -33,82 +36,34 @@ const NewsDetailPage = () => {
<Spinner />
) : (
<div>
{/* Banner */}
<img className='w-full h-100' src={`${BASE_URLS.imageEndpoint}${data?.responseData?.thumbnail}`} alt='Banner' />
{/* Breadcrumb */}
<div className='container py-10'>
<div className='flex gap-4 items-stretch'>
{/* <Breadcrumbs aria-label='breadcrumb'>
<Link
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 className='container flex flex-col gap-5'>
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-7">
<div className='pb-5 text-blue-900 text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
{/* date */}
<div className='flex items-center gap-2 text-app-grey'>
<div className='flex items-center gap-2 text-sm mb-4'>
<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>
{/* author */}
{/* <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 className='py-5' >
<hr />
</div>
</div>
{/* Content */}
<div className='flex-1 text-app-grey text-base overflow-hidden'>
<AppEditorContent value={data?.responseData?.description ?? ''} />
</div>
<div className='flex-1 text-app-grey text-base overflow-hidden'>
<AppEditorContent value={data?.responseData?.description ?? ''} />
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
</aside>
</div>
</div>
{/* Related News */}
{/* <RelatedNews newsQuery={getRelatedNewsQuery} lang={lang} newsId={infoNews.id} /> */}
</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'
/>
<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,12 +7,12 @@ function CardNews({ news }: { news: NewsAdminItem }) {
return (
<a
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
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className='w-[120px] h-20 object-cover rounded-sm'
className='w-[100px] md:w-[130px] aspect-3/2 object-cover'
/>
<div className='flex-1'>
<p className='text-[#0056b3] font-bold text-sm line-clamp-2'>
......
This diff is collapsed.
......@@ -12,7 +12,7 @@ function Header() {
return (
<>
<div className="sticky top-0 w-full h-[56px] hidden lg:flex items-center justify-center bg-[#063e8e]">
<div className="max-w-[1215px] w-full px-4 flex items-center justify-between">
<div className="container w-full px-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-[130px] h-[36px] bg-[#e8c518] flex items-center justify-center border-4 rounded-sm border-[#647792]">
<a
......
// 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="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,6 +8,7 @@ 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 { PATHS } from "@constants/paths";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
......@@ -28,7 +29,7 @@ export default function Page() {
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
<NewsContent key={news.id} news={news} link={`${PATHS.ownerRepresentatives}/chu-de/${news.id}`}/>
))}
<div className="w-full flex justify-center mt-4">
......
......@@ -3,11 +3,11 @@ import { NewsItem } from '@app/dai-dien-gioi-chu/lib/types/NewsPage.type';
import Links from '@links/index'
import dayjs from 'dayjs';
import parse from 'html-react-parser'
function NewsContent({ news }: { news: NewsItem }) {
function NewsContent({ news ,link}: { news: NewsItem ,link:string}) {
return (
<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"
>
<img
......
// 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="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,6 +8,7 @@ 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 { PATHS } from "@constants/paths";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
......@@ -21,14 +22,14 @@ export default function Page() {
return (
<div className="min-h-screen container mx-auto p-4">
<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">
{/* Main content */}
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
<NewsContent key={news.id} news={news} link={`${PATHS.ownerRepresentatives}/tap-huan-nsdld/${news.id}`} />
))}
<div className="w-full flex justify-center mt-4">
......
// 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="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,10 +8,10 @@ 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 { PATHS } from "@constants/paths";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
......@@ -28,7 +28,7 @@ export default function Page() {
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
<NewsContent key={news.id} news={news} link={`${PATHS.ownerRepresentatives}/tin-lien-quan/${news.id}`} />
))}
<div className="w-full flex justify-center mt-4">
......
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"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { usePathname, useRouter } from "next/navigation"
import React from "react"
type Category = {
title: string
href: string
title: string
href: string
}
const CATEGORIES: Category[] = [
{ title: "Về VCCI-HCM", href: "/gioi-thieu" },
{ title: "Chức năng và Nhiệm vụ", href: "/gioi-thieu/chuc-nang" },
{ title: "Sơ đồ Tổ chức", href: "/gioi-thieu/so-do" },
{ title: "Dịch vụ cung cấp", href: "/gioi-thieu/dich-vu" },
{ title: "Về VCCI-HCM", href: "/gioi-thieu" },
{ 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-to-chuc" },
{ title: "Dịch vụ cung cấp", href: "/gioi-thieu/dich-vu-cung-cap" },
]
const ListCategory: React.FC = () => {
const pathname = usePathname() || ""
const isActive = (href: string) => {
// treat the base path as active for nested routes as well
if (href === "/gioi-thieu") return pathname === href || pathname.startsWith(href + "/")
return pathname === href
}
return (
<div className="border-t border-gray-200 bg-white p-2.5">
<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">
{CATEGORIES.map((c) => {
const active = isActive(c.href)
return (
<li key={c.href}>
<Link
href={c.href}
className={
"text-sm font-medium py-3.5 px-5 transition-colors duration-150 " +
(active
? "text-yellow-500 font-semibold decoration-yellow-300 "
: "text-gray-600 hover:text-gray-800 hover:underline")
}
>
{c.title}
</Link>
</li>
)
})}
</ul>
</nav>
</div>
</div>
)
const pathname = usePathname() || ""
const router = useRouter()
const isActive = (href: string) => {
if (href === "/gioi-thieu") return pathname === "/gioi-thieu"
return pathname === href
}
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
router.push(e.target.value)
}
return (
<div className="border-t border-gray-200 bg-white p-2.5">
<div className="max-w-7xl mx-auto">
<nav aria-label="Danh mục" className="py-3">
{/* --- Desktop view --- */}
<ul className="hidden sm:flex items-center">
{CATEGORIES.map((c) => {
const active = isActive(c.href)
return (
<li key={c.href}>
<Link
href={c.href}
className={
"text-sm font-bold py-3.5 px-5 transition-colors duration-150 " +
(active
? "text-yellow-500 font-semibold decoration-yellow-300"
: "text-gray-600 hover:text-yellow-500")
}
>
{c.title}
</Link>
</li>
)
})}
</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
\ No newline at end of file
export default ListCategory
"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() {
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
......
// 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="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,6 +8,7 @@ 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 { PATHS } from "@constants/paths";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
......@@ -28,7 +29,7 @@ export default function Page() {
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
<NewsContent key={news.id} news={news} link={`${PATHS.event}/dao-tao/${news.id}`}/>
))}
<div className="w-full flex justify-center mt-4">
......
// 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="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;
......@@ -9,7 +9,7 @@ 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 { PATHS } from "@constants/paths";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
......@@ -30,7 +30,7 @@ export default function Page() {
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
<NewsContent key={news.id} news={news} link={`${PATHS.event}/su-kien/${news.id}`}/>
))}
<div className="w-full flex justify-center mt-4">
......
......@@ -30,24 +30,24 @@ export default function Page() {
<CardNews 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>
<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">
......@@ -57,4 +57,4 @@ export default function Page() {
</div>
</div>
);
}
}
\ No newline at end of file
......@@ -31,13 +31,20 @@ export default function Page() {
))}
<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))}
/>
<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>
......
// 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="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;
......@@ -9,10 +9,10 @@ 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 { PATHS } from "@constants/paths";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
......@@ -29,7 +29,11 @@ export default function Page() {
<main className="lg:col-span-2 bg-background ">
<div className="pb-5 overflow-hidden">
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} />
<NewsContent
key={news.id}
news={news}
link={`${PATHS.mediaInformation}/chuyen-de/${news.id}`}
/>
))}
<div className="w-full flex justify-center mt-4">
......
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.
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