Commit 1f5f5868 authored by Vũ Đình Nguyên's avatar Vũ Đình Nguyên

Merge branch 'update/DynamicPage' into 'develop'

Update/dynamic page

See merge request !17
parents 21563945 5f957167
...@@ -16,7 +16,7 @@ import type { ...@@ -16,7 +16,7 @@ import type {
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import type { import type {
ObjBody, Obj2Body,
PostAuthForgotPasswordBody, PostAuthForgotPasswordBody,
PutAuthAssignBusinessInterestBody, PutAuthAssignBusinessInterestBody,
PutAuthUpdatePasswordBody, PutAuthUpdatePasswordBody,
...@@ -699,7 +699,7 @@ export const getPostAuthRegisterUrl = () => { ...@@ -699,7 +699,7 @@ export const getPostAuthRegisterUrl = () => {
return `/auth/register` return `/auth/register`
} }
export const postAuthRegister = async (objBody: ObjBody, options?: RequestInit): Promise<postAuthRegisterResponse> => { export const postAuthRegister = async (obj2Body: Obj2Body, options?: RequestInit): Promise<postAuthRegisterResponse> => {
return useCustomClient<postAuthRegisterResponse>(getPostAuthRegisterUrl(), return useCustomClient<postAuthRegisterResponse>(getPostAuthRegisterUrl(),
{ {
...@@ -707,7 +707,7 @@ export const postAuthRegister = async (objBody: ObjBody, options?: RequestInit): ...@@ -707,7 +707,7 @@ export const postAuthRegister = async (objBody: ObjBody, options?: RequestInit):
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers }, headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify( body: JSON.stringify(
objBody,) obj2Body,)
} }
);} );}
...@@ -715,8 +715,8 @@ export const postAuthRegister = async (objBody: ObjBody, options?: RequestInit): ...@@ -715,8 +715,8 @@ export const postAuthRegister = async (objBody: ObjBody, options?: RequestInit):
export const getPostAuthRegisterMutationOptions = <TError = ErrorType<unknown>, export const getPostAuthRegisterMutationOptions = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthRegister>>, TError,{data: BodyType<ObjBody>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthRegister>>, TError,{data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
): UseMutationOptions<Awaited<ReturnType<typeof postAuthRegister>>, TError,{data: BodyType<ObjBody>}, TContext> => { ): UseMutationOptions<Awaited<ReturnType<typeof postAuthRegister>>, TError,{data: BodyType<Obj2Body>}, TContext> => {
const mutationKey = ['postAuthRegister']; const mutationKey = ['postAuthRegister'];
const {mutation: mutationOptions, request: requestOptions} = options ? const {mutation: mutationOptions, request: requestOptions} = options ?
...@@ -728,7 +728,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -728,7 +728,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const mutationFn: MutationFunction<Awaited<ReturnType<typeof postAuthRegister>>, {data: BodyType<ObjBody>}> = (props) => { const mutationFn: MutationFunction<Awaited<ReturnType<typeof postAuthRegister>>, {data: BodyType<Obj2Body>}> = (props) => {
const {data} = props ?? {}; const {data} = props ?? {};
return postAuthRegister(data,requestOptions) return postAuthRegister(data,requestOptions)
...@@ -740,15 +740,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -740,15 +740,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return { mutationFn, ...mutationOptions }} return { mutationFn, ...mutationOptions }}
export type PostAuthRegisterMutationResult = NonNullable<Awaited<ReturnType<typeof postAuthRegister>>> export type PostAuthRegisterMutationResult = NonNullable<Awaited<ReturnType<typeof postAuthRegister>>>
export type PostAuthRegisterMutationBody = BodyType<ObjBody> export type PostAuthRegisterMutationBody = BodyType<Obj2Body>
export type PostAuthRegisterMutationError = ErrorType<unknown> export type PostAuthRegisterMutationError = ErrorType<unknown>
export const usePostAuthRegister = <TError = ErrorType<unknown>, export const usePostAuthRegister = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthRegister>>, TError,{data: BodyType<ObjBody>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthRegister>>, TError,{data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient): UseMutationResult< , queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof postAuthRegister>>, Awaited<ReturnType<typeof postAuthRegister>>,
TError, TError,
{data: BodyType<ObjBody>}, {data: BodyType<Obj2Body>},
TContext TContext
> => { > => {
...@@ -779,7 +779,7 @@ export const getPostAuthResendOTPUrl = () => { ...@@ -779,7 +779,7 @@ export const getPostAuthResendOTPUrl = () => {
return `/auth/resendOTP` return `/auth/resendOTP`
} }
export const postAuthResendOTP = async (objBody: ObjBody, options?: RequestInit): Promise<postAuthResendOTPResponse> => { export const postAuthResendOTP = async (obj2Body: Obj2Body, options?: RequestInit): Promise<postAuthResendOTPResponse> => {
return useCustomClient<postAuthResendOTPResponse>(getPostAuthResendOTPUrl(), return useCustomClient<postAuthResendOTPResponse>(getPostAuthResendOTPUrl(),
{ {
...@@ -787,7 +787,7 @@ export const postAuthResendOTP = async (objBody: ObjBody, options?: RequestInit) ...@@ -787,7 +787,7 @@ export const postAuthResendOTP = async (objBody: ObjBody, options?: RequestInit)
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers }, headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify( body: JSON.stringify(
objBody,) obj2Body,)
} }
);} );}
...@@ -795,8 +795,8 @@ export const postAuthResendOTP = async (objBody: ObjBody, options?: RequestInit) ...@@ -795,8 +795,8 @@ export const postAuthResendOTP = async (objBody: ObjBody, options?: RequestInit)
export const getPostAuthResendOTPMutationOptions = <TError = ErrorType<unknown>, export const getPostAuthResendOTPMutationOptions = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthResendOTP>>, TError,{data: BodyType<ObjBody>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthResendOTP>>, TError,{data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
): UseMutationOptions<Awaited<ReturnType<typeof postAuthResendOTP>>, TError,{data: BodyType<ObjBody>}, TContext> => { ): UseMutationOptions<Awaited<ReturnType<typeof postAuthResendOTP>>, TError,{data: BodyType<Obj2Body>}, TContext> => {
const mutationKey = ['postAuthResendOTP']; const mutationKey = ['postAuthResendOTP'];
const {mutation: mutationOptions, request: requestOptions} = options ? const {mutation: mutationOptions, request: requestOptions} = options ?
...@@ -808,7 +808,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -808,7 +808,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const mutationFn: MutationFunction<Awaited<ReturnType<typeof postAuthResendOTP>>, {data: BodyType<ObjBody>}> = (props) => { const mutationFn: MutationFunction<Awaited<ReturnType<typeof postAuthResendOTP>>, {data: BodyType<Obj2Body>}> = (props) => {
const {data} = props ?? {}; const {data} = props ?? {};
return postAuthResendOTP(data,requestOptions) return postAuthResendOTP(data,requestOptions)
...@@ -820,15 +820,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -820,15 +820,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return { mutationFn, ...mutationOptions }} return { mutationFn, ...mutationOptions }}
export type PostAuthResendOTPMutationResult = NonNullable<Awaited<ReturnType<typeof postAuthResendOTP>>> export type PostAuthResendOTPMutationResult = NonNullable<Awaited<ReturnType<typeof postAuthResendOTP>>>
export type PostAuthResendOTPMutationBody = BodyType<ObjBody> export type PostAuthResendOTPMutationBody = BodyType<Obj2Body>
export type PostAuthResendOTPMutationError = ErrorType<unknown> export type PostAuthResendOTPMutationError = ErrorType<unknown>
export const usePostAuthResendOTP = <TError = ErrorType<unknown>, export const usePostAuthResendOTP = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthResendOTP>>, TError,{data: BodyType<ObjBody>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAuthResendOTP>>, TError,{data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient): UseMutationResult< , queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof postAuthResendOTP>>, Awaited<ReturnType<typeof postAuthResendOTP>>,
TError, TError,
{data: BodyType<ObjBody>}, {data: BodyType<Obj2Body>},
TContext TContext
> => { > => {
......
...@@ -31,7 +31,7 @@ import type { ...@@ -31,7 +31,7 @@ import type {
import type { import type {
LookingFor, LookingFor,
Obj2Body, Obj3Body,
Response Response
} from '../models'; } from '../models';
...@@ -268,7 +268,7 @@ export const getPutContactUrl = () => { ...@@ -268,7 +268,7 @@ export const getPutContactUrl = () => {
return `/contact` return `/contact`
} }
export const putContact = async (obj2Body: Obj2Body, options?: RequestInit): Promise<putContactResponse> => { export const putContact = async (obj3Body: Obj3Body, options?: RequestInit): Promise<putContactResponse> => {
return useCustomClient<putContactResponse>(getPutContactUrl(), return useCustomClient<putContactResponse>(getPutContactUrl(),
{ {
...@@ -276,7 +276,7 @@ export const putContact = async (obj2Body: Obj2Body, options?: RequestInit): Pro ...@@ -276,7 +276,7 @@ export const putContact = async (obj2Body: Obj2Body, options?: RequestInit): Pro
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers }, headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify( body: JSON.stringify(
obj2Body,) obj3Body,)
} }
);} );}
...@@ -284,8 +284,8 @@ export const putContact = async (obj2Body: Obj2Body, options?: RequestInit): Pro ...@@ -284,8 +284,8 @@ export const putContact = async (obj2Body: Obj2Body, options?: RequestInit): Pro
export const getPutContactMutationOptions = <TError = ErrorType<unknown>, export const getPutContactMutationOptions = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putContact>>, TError,{data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putContact>>, TError,{data: BodyType<Obj3Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
): UseMutationOptions<Awaited<ReturnType<typeof putContact>>, TError,{data: BodyType<Obj2Body>}, TContext> => { ): UseMutationOptions<Awaited<ReturnType<typeof putContact>>, TError,{data: BodyType<Obj3Body>}, TContext> => {
const mutationKey = ['putContact']; const mutationKey = ['putContact'];
const {mutation: mutationOptions, request: requestOptions} = options ? const {mutation: mutationOptions, request: requestOptions} = options ?
...@@ -297,7 +297,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -297,7 +297,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const mutationFn: MutationFunction<Awaited<ReturnType<typeof putContact>>, {data: BodyType<Obj2Body>}> = (props) => { const mutationFn: MutationFunction<Awaited<ReturnType<typeof putContact>>, {data: BodyType<Obj3Body>}> = (props) => {
const {data} = props ?? {}; const {data} = props ?? {};
return putContact(data,requestOptions) return putContact(data,requestOptions)
...@@ -309,15 +309,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -309,15 +309,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return { mutationFn, ...mutationOptions }} return { mutationFn, ...mutationOptions }}
export type PutContactMutationResult = NonNullable<Awaited<ReturnType<typeof putContact>>> export type PutContactMutationResult = NonNullable<Awaited<ReturnType<typeof putContact>>>
export type PutContactMutationBody = BodyType<Obj2Body> export type PutContactMutationBody = BodyType<Obj3Body>
export type PutContactMutationError = ErrorType<unknown> export type PutContactMutationError = ErrorType<unknown>
export const usePutContact = <TError = ErrorType<unknown>, export const usePutContact = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putContact>>, TError,{data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putContact>>, TError,{data: BodyType<Obj3Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient): UseMutationResult< , queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof putContact>>, Awaited<ReturnType<typeof putContact>>,
TError, TError,
{data: BodyType<Obj2Body>}, {data: BodyType<Obj3Body>},
TContext TContext
> => { > => {
......
This diff is collapsed.
...@@ -30,7 +30,7 @@ import type { ...@@ -30,7 +30,7 @@ import type {
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import type { import type {
Obj2Body, Obj3Body,
Response Response
} from '../models'; } from '../models';
...@@ -871,7 +871,7 @@ export const getPutPageConfigTabIdUrl = (id: string,) => { ...@@ -871,7 +871,7 @@ export const getPutPageConfigTabIdUrl = (id: string,) => {
} }
export const putPageConfigTabId = async (id: string, export const putPageConfigTabId = async (id: string,
obj2Body: Obj2Body, options?: RequestInit): Promise<putPageConfigTabIdResponse> => { obj3Body: Obj3Body, options?: RequestInit): Promise<putPageConfigTabIdResponse> => {
return useCustomClient<putPageConfigTabIdResponse>(getPutPageConfigTabIdUrl(id), return useCustomClient<putPageConfigTabIdResponse>(getPutPageConfigTabIdUrl(id),
{ {
...@@ -879,7 +879,7 @@ export const putPageConfigTabId = async (id: string, ...@@ -879,7 +879,7 @@ export const putPageConfigTabId = async (id: string,
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers }, headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify( body: JSON.stringify(
obj2Body,) obj3Body,)
} }
);} );}
...@@ -887,8 +887,8 @@ export const putPageConfigTabId = async (id: string, ...@@ -887,8 +887,8 @@ export const putPageConfigTabId = async (id: string,
export const getPutPageConfigTabIdMutationOptions = <TError = ErrorType<unknown>, export const getPutPageConfigTabIdMutationOptions = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putPageConfigTabId>>, TError,{id: string;data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putPageConfigTabId>>, TError,{id: string;data: BodyType<Obj3Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
): UseMutationOptions<Awaited<ReturnType<typeof putPageConfigTabId>>, TError,{id: string;data: BodyType<Obj2Body>}, TContext> => { ): UseMutationOptions<Awaited<ReturnType<typeof putPageConfigTabId>>, TError,{id: string;data: BodyType<Obj3Body>}, TContext> => {
const mutationKey = ['putPageConfigTabId']; const mutationKey = ['putPageConfigTabId'];
const {mutation: mutationOptions, request: requestOptions} = options ? const {mutation: mutationOptions, request: requestOptions} = options ?
...@@ -900,7 +900,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -900,7 +900,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const mutationFn: MutationFunction<Awaited<ReturnType<typeof putPageConfigTabId>>, {id: string;data: BodyType<Obj2Body>}> = (props) => { const mutationFn: MutationFunction<Awaited<ReturnType<typeof putPageConfigTabId>>, {id: string;data: BodyType<Obj3Body>}> = (props) => {
const {id,data} = props ?? {}; const {id,data} = props ?? {};
return putPageConfigTabId(id,data,requestOptions) return putPageConfigTabId(id,data,requestOptions)
...@@ -912,15 +912,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ? ...@@ -912,15 +912,15 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return { mutationFn, ...mutationOptions }} return { mutationFn, ...mutationOptions }}
export type PutPageConfigTabIdMutationResult = NonNullable<Awaited<ReturnType<typeof putPageConfigTabId>>> export type PutPageConfigTabIdMutationResult = NonNullable<Awaited<ReturnType<typeof putPageConfigTabId>>>
export type PutPageConfigTabIdMutationBody = BodyType<Obj2Body> export type PutPageConfigTabIdMutationBody = BodyType<Obj3Body>
export type PutPageConfigTabIdMutationError = ErrorType<unknown> export type PutPageConfigTabIdMutationError = ErrorType<unknown>
export const usePutPageConfigTabId = <TError = ErrorType<unknown>, export const usePutPageConfigTabId = <TError = ErrorType<unknown>,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putPageConfigTabId>>, TError,{id: string;data: BodyType<Obj2Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>} TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof putPageConfigTabId>>, TError,{id: string;data: BodyType<Obj3Body>}, TContext>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient): UseMutationResult< , queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof putPageConfigTabId>>, Awaited<ReturnType<typeof putPageConfigTabId>>,
TError, TError,
{id: string;data: BodyType<Obj2Body>}, {id: string;data: BodyType<Obj3Body>},
TContext TContext
> => { > => {
......
/**
* 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 GetNewsPageConfigGetHierarchicalParams = {
/**
* Static link of the news page config to retrieve
*/
static_link?: string;
/**
* ID of the news page config to retrieve
*/
id?: string;
/**
* Code of the news page config to retrieve
*/
code?: string;
};
...@@ -52,6 +52,7 @@ export * from './getFooterParams'; ...@@ -52,6 +52,7 @@ export * from './getFooterParams';
export * from './getMembershipFeeParams'; export * from './getMembershipFeeParams';
export * from './getMembershipFeePrintParams'; export * from './getMembershipFeePrintParams';
export * from './getNewsAdminParams'; export * from './getNewsAdminParams';
export * from './getNewsPageConfigGetHierarchicalParams';
export * from './getNewsParams'; export * from './getNewsParams';
export * from './getNotificationsParams'; export * from './getNotificationsParams';
export * from './getOrderGetMyOrdersParams'; export * from './getOrderGetMyOrdersParams';
...@@ -94,6 +95,7 @@ export * from './membershipFeeBody'; ...@@ -94,6 +95,7 @@ export * from './membershipFeeBody';
export * from './news'; export * from './news';
export * from './notification'; export * from './notification';
export * from './obj2Body'; export * from './obj2Body';
export * from './obj3Body';
export * from './objBody'; export * from './objBody';
export * from './order'; export * from './order';
export * from './orderPayment'; export * from './orderPayment';
......
...@@ -7,5 +7,5 @@ ...@@ -7,5 +7,5 @@
*/ */
export type Obj2Body = { export type Obj2Body = {
content?: string; email?: string;
}; };
/**
* 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 Obj3Body = {
content?: string;
};
...@@ -7,5 +7,16 @@ ...@@ -7,5 +7,16 @@
*/ */
export type ObjBody = { export type ObjBody = {
email?: string; /** Page name */
name?: string;
/** Page code */
code?: string;
/** Static link */
static_link?: string;
/** Static link English */
static_link_en?: string;
/** Parent page ID */
parent_id?: string;
/** Is article page */
is_article?: boolean;
}; };
export interface NewsPageConfigItem {
id: string,
name: string,
code: string,
static_link: string,
is_article: boolean,
level: number,
sort_order: number,
children: Array<NewsPageConfigItem>,
};
export interface GetNewsPageConfigResponseType {
message: string,
message_en: string,
responseData: NewsPageConfigItem,
status: string,
timeStamp: string,
violations: "null | Object",
};
\ No newline at end of file
export interface NewsItem {
export interface NewsAdminItem {
id: string id: string
title: string title: string
thumbnail: string thumbnail: string
...@@ -13,19 +12,25 @@ export interface NewsAdminItem { ...@@ -13,19 +12,25 @@ export interface NewsAdminItem {
updated_by: string | null updated_by: string | null
mode: 'NOW' | string mode: 'NOW' | string
category: string category: string
page_config: {
id: string
name: string
static_link: string
static_link_en: string
}
} }
export interface NewsAdminResponseData { export interface NewsResponseData {
count: number count: number
rows: NewsAdminItem[] rows: NewsItem[]
totalPages: number totalPages: number
currentPage: number currentPage: number
} }
export interface GetNewsAdminResponseType { export interface GetNewsResponseType {
message: string message: string
message_en: string message_en: string
responseData: NewsAdminResponseData responseData: NewsResponseData
status: 'success' | 'error' status: 'success' | 'error'
timeStamp: string timeStamp: string
violations: string | null violations: string | null
......
'use client'
//Core
import dayjs from 'dayjs'
// App
import { Spinner } from '@/components/ui'
import AppEditorContent from '@/components/shared/editor-content'
import BASE_URLS from '@/links'
import { useGetNewsId } from '@/api/endpoints/news';
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 parse from "html-react-parser";
// import { t } from 'i18next'
// Component
const NewsDetailPage = () => {
const { id } = useParams()
// server
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
// const { t, i18n } = useTranslation('newsPage')
// const { newsDetail, fallbackClient } = data as Data
// const lang = i18n.language == 'vi' ? 'vi' : 'vi'
console.log('newsDetail', data);
// Template
return (
<div className='pb-10'>
{isLoading ? (
<Spinner />
) : (
<div>
<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-8">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<div className='flex items-center gap-2 text-sm mb-4'>
<CalendarFold />
<span className='text-base text-blue-700'>{dayjs(data?.responseData?.created_at).format('DD/MM/YYYY')}</span>
</div>
<div className='py-5' >
<hr />
</div>
<div className='flex-1 text-app-grey text-base overflow-hidden'>
<div className="prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
</aside>
</div>
</div>
</div>
)}
</div >
)
}
export default NewsDetailPage
\ No newline at end of file
import { NewsAdminItem } from "@/api/types/news"; import { NewsItem } from "@/api/types/news";
import BASE_URL from "@/links"; import BASE_URL from "@/links";
import dayjs from "dayjs"; import dayjs from "dayjs";
import AppEditorContent from "@/components/shared/editor-content"; import AppEditorContent from "@/components/shared/editor-content";
function CardNews({ news }: { news: NewsAdminItem }) { function CardNews({ news }: { news: NewsItem }) {
return ( return (
<a <a
href={`${news.id}`} href={`${news.page_config.static_link}/${news.id}`}
className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3" className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3"
> >
<img <img
......
...@@ -14,58 +14,45 @@ import CardNews from "./components/card-news"; ...@@ -14,58 +14,45 @@ import CardNews from "./components/card-news";
import CardEvent from "./components/card-event"; import CardEvent from "./components/card-event";
import EventCalendar from "@components/base/event-calendar"; import EventCalendar from "@components/base/event-calendar";
import dayjs from "dayjs"; import dayjs from "dayjs";
import AppEditorContent from "@/components/shared/editor-content";
// server // server
import { useGetEvents } from "@/api/endpoints/event"; import { useGetEvents } from "@/api/endpoints/event";
import { useGetCategory } from "@/api/endpoints/category"; import { useGetCategory } from "@/api/endpoints/category";
import { useGetNews } from "@/api/endpoints/news"; import { useGetNews } from "@/api/endpoints/news";
import { GetCategoryAdminResponseType } from "@/api/types/category"; import { GetCategoryAdminResponseType } from "@/api/types/category";
import { GetNewsAdminResponseType, NewsAdminItem } from "@/api/types/news"; import { GetNewsResponseType, NewsItem } from "@/api/types/news";
import { EventApiResponse, EventItem } from "@/api/types/event"; import { EventApiResponse, EventItem } from "@/api/types/event";
import { ChevronsRight, Link } from "lucide-react"; import { ChevronsRight, Link } from "lucide-react";
import { useParams } from "next/navigation";
const Page = () => { const Page = () => {
// state
const [tab, setTab] = useState("all"); const [tab, setTab] = useState("all");
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const swiperRef = useRef<SwiperType | null>(null); const swiperRef = useRef<SwiperType | null>(null);
// helpers // query
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, '')
}
const { data: categoryData, isLoading: isLoadingCategory } = useGetCategory<GetCategoryAdminResponseType>(); const { data: categoryData, isLoading: isLoadingCategory } = useGetCategory<GetCategoryAdminResponseType>();
const { data: newsData, isLoading: isLoadingNews } = useGetNews<GetNewsAdminResponseType>( const { data: newsData, isLoading: isLoadingNews } = useGetNews<GetNewsResponseType>(
{ {
pageSize: '5', pageSize: '5',
filters: tab === "all" ? `` : `category @=${tab}`, filters: tab === "all" ? `` : `category @=${tab}`,
} }
); );
const { data: newsAll, isLoading: isLoadingNewsAll } = useGetNews<GetNewsAdminResponseType>(
const { data: newsAll, isLoading: isLoadingNewsAll } = useGetNews<GetNewsResponseType>(
{ {
pageSize: '10', pageSize: '10',
} }
); );
const { data: businessOpportunities, isLoading: isLoadingBusinessOpportunities } = useGetNews<GetNewsAdminResponseType>( const { data: businessOpportunities, isLoading: isLoadingBusinessOpportunities } = useGetNews<GetNewsResponseType>(
{ {
pageSize: '5', pageSize: '5',
filters: `category @=Cơ hội kinh doanh`, filters: `category @=Cơ hội kinh doanh`,
} }
); );
const { data: policyAndLegalInformation, isLoading: isLoadingPolicyAndLegalInformation } = useGetNews<GetNewsAdminResponseType>( const { data: policyAndLegalInformation, isLoading: isLoadingPolicyAndLegalInformation } = useGetNews<GetNewsResponseType>(
{ {
pageSize: '5', pageSize: '5',
filters: `category @=Thông tin chính sách và pháp luật`, filters: `category @=Thông tin chính sách và pháp luật`,
...@@ -73,6 +60,23 @@ const Page = () => { ...@@ -73,6 +60,23 @@ const Page = () => {
); );
const { data: eventData, isLoading: isLoadingEvent } = useGetEvents<EventApiResponse>(); const { data: eventData, isLoading: isLoadingEvent } = useGetEvents<EventApiResponse>();
// helpers
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, '')
}
const images = [ const images = [
"/home/doi-tac/AMFORI-1.png.webp", "/home/doi-tac/AMFORI-1.png.webp",
"/home/doi-tac/AUS4SKILLS-1.png.webp", "/home/doi-tac/AUS4SKILLS-1.png.webp",
...@@ -166,13 +170,17 @@ const Page = () => { ...@@ -166,13 +170,17 @@ const Page = () => {
{newsAll?.responseData?.rows.map((news) => ( {newsAll?.responseData?.rows.map((news) => (
<SwiperSlide key={news.id}> <SwiperSlide key={news.id}>
<a <a
href={`/${news.id}`} href={`${news.page_config.static_link}/${news.id}`}
className="relative block bg-white shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300" className="relative block bg-white shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300"
> >
<img <img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title} alt={news.title}
className="w-full aspect-3/2 sm:h-56 md:h-64 object-cover" className="w-full aspect-3/2 sm:h-56 md:h-64 object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/> />
<div className="absolute bottom-0 left-0 right-0 h-20 md:h-24 bg-linear-to-t from-black/80 to-transparent flex items-center justify-center p-3"> <div className="absolute bottom-0 left-0 right-0 h-20 md:h-24 bg-linear-to-t from-black/80 to-transparent flex items-center justify-center p-3">
<p className="text-white text-center font-semibold line-clamp-2 text-sm sm:text-base leading-snug"> <p className="text-white text-center font-semibold line-clamp-2 text-sm sm:text-base leading-snug">
...@@ -214,10 +222,10 @@ const Page = () => { ...@@ -214,10 +222,10 @@ const Page = () => {
<div className="flex flex-col md:flex-row gap-5"> <div className="flex flex-col md:flex-row gap-5">
{newsAll?.responseData.rows {newsAll?.responseData.rows
.slice(0, 1) .slice(0, 1)
.map((news: NewsAdminItem) => ( .map((news: NewsItem) => (
<a <a
key={news.id} key={news.id}
href={`${news.id}`} href={`${news.page_config.static_link}/${news.id}`}
className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 bg-white" className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 bg-white"
> >
<div className="w-full aspect-3/2 overflow-hidden"> <div className="w-full aspect-3/2 overflow-hidden">
...@@ -225,6 +233,10 @@ const Page = () => { ...@@ -225,6 +233,10 @@ const Page = () => {
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title} alt={news.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/> />
</div> </div>
...@@ -420,13 +432,17 @@ const Page = () => { ...@@ -420,13 +432,17 @@ const Page = () => {
<div className="pt-2"> <div className="pt-2">
{businessOpportunities?.responseData.rows {businessOpportunities?.responseData.rows
.slice(0, 1) .slice(0, 1)
.map((news: NewsAdminItem) => ( .map((news: NewsItem) => (
<a key={news.id} href={`${news.id}`}> <a key={news.id} href={`${news.page_config.static_link}/${news.id}`}>
<div className="w-full aspect-3/2 relative overflow-hidden mb-5"> <div className="w-full aspect-3/2 relative overflow-hidden mb-5">
<img <img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title} alt={news.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/> />
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5"> <div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
<p className="text-[#063e8e] font-semibold text-sm sm:text-base z-10 line-clamp-3"> <p className="text-[#063e8e] font-semibold text-sm sm:text-base z-10 line-clamp-3">
...@@ -461,13 +477,17 @@ const Page = () => { ...@@ -461,13 +477,17 @@ const Page = () => {
<div className="pt-2"> <div className="pt-2">
{policyAndLegalInformation?.responseData.rows {policyAndLegalInformation?.responseData.rows
.slice(0, 1) .slice(0, 1)
.map((news: NewsAdminItem) => ( .map((news: NewsItem) => (
<a key={news.id} href={`${news.id}`}> <a key={news.id} href={`${news.page_config.static_link}/${news.id}`}>
<div className="w-full aspect-3/2 relative overflow-hidden mb-5"> <div className="w-full aspect-3/2 relative overflow-hidden mb-5">
<img <img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title} alt={news.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
}}
/> />
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5"> <div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
<p className="text-[#063e8e] font-semibold text-sm sm:text-base z-10 line-clamp-3"> <p className="text-[#063e8e] font-semibold text-sm sm:text-base z-10 line-clamp-3">
......
"use client";
import { useParams } from "next/navigation";
import { useState } from "react";
import { Spinner } from "@/components/ui";
import { Pagination } from "@/components/base/pagination";
import { ListFilter } from "@/components/base/list-filter";
import EventCalendar from "@/components/base/event-calendar";
import ListCategory from "@/components/base/list-category";
import CardNews from "@/components/base/card-news";
import Image from "next/image";
import parse from "html-react-parser";
import dayjs from "dayjs";
// API hooks
import { useGetNews, useGetNewsId } from "@/api/endpoints/news";
import { GetNewsResponseType } from "@/api/types/news";
import { GetNewsDetailResponseType } from "./page.type";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
export default function DynamicPage() {
const params = useParams();
const slugArray = Array.isArray(params.slug) ? params.slug : [params.slug];
const lastPart = slugArray[slugArray.length - 1];
//check id post
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
lastPart as string
);
const { data: categoriesPage } =
useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>({
static_link: `/${slugArray[0]}`,
});
if (isUUID) {
const id = lastPart;
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(
id as string
);
return (
<div className='container w-full flex justify-center items-center pb-10'>
{isLoading ? (
<Spinner />
) : (
<div className='flex flex-col gap-5 w-full'>
<ListCategory categories={categoriesPage?.responseData?.children} />
<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-8">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<div className='flex items-center gap-2 text-sm mb-4'>
<span className='text-base text-blue-700'>
{dayjs(data?.responseData?.created_at).format('DD/MM/YYYY')}
</span>
</div>
<hr className="my-5" />
<div className='flex-1 text-app-grey text-base overflow-hidden'>
<div className="prose tiptap overflow-hidden">
{parse(data?.responseData?.description ?? '')}
</div>
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
</aside>
</div>
</div>
)}
</div>
);
}
// nếu là trang danh sách tin tức
const url = slugArray.join("/");
const [submitSearch, setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: news, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: `page_config.static_link==/${url}` + (submitSearch ? `,title@=${submitSearch}` : ""),
});
return (
<div className="min-h-screen container mx-auto">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={categoriesPage?.responseData?.children} />
<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ải tin VCCI...</span>
</div>
) : news?.responseData.rows.length === 0 ? (
<p className="text-center py-4">Không có dữ liệu</p>
) : (
<>
{news?.responseData.rows.map((item) => (
<CardNews
key={item.id}
news={item}
link={`/${url}/${item.id}`}
/>
))}
<div className="w-full flex justify-center mt-4">
<Pagination
pageCount={Number(news?.responseData.totalPages ?? 1)}
page={Number(news?.responseData.currentPage ?? page)}
onChangePage={setPage}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() =>
setPage(Math.min(Number(news?.responseData.totalPages ?? 1), page + 1))
}
/>
</div>
</>
)}
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter onSearch={setSubmitSearch} />
<EventCalendar />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full relative bg-gray-100">
<img
src="/banner.webp"
alt="Quảng cáo"
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
}
...@@ -4,12 +4,17 @@ import { useRouter } from "next/navigation"; ...@@ -4,12 +4,17 @@ import { useRouter } from "next/navigation";
import { Menu, X, Facebook, Linkedin, Twitter, Youtube } from "lucide-react"; import { Menu, X, Facebook, Linkedin, Twitter, Youtube } from "lucide-react";
import logo from "@/assets/VCCI-HCM-logo-VN-2025.png"; import logo from "@/assets/VCCI-HCM-logo-VN-2025.png";
import Image from "next/image"; import Image from "next/image";
import MenuItem from "./MenuItem"; import MenuItem from "@/components/base/menu-item";
import Link from "next/link"; import Link from "next/link";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
function Header() { function Header() {
const [toggleMenu, setToggleMenu] = useState<boolean>(false); const [toggleMenu, setToggleMenu] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
const { data: categoriesPage } = useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>();
return ( return (
<> <>
<div className="sticky top-0 w-full h-14 hidden lg:flex items-center justify-center bg-[#063e8e]"> <div className="sticky top-0 w-full h-14 hidden lg:flex items-center justify-center bg-[#063e8e]">
...@@ -80,166 +85,19 @@ function Header() { ...@@ -80,166 +85,19 @@ function Header() {
{/* Desktop Menu */} {/* Desktop Menu */}
<nav className="hidden lg:flex items-center"> <nav className="hidden lg:flex items-center">
{/* Dùng component MenuItem để gọn */} {categoriesPage?.responseData?.children?.map((category) => (
<MenuItem <MenuItem
title="Giới thiệu" key={category.id}
link="gioi-thieu" title={category.name}
items={[ link={category.static_link}
{ items={[
title: "Về VCCI-HCM", ...category.children.map((child) => ({
link: "" title: child.name,
}, link: child.static_link,
{ })),
title: "Chức Năng Và Nhiệm Vụ", ]}
link: "chuc-nang-va-nhiem-vu", />
}, ))}
{ title: "Sơ Đồ Tổ Chức", link: "so-do-to-chuc" },
{ title: "Dịch Vụ Cung Cấp", link: "dich-vu-cung-cap" },
]}
/>
<MenuItem
title="Hội viên"
link="hoi-vien"
items={[
{
title: "Lợi Ích Của Hội Viên VCCI",
link: "",
},
{ title: "Đăng Ký Hội Viên", link: "dang-ky-hoi-vien" },
{ title: "Kết Nối Hội Viên", link: "ket-noi-hoi-vien" },
{ title: "Tin Hội Viên", link: "tin-hoi-vien" },
]}
/>
<MenuItem
title="Hoạt động"
link="hoat-dong"
items={[
{ title: "Sự Kiện", link: "" },
{ title: "Đào Tạo", link: "dao-tao" },
]}
/>
<MenuItem
title="Xuất Xứ Hàng Hóa"
link="xuat-xu-hang-hoa"
items={[
{
title: "Định Nghĩa Chung",
link: "",
},
{
title: "Mục Đích Của C/O",
link: "muc-dich",
},
{
title: "Luật Áp Dụng Về C/O",
link: "luat-ap-dung",
},
{
title: "Thủ Tục Cấp C/O",
link: "thu-tuc-cap",
},
{
title: "Biểu Mẫu C/O Và Cách Khai",
link: "bieu-mau-c-o-va-cach-khai",
},
{
title: "Phí Và Lệ Phí Cấp C/O",
link: "phi-va-le-phi-cap",
},
{
title: "Điểm Cấp Và Thời Gian Cấp C/O",
link: "diem-cap-va-thoi-gian-cap",
},
{
title: "Thông Tin Liên Hệ",
link: "thong-tin-lien-he",
},
]}
/>
{/* Đại Diện Giới Chủ - có submenu cấp 2 */}
<div className="group relative">
<a
className="px-3 py-5 text-[16px] font-[600] text-[#124588] hover:text-[#E8C518] transition block"
href={`${"/dai-dien-gioi-chu"}`}
>
Đại Diện Giới Chủ
</a>
<div className="absolute left-0 top-full hidden group-hover:block bg-[#124588]/98 text-white text-[14px] font-[500] min-w-[280px] shadow-lg">
<div className="flex flex-col">
<Link
href={
"/dai-dien-gioi-chu/chuc-nang-dai-dien-nguoi-su-dung-lao-dong"
}
className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer"
>
Chức Năng Đại Diện Người Sử Dụng Lao Động
</Link>
<Link
href={"/dai-dien-gioi-chu/tap-huan-nsdld"}
className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer"
>
Sự Kiện – Tập Huấn NSDLĐ
</Link>
<Link
href={"/dai-dien-gioi-chu/tin-lien-quan"}
className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer"
>
Tin Liên Quan
</Link>
<div className="relative group/submenu">
<Link
href={"/dai-dien-gioi-chu/chu-de"}
className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer flex justify-between items-center"
>
Chủ Đề <span className="ml-2 text-xs"></span>
</Link>
<div className="absolute left-full top-0 hidden group-hover/submenu:block bg-[#124588]/98 text-white text-[14px] font-[500] min-w-[220px] shadow-lg">
<div className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer">
Quan Hệ Lao Động
</div>
<div className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer">
Phát Triển Kỹ Năng
</div>
<div className="px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer">
Phát Triển Bền Vững
</div>
</div>
</div>
</div>
</div>
</div>
<MenuItem
title="Xúc tiến thương mại"
link="xuc-tien-thuong-mai"
items={[
{ title: "Hồ Sơ Thị Trường", link: "ho-so-thi-truong" },
{
title: "Môi Trường Kinh Doanh",
link: "moi-truong-kinh-doanh",
},
{ title: "Cơ Hội Kinh Doanh", link: "co-hoi-kinh-doanh" },
{ title: "Hỗ Trợ Kinh Doanh", link: "ho-tro-kinh-doanh" },
]}
/>
<MenuItem
title="Thông tin truyền thông"
link="thong-tin-truyen-thong"
items={[
{ title: "Tin VCCI", link: "tin-vcci" },
{ title: "Tin Kinh Tế", link: "tin-kinh-te" },
{ title: "Tin Doanh Nghiệp", link: "tin-doanh-nghiep" },
{ title: "Chuyên Đề", link: "chuyen-de" },
{
title: "Thông Tin Chính Sách Và Pháp Luật",
link: "thong-tin-chinh-sach-va-phap-luat",
},
{ title: "Ấn Phẩm", link: "an-pham" },
{ title: "Thư Viện Tài Liệu", link: "thu-vien-tai-lieu" },
]}
/>
</nav> </nav>
{/* Mobile Button */} {/* Mobile Button */}
...@@ -254,25 +112,19 @@ function Header() { ...@@ -254,25 +112,19 @@ function Header() {
{/* Mobile Menu */} {/* Mobile Menu */}
<div <div
className={`lg:hidden bg-white shadow-lg transition-all duration-300 overflow-hidden ${toggleMenu ? "max-h-96 opacity-100" : "max-h-0 opacity-0" className={`lg:hidden bg-white shadow-lg transition-all duration-300 overflow-hidden ${toggleMenu ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"
}`} }`}
> >
{[ {categoriesPage?.responseData?.children?.map((category) => (
"Giới thiệu", <div key={category.id} className="border-b border-gray-200">
"Hội viên", <Link
"Hoạt động", href={category.static_link || "#"}
"Xuất Xứ Hàng Hóa", className="block py-3 text-center hover:bg-[#124588] hover:text-white text-[16px] font-medium"
"Đại Diện Giới Chủ", onClick={() => setToggleMenu(false)}
"Xúc tiến thương mại", >
"Thông tin truyền thông", {category.name}
].map((item) => ( </Link>
<a </div>
key={item}
href="#"
className="block py-3 text-center hover:bg-[#124588] hover:text-white text-[16px]"
>
{item}
</a>
))} ))}
</div> </div>
</div> </div>
......
// 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 EventCalendar from '@/components/base/event-calendar'
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">
<EventCalendar />
<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;
"use client";
import React, { useState } 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 { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setsubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData,isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category@=Chủ đề` : 'category@=Chủ đề',
});
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} />
<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ải dữ liệu...</span>
</div>
) : (
<>
{(!allData || (allData.responseData.rows || []).length === 0) ? (
<div className="py-12 text-center text-gray-600">Không có dữ liệu</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">
<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">
<ListFilter onSearch={setsubmitSearch}/>
<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>
);
}
// Core
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 parse from "html-react-parser";
import { SAMPLE_HTML } from "../lib/sampleHtml";
// ...existing code...
const Page: React.FC = () => {
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">{parse(SAMPLE_HTML)}</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;
import { NewsItem } from '@app/dai-dien-gioi-chu/lib/types/NewsPage.type';
import Links from '@links/index'
import dayjs from 'dayjs';
import {EventItem} from "@api/types/event";
// 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 (
<a
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
src={`${Links.imageEndpoint}${news?news.thumbnail:event?.image}`}
alt={news?news.title:event?.name}
className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0"
onError={(e) => {
e.currentTarget.src = "/img-error.png"
}}
/>
<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">
{news?.title}{event?.name}
</p>
<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 prose tiptap">{stripImagesAndHtml(news?news.description:event?.description)}</div>
</div>
</div>
</a>
)
}
export default NewsContent;
\ No newline at end of file
"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 React, { useState } from 'react'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
type Category = { id: string; title: string }
type FilterPayload = {
upcoming: boolean
past: boolean
query: string
categories: string[]
fromDate: string
toDate: string
}
export const EventFilter: React.FC<{
categories?: Category[]
onFilter?: (payload: FilterPayload) => void
onReset?: () => void
}> = ({ categories: categoriesProp, onFilter, onReset }) => {
const [upcoming, setUpcoming] = useState(false)
const [past, setPast] = useState(false)
const [query, setQuery] = useState('')
// Use categories passed via props if available; otherwise use an empty array
const categoriesList = categoriesProp ?? []
const [categories, setCategories] = useState<Record<string, boolean>>(() => {
const map: Record<string, boolean> = {}
categoriesList.forEach((c) => (map[c.id] = false))
return map
})
const [fromDate, setFromDate] = useState('')
const [toDate, setToDate] = useState('')
const toggleCategory = (id: string) => {
setCategories((s) => ({ ...s, [id]: !s[id] }))
}
const handleFilter = () => {
const payload = {
upcoming,
past,
query,
categories: Object.keys(categories).filter((k) => categories[k]),
fromDate,
toDate
}
onFilter?.(payload)
}
const handleReset = () => {
setUpcoming(false)
setPast(false)
setQuery('')
setCategories(Object.keys(categories).reduce((acc, k) => ({ ...acc, [k]: false }), {} as Record<string, boolean>))
setFromDate('')
setToDate('')
onReset?.()
}
return (
<aside className="p-6 bg-white border rounded-md">
<h3 className="text-lg font-semibold mb-4">Tìm kiếm sự kiện</h3>
<div className="flex flex-col gap-3 mb-4">
<label className="flex items-center gap-2">
<Checkbox checked={upcoming} onCheckedChange={() => setUpcoming((v) => !v)} />
<span className="text-sm">Sự kiện sắp diễn ra</span>
</label>
<label className="flex items-center gap-2">
<Checkbox checked={past} onCheckedChange={() => setPast((v) => !v)} />
<span className="text-sm">Sự kiện đã diễn ra</span>
</label>
</div>
<div className="mb-4">
<Input
placeholder="Tên sự kiện ..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className='text-black placeholder:text-gray-400 rounded-none py-2.5 px-2'
/>
</div>
{categoriesList && categoriesList.length > 0 && (
<div className="mb-4">
{categoriesList.map((c) => (
<label key={c.id} className="flex items-center gap-2 mb-2">
<Checkbox checked={!!categories[c.id]} onCheckedChange={() => toggleCategory(c.id)} />
<span className="text-sm">{c.title}</span>
</label>
))}
</div>
)}
<div className="mb-4">
<Label className="block text-sm mb-1">Từ ngày:</Label>
<Input value={fromDate} onChange={(e) => setFromDate(e.target.value)} type="date" />
</div>
<div className="mb-4">
<Label className="block text-sm mb-1">Đến ngày:</Label>
<Input value={toDate} onChange={(e) => setToDate(e.target.value)} type="date" />
</div>
<div className="flex items-center gap-3">
<Button onClick={handleFilter} className="flex-1 rounded-none font-medium text-lg text-white hover:bg-muted-foreground hover:outline-1 outline-primary hover:text-primary">Lọc sự kiện</Button>
<Button onClick={handleReset} className="flex-1 rounded-none font-medium text-lg text-white hover:bg-muted-foreground hover:outline-1 outline-primary hover:text-primary">Bỏ lọc</Button>
</div>
</aside>
)
}
export default EventFilter
"use client"
import { usePathname } from "next/navigation"
import React from "react"
import { MenuItem } from '../menu-category'
// Local Menu shape compatible with MenuItem
type Menu = {
id: string | number
name: string
link?: string
}
type Category = {
title: string
href: string
}
// Default categories removed — component now accepts `categories` via props.
const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] }) => {
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="w-full px-4 sm:px-6 lg:px-8">
<div className="py-3">
<div className="flex flex-wrap items-center max-w-full overflow-x-auto">
{categories.map((c) => {
const menu: Menu = { id: c.href, name: c.title, link: c.href }
const active = isActive(c.href)
return (
<div key={c.href} className="shrink-0">
<MenuItem menu={menu} active={active} />
</div>
)
})}
</div>
</div>
</div>
</div>
)
}
export default ListCategory
\ No newline at end of file
"use client"
import React, { useState, useEffect } 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 }
export const ListFilter: React.FC<{
categories?: Category[]
onSearch?: (q: string) => void
onReset?: () => void
}> = ({ categories, onSearch, onReset }) => {
const [query, setQuery] = useState('')
const [visibleCount, setVisibleCount] = useState(5)
const [selected, setSelected] = useState<Record<string, boolean>>(() => {
const map: Record<string, boolean> = {}
if (categories && categories.length) {
categories.forEach((c: Category) => (map[c.id] = false))
}
return map
})
// Keep selected map in sync when categories prop changes.
// Defer setSelected to avoid calling setState synchronously inside the effect.
useEffect(() => {
const timer = setTimeout(() => {
setSelected((prev) => {
const map: Record<string, boolean> = {}
if (categories && categories.length) {
categories.forEach((c: Category) => (map[c.id] = !!prev[c.id]))
}
return map
})
}, 0)
return () => clearTimeout(timer)
}, [categories])
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">
{categories && categories.length > 0 ? (
categories.slice(0, visibleCount).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>
))
) : null}
<div className="mt-2 flex items-center gap-3">
{(categories?.length ?? 0) > visibleCount && (
<button
className="text-sm text-primary self-start"
onClick={() => setVisibleCount((v) => v + 5)}
>
Xem thêm
</button>
)}
{visibleCount > 5 && (
<button
className="text-sm text-gray-500 self-start"
onClick={() => setVisibleCount(5)}
>
Thu gọn
</button>
)}
</div>
</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> = {}
if (categories && categories.length) {
categories.forEach((c) => (map[c.id] = false))
}
setSelected(map)
setVisibleCount(5)
onReset?.()
}}
>
Bỏ tìm
</Button>
</div>
</aside>
)
}
export default ListFilter
'use client'
type Menu = {
id: string | number
name: string
link?: string
children?: Array<{ id: string | number; name: string; link?: string }>
}
import { buttonVariants } from '@components/ui/button'
import { cn } from '@lib/utils'
import { useCallback, useMemo } from 'react'
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@components/ui/hover-card'
import { cva } from 'class-variance-authority'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function MenuItem(props: { variant?: 'main' | 'secondary' ; menu: Menu; active?: boolean }) {
const { menu, variant = 'main', active } = props
const pathname = usePathname()
const linkId = useMemo(() => `trigger_${menu.id}`, [menu.id])
const hoverCardRef = useCallback(
(element: HTMLDivElement) => {
if (!element) return
element.style.minWidth = `${document.getElementById(linkId)?.offsetWidth ?? 0}px`
},
[linkId]
)
return (
<HoverCard openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<Link
aria-selected={active || pathname == menu.link}
id={linkId}
target={(menu.link ?? '').startsWith('/') ? '_self' : '_blank'}
href={menu.link ?? '/'}
className={menuItemTriggerVariant({ variant })}
>
{menu.name}
</Link>
</HoverCardTrigger>
{menu.children && (
<HoverCardContent ref={hoverCardRef} className={menuItemHoverBoxVariant({ variant })}>
{menu.children.map((subMenu) => (
<Link key={subMenu.id} href={subMenu.link ?? '/'} className={menuItemChildVariant({ variant })}>
{subMenu.name}
</Link>
))}
</HoverCardContent>
)}
</HoverCard>
)
}
const menuItemTriggerVariant = cva(
cn(buttonVariants({ variant: 'ghost' }), 'font-semibold focus-visible:ring-0 focus-visible:ring-offset-0 py-'),
{
variants: {
variant: {
main: cn(
'font-semibold text-[#363636] text-2xl hover:text-muted-foreground hover:bg-white py-3.5 px-5',
'aria-selected:text-muted-foreground'
),
secondary: cn(
'font-boldtext-primary border-t-2 border-t-transparent rounded-none',
'hover:text-primary/90',
'aria-selected:border-t-secondary aria-selected:bg-accent',
'aria-selected:bg-[#E9C826]'
)
}
},
defaultVariants: {
variant: 'main'
}
}
)
const menuItemHoverBoxVariant = cva('flex w-full flex-col gap-2 p-0', {
variants: {
variant: {
main: 'bg-secondary',
secondary: 'bg-muted '
}
},
defaultVariants: {
variant: 'main'
}
})
const menuItemChildVariant = cva(cn(buttonVariants({ variant: 'ghost' }), 'justify-start'), {
variants: {
variant: {
main: 'text-secondary-foreground hover:text-muted-foreground hover:bg-secondary',
secondary: 'text-accent-foreground hover:text-primary/90 '
}
},
defaultVariants: {
variant: 'main'
}
})
export const SAMPLE_HTML = `
<div class="document">
<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>
Sau Đại hội đại biểu toàn quốc lần thứ tư của Phòng Thương mại và Công nghiệp Việt nam tháng 4/2003 và theo Quyết định số 123/2003/QĐ – TTg (tháng 6/2003) của Thủ tướng Chính phủ phê duyệt Điều lệ sửa đổi của Phòng Thương mại và Công nghiệp Việt Nam, vai trò của Phòng đã được bổ sung thêm chức năng mới là tổ chức đại diện để thúc đẩy và bảo vệ quyền lợi hợp pháp chính đáng của Người sử dụng lao động (NSDLĐ) ở Việt Nam trong các quan hệ lao động trong nước và quốc tế.
</p>
<p>
Trước đó từ tháng 05/1997 Văn phòng Giới sử dụng Lao động đã được thành lập để tham mưu cho lãnh đạo Phòng Thương mại và Công nghiệp Việt Nam về các vấn đề lao động, đồng thời duy trì quan hệ ba bên về lao động với các đối tác xã hội như Bộ Lao động-Thương binh và Xã hội, Tổng Liên đoàn lao động Việt Nam. Hiện nay Phòng Thương mại và Công nghiệp Việt Nam là thành viên chính thức của Tổ chức Giới chủ thế giới (IOE) và Liên đoàn Giới chủ Châu Á – Thái Bình Dưong (CAPE).
</p>
<p>Nhiệm vụ:</p>
<p>Văn phòng Giới sử dụng lao động là cơ quan chuyên môn của Phòng Thương mại và Công nghiệp Việt Nam (BEA-VCCI) thực hiện công tác đại diện để thúc đẩy và bảo vệ quyền lợi của người sử dụng lao động, xúc tiến quan hệ lao động tiến tiến ở Việt Nam:</p>
<ul>
<li>Đại diện cho giới sử dụng lao động Việt Nam trong cơ chế tư vấn ba bên về lao động ở trong nước và phối hợp với tổ chức đại diện của người lao động như Tổng Liên đoàn lao động Việt Nam (VGCL) và Bộ Lao động-Thương binh và Xã hội (MOLISA).</li>
<li>Tư vấn tạo một môi trường lao động thuận lợi cho sự phát triển doanh nghiệp, đóng vai trò như một tổ chức đại diện phản ánh ý kiến của giới sử dụng lao động về những khó khăn, vướng mắc trong quá trình thực hiện pháp luật lao động, từ đó bảo vệ lợi ích của giới sử dụng lao động.</li>
<li>Hỗ trợ phát triển hiệp hội người sử dụng lao động cấp tỉnh.</li>
<li>Cung cấp các dịch vụ và đào tạo cho cộng đồng doanh nghiệp về:</li>
</ul>
<h2 style="font-size:16px; font-weight:600; margin-top:16px;">Thông tin liên hệ</h2>
<p>
Địa chỉ: 42 Đường ABC, Quận 1, TP. Hồ Chí Minh<br />
Điện thoại: <a href="tel:+842823922025">(+84) 28 2392 2025</a><br />
Email: <a href="mailto:info@vcci-hcm.vn">info@vcci-hcm.vn</a>
</p>
<div class="notice" style="margin-top:16px; padding:10px; background:#fafafa; border-left:4px solid #ffd54f;">
<strong>Ghi chú:</strong> Thông tin trên mang tính tham khảo và cần được kiểm tra lại trước khi sử dụng chính thức.
</div>
</div>
`;
export default SAMPLE_HTML;
"use client"
// Core
import Image from "next/image";
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import ListCategory from "./components/list-category";
import ListFilter from "./components/list-filter";
import parse from "html-react-parser";
import { SAMPLE_HTML } from "./lib/sampleHtml";
import { PATHS } from "@constants/paths";
import { OWNER_REPRESENTATIVES_CATEGORIES } from "@constants/categories";
const Page = () => {
const router = useRouter()
useEffect(() => {
const firstHref = `${PATHS.ownerRepresentatives}/chuc-nang-dai-dien-nguoi-su-dung-lao-dong`
router.push(firstHref)
}, [router])
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>
</div>
);
};
export default Page;
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } 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 { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setsubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : undefined,
});
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} />
<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ả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">
<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">
<ListFilter onSearch={setsubmitSearch}/>
<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>
);
}
// 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";
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } 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 { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setsubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category@=Tin liên quan` : 'category@=Tin liên quan',
});
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} />
<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ải tin liên quan...</span>
</div>
) : (
<>
{(!allData || (allData.responseData.rows || []).length === 0) ? (
<div className="py-12 text-center text-gray-600">Không có dữ liệu</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">
<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">
<ListFilter onSearch={setsubmitSearch}/>
<div className="bg-white border rounded-md overflow-hidden hidden lg:block">
<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>
);
}
"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, useRouter } from "next/navigation"
import React from "react"
type Category = {
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-va-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 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
"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/base/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
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
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";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch] = useState("");
const [page, setPage] = useState(1);
const [filtersString, setFiltersString] = useState<string | undefined>('')
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: filtersString ? `${filtersString},category @=Đào tạo` : 'category @=Đào tạo',
});
return (
<div className="min-h-screen 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-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ải khóa đào tạo...</span>
</div>
) : (
<>
{(!allData || (allData.responseData.rows || []).length === 0) ? (
<div className="py-12 text-center text-gray-600">Không có dữ liệu</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">
<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
onFilter={(payload) => {
const parts: string[] = []
// query
if (payload.query) parts.push(`title @=${payload.query}`)
const nowIso = new Date().toISOString()
// upcoming / past
if (payload.upcoming && !payload.past) {
parts.push(`created_at>=${nowIso}`)
} else if (payload.past && !payload.upcoming) {
parts.push(`created_at<=${nowIso}`)
}
if (payload.fromDate) {
const fromIso = new Date(payload.fromDate).toISOString()
parts.push(`created_at>=${fromIso}`)
}
if (payload.toDate) {
const toIso = new Date(payload.toDate).toISOString()
parts.push(`created_at<=${toIso}`)
}
const filters = parts.length > 0 ? parts.join(',') : undefined
setFiltersString(filters)
setPage(1)
}}
onReset={() => {
setFiltersString(undefined)
setPage(1)
}}
/>
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
}
"use client";
import React, { useEffect } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { PATHS } from "@constants/paths";
import { EVENT_CATEGORIES } from "@constants/categories";
import { useRouter } from "next/navigation";
// ...existing code...
export default function Page() {
const router = useRouter();
useEffect(() => {
const firstHref = `${PATHS.event}/su-kien`;
router.push(firstHref);
}, [router]);
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} />
</div>
</div>
);
}
// Core
"use client";
import Image from "next/image";
import { useState } from "react";
import { Calendar, MapPin, CreditCard } from "lucide-react";
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";
import Links from "@links/index";
import EventCalendar from '@/components/base/event-calendar'
// ...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" />
{/* Top summary with image + details */}
<div className="flex flex-col lg:flex-row gap-6 my-6">
<div className="w-full lg:w-1/2 bg-gray-50 rounded-md overflow-hidden">
{data?.responseData?.image ? (
<div className="w-full h-52 relative ">
{/* Use controlled src state to avoid mutating Image DOM directly */}
{/* Next.js <Image> does not expose the underlying img element reliably in all builds */}
{/* so keep a React state for src and swap to a fallback on error. */}
<EventImage
src={`${Links.imageEndpoint}${data.responseData.image}`}
alt={data.responseData.name || "image"}
/>
</div>
) : (
<div className="w-full h-52 bg-gray-200" />
)}
</div>
<div className="w-full lg:w-1/2 bg-white border rounded-md p-6">
<div className="flex flex-col gap-3">
<div className="text-sm text-gray-500">
Hạn đăng kí:{" "}
<span className="text-gray-900 font-medium">
{data?.responseData?.created_at
? new Date(
data.responseData.created_at
).toLocaleDateString()
: "-"}
</span>
</div>
<div className="text-sm text-gray-500 flex items-start gap-2">
<Calendar className="h-5 w-5 text-yellow-500" />
<div>
<div className="text-sm font-medium text-gray-800">
Bắt đầu: {data?.responseData?.start_time
? new Date(
data.responseData.start_time
).toLocaleString()
: "-"}
</div>
<div className="text-sm font-medium text-gray-800">
Kết thúc: {data?.responseData?.end_time
? new Date(
data.responseData.end_time
).toLocaleString()
: "-"}
</div>
</div>
</div>
<div className="text-sm text-gray-500 flex items-center gap-2">
<MapPin className="h-5 w-5 text-blue-600" />
<div className="text-sm font-medium text-gray-800">
Địa điểm: {data?.responseData?.location ??
data?.responseData?.province ??
"-"}
</div>
</div>
<div className="text-sm text-gray-500 flex items-center gap-2">
<CreditCard className="h-5 w-5 text-yellow-400" />
<div className="text-sm font-medium text-gray-800">
Phí tham dự: {data?.responseData?.table_cost
? `${
data.responseData.table_count
} Bàn : ${data.responseData.table_cost.toLocaleString()} đ`
: "Vui lòng xem chi tiết trong bài"}
</div>
</div>
</div>
</div>
</div>
{/* Full description */}
<div className="p-7.5 prose tiptap overflow-hidden">
{parse(data?.responseData?.description ?? "")}
</div>
</>
)}
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
<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;
// Local small component to safely handle Image src fallback without mutating DOM
type EventImageProps = {
src: string;
alt?: string;
};
function EventImage({ src, alt }: EventImageProps) {
const [imgSrc, setImgSrc] = useState<string>(src);
return (
<Image
src={imgSrc}
alt={alt ?? "image"}
fill
className="object-cover"
onError={() => {
// swap to local fallback file when Next/Image fails to load the provided URL
if (imgSrc !== "/img-error.png") setImgSrc("/img-error.png");
}}
/>
);
}
\ No newline at end of file
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import { useGetEvents } from '@api/endpoints/event'
import { EventApiResponse } from '@api/types/event'
import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [page, setPage] = useState(1);
const [filtersString, setFiltersString] = useState<string | undefined>('')
const pageSize = 5;
const { data: allData, isLoading } = useGetEvents<EventApiResponse>({
pageSize: String(pageSize),
currentPage: String(page),
sortField: 'start_time',
sortOrder: 'ASC',
filters: filtersString ?? undefined,
});
return (
<div className="min-h-screen 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-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ả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">
<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
onFilter={(payload) => {
const parts: string[] = []
// query
if (payload.query) parts.push(`name @=${payload.query}`)
const nowIso = new Date().toISOString()
// upcoming / past
if (payload.upcoming && !payload.past) {
parts.push(`start_time>${nowIso}`)
} else if (payload.past && !payload.upcoming) {
parts.push(`start_time<=${nowIso}`)
}
if (payload.fromDate) {
const fromIso = new Date(payload.fromDate).toISOString()
parts.push(`created_at>=${fromIso}`)
}
if (payload.toDate) {
const toIso = new Date(payload.toDate).toISOString()
parts.push(`created_at<=${toIso}`)
}
const filters = parts.length > 0 ? parts.join(',') : undefined
setFiltersString(filters)
setPage(1)
}}
onReset={() => {
setFiltersString(undefined)
setPage(1)
}}
/>
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
}
import { NewsAdminItem } from '@/api/types/news'
import BASE_URL from '@/links'
import dayjs from 'dayjs';
import AppEditorContent from '@/components/shared/editor-content';
import parse from 'html-react-parser'
function CardNews({ news }: { news: NewsAdminItem }) {
return (
<a
href={`${news.id}`}
className="flex flex-col sm:flex-row gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3 hover:shadow-md transition"
>
<img
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`}
alt={news.title}
className="w-full sm:w-40 h-40 sm:h-28 object-cover rounded-md"
/>
<div className="flex-1">
<p className="text-[#0056b3] font-bold text-sm sm:text-base line-clamp-2">
{news.title}
</p>
<p className="text-gray-500 text-xs sm:text-sm my-1">
{dayjs(news.release_at).format('DD/MM/YYYY')}
</p>
<div className="text-xs sm:text-sm text-[#777] line-clamp-3 prose tiptap">
{parse(news.description)}
</div>
</div>
</a>
);
}
export default CardNews;
\ No newline at end of file
"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 Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import React from "react"
type Category = {
title: string
href: string
}
const CATEGORIES: Category[] = [
{ title: "Lợi ích của Hội Viên VCCI", href: "/hoi-vien" },
{ title: "Đăng ký Hội Viên", href: "/hoi-vien/dang-ky-hoi-vien" },
{ title: "Kết nối Hội Viên", href: "/hoi-vien/ket-noi-hoi-vien" },
{ title: "Tin Hội Viên", href: "/hoi-vien/tin-hoi-vien" },
]
const ListCategory: React.FC = () => {
const pathname = usePathname() || ""
const router = useRouter()
const isActive = (href: string) => {
if (href === "/hoi-vien") return pathname === "/hoi-vien"
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
"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";
const Page = () => {
return (
<div className="min-h-screen container mx-auto px-4 sm:px-6 lg:px-8 pb-6">
<div className="flex flex-col gap-5">
<ListCategory />
<main className="bg-white border rounded-md p-5 sm:p-7 lg:py-10 lg:px-40">
<h1 className="text-center mb-4 text-xl sm:text-2xl font-bold text-[#153e8e]">
THỦ TỤC GIA NHẬP HỘI VIÊN CHÍNH THỨC
</h1>
<p className="text-justify leading-7 mb-3">
Điều lệ sửa đổi của Liên đoàn Thương mại và Công nghiệp Việt Nam (VCCI) được Đại hội đại biểu toàn quốc VCCI lần thứ VII thông qua và được Thủ tướng Chính phủ Phê duyệt tại Quyết định số 1496/QĐ-TTg ngày 30/11/2022 đã quy định tất cả các doanh nghiệp, các tổ chức sản xuất, kinh doanh, người sử dụng lao động, các hiệp hội doanh nghiệp có đăng ký và hoạt động hợp pháp ở Việt Nam đều có thể trở thành hội viên của VCCI.
</p>
<p className="text-justify leading-7 mb-3">
Để trở thành hội viên chính thức, tổ chức quan tâm cần gửi VCCI tại Hà Nội hoặc các Chi nhánh, Văn phòng đại diện của VCCI hồ sơ gia nhập gồm:
</p>
<ul className="list-disc pl-6 sm:pl-10 space-y-1 font-bold">
<li className="text-justify leading-7">
Đơn xin gia nhập làm hội viên chính thức VCCI (2 bản theo mẫu của VCCI)
</li>
<li className="text-justify leading-7">
Giấy phép đăng ký kinh doanh, hoặc giấy phép thành lập hoặc quyết định thành lập (2 bản sao).
</li>
</ul>
<p className="text-justify leading-7 my-3">
Khi nhận được đơn, Ban Thường trực sẽ xét và thông báo cho tổ chức liên quan về quyết định kết nạp. Trong vòng 1 tháng kể từ ngày nhận thông báo, tổ chức phải thực hiện đóng lệ phí gia nhập. Chỉ khi nào tổ chức đóng lệ phí gia nhập mới được coi là hội viên chính thức.
</p>
<p className="text-justify leading-7 my-3">
Mức lệ phí gia nhập bằng mức hội phí hàng năm, được tính căn cứ vào doanh số của tổ chức trong năm trước theo các mức:
</p>
<ul className="list-disc pl-6 sm:pl-10 space-y-1 font-bold">
<li className="text-justify leading-7">
Doanh số dưới 10 tỉ đồng đóng 3 triệu đồng/năm
</li>
<li className="text-justify leading-7">
Doanh số từ 10 - 50 tỉ đồng đóng 7 triệu đồng/năm
</li>
<li className="text-justify leading-7">
Doanh số trên 50 tỉ đồng đóng 15 triệu đồng/năm
</li>
</ul>
<p className="text-justify leading-7 my-3">
Mức lệ phí gia nhập và hội phí trên có thể được điều chỉnh bởi quyết định của Ban chấp hành VCCI trong từng thời gian cụ thể.
</p>
<div className="my-4">
<p className="text-[#063e8e] mb-3">
Để biết thêm thông tin chi tiết, vui lòng liên hệ:
</p>
<p className="text-[#063e8e]">
<b>Phòng Hội viên Đào tạo và Truyền thông</b>
<br />
<b>C. Thúy - ĐĐ: 0903 909 756</b>
<span className="block">
Email: luuthanhthuy72@yahoo.com; hoivien@vcci-hcm.org.vn;
</span>
<span className="block">
Điện thoại: 028.3932.0611 - Fax: 028.3932.5472
</span>
<span className="block">
Địa chỉ: P. 306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, Phường Xuân Hoà, TP. Hồ Chí Minh
</span>
</p>
</div>
<p className="text-justify leading-7 my-3 font-semibold">
Biểu mẫu đính kèm:
</p>
<ul className="list-disc pl-6 sm:pl-10 space-y-1">
<li className="text-justify leading-7">
<a
href="https://vcci-hcm.org.vn/wp-content/uploads/2025/08/Don-dang-ky-tham-gia-nhap-hoi-vien-VCCI_Mau-Doanh-nghiep-1.docx"
className="text-[#063e8e] hover:text-yellow-500 italic"
download
>
Đơn đăng ký tham gia nhập hội viên VCCI (Mẫu Doanh nghiệp)
</a>
</li>
<li className="text-justify leading-7">
<a
href="https://vcci-hcm.org.vn/wp-content/uploads/2025/08/Don-dang-ky-tham-gia-nhap-hoi-vien-VCCI_Mau-Hiep-hoi.docx"
className="text-[#063e8e] hover:text-yellow-500 italic"
download
>
Đơn đăng ký tham gia nhập hội viên VCCI (Mẫu Hiệp hội)
</a>
</li>
<li className="text-justify leading-7">
<a
href="https://vcci-hcm.org.vn/wp-content/uploads/2025/08/Huong-dan-ho-so-dang-ky-Hoi-vien-VCCI.docx"
className="text-[#063e8e] hover:text-yellow-500 italic"
download
>
Hướng dẫn hồ sơ đăng ký Hội viên VCCI
</a>
</li>
</ul>
</main>
</div>
</div>
);
};
export default Page;
"use client";
import React, { useState } from "react";
import ListCategory from "./../components/list-category";
import ListFilter from "./../components/list-filter";
import CardNews from "./../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";
export default function Page() {
const [submitSearch,setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category@=Kết nối hội viên` : `category@=Kết nối hội viên`,
});
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-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ải dữ liệu kết nối hội viên...</span>
</div>
) : (!allData || (allData.responseData.rows || []).length === 0) ? (
<div className="py-12 text-center text-gray-600">Không có dữ liệu</div>
) : (
<>
{allData.responseData.rows.map((news) => (
<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>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter onSearch={setSubmitSearch} />
</aside>
</div>
</div>
</div>
);
}
\ No newline at end of file
'use client'
import React from "react";
import ListCategory from "./components/list-category";
import EventCalendar from "@/components/base/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">
<div>
<h1 className="text-2xl font-bold text-[#153e8e]">Lợi ích của Hội viên VCCI</h1>
<hr className="my-5" />
<p className="text-justify leading-7">
Hội viên chính thức là các doanh nghiệp, các tổ chức sản xuất, kinh doanh, người sử dụng lao động, các hiệp hội doanh nghiệp có đăng ký và hoạt động hợp pháp ở Việt Nam. Hội viên chính thức được thừa hưởng những quyền lợi và sau đây:
</p>
<div>
<img src="/hoi-vien/thong-tin.webp" alt="Thông tin" className="w-12 py-5" />
<strong className="block text-2xl text-gray-600 py-3">TIẾNG NÓI</strong>
<ul className="list-disc pl-10 space-y-1">
<li className="text-justify leading-7">Được hỗ trợ giải quyết các vướng mắc, kiến nghị của doanh nghiệp với các cơ quan quản lý trong quá trình kinh doanh và thực thi pháp luật.</li>
<li className="text-justify leading-7">Được tham dự miễn phí các hội nghị, hội thảo, đối thoại với các cơ quan ban ngành về pháp luật và chính sách hàng năm.</li>
<li className="text-justify leading-7">Được tham dự hội nghị đối thoại thường niên về chính sách thuế, hải quan và thủ tục hành chính.</li>
</ul>
</div>
<div>
<img src="/hoi-vien/thong-tin.webp" alt="Thông tin" className="w-12 py-5" />
<strong className="block text-2xl text-gray-600 py-3">ĐỘ NHẬN DIỆN</strong>
<ul className="list-disc pl-10 space-y-1">
<li className="text-justify leading-7">Hồ sơ doanh nghiệp được đăng tải miễn phí trên Danh bạ Hội viên CONNECTIONS.</li>
<li className="text-justify leading-7">Thông tin doanh nghiệp được giới thiệu miễn phí trên website và các kênh truyền thông của VCCI-HCM.</li>
<li className="text-justify leading-7">Cơ hội trở thành nhà tài trợ cho các sự kiện của VCCI-HCM.</li>
<li className="text-justify leading-7">Được ưu đãi đặc biệt khi sử dụng các dịch vụ truyền thông, quảng bá thương hiệu của VCCI-HCM.</li>
</ul>
</div>
<div>
<img src="/hoi-vien/thong-tin.webp" alt="Thông tin" className="w-12 py-5" />
<strong className="block text-2xl text-gray-600 py-3">MẠNG LƯỚI LIÊN KẾT</strong>
<ul className="list-disc pl-10 space-y-1">
<li className="text-justify leading-7">Được tham dự miễn phí các sự kiện xúc tiến thương mại và đầu tư hàng năm.</li>
<li className="text-justify leading-7">Được ưu đãi khi tham gia các đoàn khảo sát thị trường nước ngoài do VCCI-HCM tổ chức.</li>
<li className="text-justify leading-7">Tương tác trong mạng lưới hội viên VCCI- HCM, tiếp cận các đối tác tin cậy và khách hàng tiềm năng.</li>
</ul>
</div>
<div>
<img src="/hoi-vien/thong-tin.webp" alt="Thông tin" className="w-12 py-5" />
<strong className="block text-2xl text-gray-600 py-3">PHÁT TRIỂN</strong>
<ul className="list-disc pl-10 space-y-1">
<li className="text-justify leading-7">Được tham gia các khóa đào tạo của VCCI- HCM với chi phí ưu đãi.</li>
<li className="text-justify leading-7">Được tiếp cận các dự án hỗ trợ doanh nghiệp phát triển được tài trợ bởi các tổ chức trong nước và quốc tế.</li>
<li className="text-justify leading-7">Được tư vấn bởi các chuyên gia về pháp luật, quan hệ lao động, thị trường,… với chi phí ưu đãi.</li>
</ul>
</div>
<div>
<img src="/hoi-vien/thong-tin.webp" alt="Thông tin" className="w-12 py-5" />
<strong className="block text-2xl text-gray-600 py-3">ĐỘ TIN CẬY</strong>
<ul className="list-disc pl-10 space-y-1">
<li className="text-justify leading-7">Tăng uy tín và độ tin cậy cho doanh nghiệp khi trở thành hội viên VCCI-HCM.</li>
<li className="text-justify leading-7">Tạo thuận lợi cho doanh nghiệp trong các hoạt động hợp tác kinh doanh {`-`} đầu tư.</li>
</ul>
</div>
<div>
<img src="/hoi-vien/thong-tin.webp" alt="Thông tin" className="w-12 py-5" />
<strong className="block text-2xl text-gray-600 py-3">THÔNG TIN</strong>
<ul className="list-disc pl-10 space-y-1">
<li className="text-justify leading-7">Được nhận miễn phí Bản tin điện tử phát hành hàng tháng và Tạp chí VCCI-HCM phát hành hàng quý.</li>
<li className="text-justify leading-7">Được nhận miễn phí Danh bạ Hội viên VCCI-HCM.</li>
<li className="text-justify leading-7">Được nhận các sản phẩm thông tin phục vụ cho doanh nghiệp và các nhà đầu tư.</li>
</ul>
</div>
<div>
<p className="text-[#063e8e] py-5">
Để biết thêm thông tin chi tiết, vui lòng liên hệ:
</p>
<p className="text-[#063e8e]">
<b>Phòng Hội viên và Đào tạo:</b>
<br />
<b>C. Thanh Thúy {`-`} DĐ: 0903 909 756</b>
<span className="block">
Email: luuthanhthuy72@yahoo.com; hoivien@vcci-hcm.org.vn;
</span>
<span className="block">
Điện thoại: 028.3932.0611 {`-`} Fax: 028.3932.5472
</span>
<span className="block">
Địa chỉ: P.306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, Quận 3, TP. Hồ Chí Minh
</span>
</p>
</div>
</div >
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
</aside>
</div >
</div >
</div >
);
};
export default Page;
\ No newline at end of file
"use client";
import React, { useState } from "react";
import ListCategory from "./../components/list-category";
import ListFilter from "./../components/list-filter";
import CardNews from "./../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";
export default function Page() {
const [submitSearch,setSubmitSearch] = useState('')
const [page, setPage] = useState(1)
const pageSize = 5
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}category@=Tin hội viên` : 'category@=Tin hội viên',
})
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-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ải tin hội viên...</span>
</div>
) : (!allData || (allData.responseData.rows || []).length === 0) ? (
<div className="py-12 text-center text-gray-600">Không có dữ liệu</div>
) : (
<>
{allData.responseData.rows.map((news) => (
<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>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter onSearch={setSubmitSearch} />
</aside>
</div>
</div>
</div>
);
}
\ No newline at end of file
"use client"; "use client";
import React, { useState, Suspense } from "react"; import React, { useState, Suspense } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category"; import ListCategory from "@/components/base/list-category";
import { OWNER_REPRESENTATIVES_CATEGORIES } from "@constants/categories"; import ListFilter from "@/components/base/list-filter";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter"; import CardNews from "@/components/base/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/news";
import { Spinner } from "@components/ui/spinner"; import { Spinner } from "@components/ui/spinner";
import { PATHS } from "@constants/paths";
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
function SearchContent() { function SearchContent() {
...@@ -22,7 +20,7 @@ function SearchContent() { ...@@ -22,7 +20,7 @@ function SearchContent() {
currentPage: String(page), currentPage: String(page),
filters: query ? `title @=${query}` : undefined, filters: query ? `title @=${query}` : undefined,
}); });
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">
...@@ -49,10 +47,10 @@ function SearchContent() { ...@@ -49,10 +47,10 @@ function SearchContent() {
) : ( ) : (
<> <>
{allData?.responseData.rows.map((news) => ( {allData?.responseData.rows.map((news) => (
<NewsContent <CardNews
key={news.id} key={news.id}
news={news} news={news}
link={`${PATHS.mediaInformation}/tin-vcci/${news.id}`} link={`${news.page_config.static_link}/${news.id}`}
/> />
))} ))}
...@@ -80,11 +78,10 @@ function SearchContent() { ...@@ -80,11 +78,10 @@ function SearchContent() {
{/* Sidebar */} {/* Sidebar */}
<aside className="space-y-6 order-first lg:order-last"> <aside className="space-y-6 order-first lg:order-last">
<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-62 relative bg-gray-100"> <div className="w-full relative bg-gray-100">
<Image <img
src="/banner.webp" src="/banner.webp"
alt="Quảng cáo" alt="Quảng cáo"
fill
className="object-cover" className="object-cover"
/> />
</div> </div>
......
"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 EventCalendar from "@/components/base/event-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="container 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">
<EventCalendar />
<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 } 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>
);
}
import ListCategory from "@/components/base/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@/constants/categories";
import EventFilter from "@app/dai-dien-gioi-chu/components/event-filter";
import Image from "next/image";
import PublicationList from "./components/publicationList";
export default function Page() {
return (
<div className="bg-[#f6f6f6]">
<div className="container m-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">
<PublicationList />
<div className="lg:w-[calc(35%-10px)] w-full">
<EventFilter />
<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>
);
}
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { 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";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category@=Chuyên đề` : 'category@=Chuyên đề',
});
return (
<div className="min-h-screen 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-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ả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">
<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">
<ListFilter onSearch={setSubmitSearch} />
<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>
);
}
"use client";
import React, { useEffect } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { PATHS } from "@constants/paths";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
import { useRouter } from 'next/navigation'
// ...existing code...
export default function Page() {
const router = useRouter()
useEffect(() => {
const firstHref = `${PATHS.mediaInformation}/tin-vcci`
router.push(firstHref)
}, [router])
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
</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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { 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";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData,isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
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 (
<div className="min-h-screen 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-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ải dữ liệu...</span>
</div>
) : (!allData || (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}/thong-tin-chinh-sach-va-phap-luat/${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">
<ListFilter onSearch={setSubmitSearch} />
<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>
);
}
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code...
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination } from "@components/base/pagination";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import Image from "next/image";
import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category @=Thư viện tài liệu'` : 'category @=Thư viện tài liệu',
});
return (
<div className="min-h-screen 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-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ả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">
<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">
<ListFilter onSearch={setSubmitSearch} />
<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>
);
}
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { MEDIA_INFORMATION_CATEGORIES } from "@constants/categories";
// ...existing code...
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 { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch,setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category@=Tin doanh nghiệp` : 'category@=Tin doanh nghiệp',
});
return (
<div className="min-h-screen 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-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ải tin doanh nghiệp...</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}/tin-doanh-nghiep/${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">
<ListFilter onSearch={setSubmitSearch} />
<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>
);
}
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
// 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';
import EventCalendar from '@/components/base/event-calendar'
// ...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">
<EventCalendar />
<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;
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.
...@@ -14,7 +14,7 @@ import { cva } from 'class-variance-authority' ...@@ -14,7 +14,7 @@ import { cva } from 'class-variance-authority'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
export function MenuItem(props: { variant?: 'main' | 'secondary' ; menu: Menu; active?: boolean }) { export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; active?: boolean }) {
const { menu, variant = 'main', active } = props const { menu, variant = 'main', active } = props
const pathname = usePathname() const pathname = usePathname()
......
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