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

[tag]0.1-vcci

parents 29e06f04 1f5f5868
Pipeline #43723 passed with stages
in 4 minutes and 59 seconds
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import links from "./src/links/index";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
images: {
remotePatterns: [
{
protocol: "https",
hostname: links.backendHost,
port: "",
pathname: "/vcci/images/**",
},
],
},
}; };
export default nextConfig; export default nextConfig;
...@@ -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
> => { > => {
......
...@@ -30,6 +30,7 @@ import type { ...@@ -30,6 +30,7 @@ import type {
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import type { import type {
GetNewsPageConfigGetHierarchicalParams,
PutNewsPageConfigCategoryIdBody, PutNewsPageConfigCategoryIdBody,
Response Response
} from '../models'; } from '../models';
...@@ -727,6 +728,214 @@ export const prefetchGetNewsPageConfigGetAllLinksQuery = async <TData = Awaited< ...@@ -727,6 +728,214 @@ export const prefetchGetNewsPageConfigGetAllLinksQuery = async <TData = Awaited<
/**
* Get hierarchical page config
*/
export type getNewsPageConfigGetHierarchicalResponse200 = {
data: Response
status: 200
}
export type getNewsPageConfigGetHierarchicalResponseSuccess = (getNewsPageConfigGetHierarchicalResponse200) & {
headers: Headers;
};
;
export type getNewsPageConfigGetHierarchicalResponse = (getNewsPageConfigGetHierarchicalResponseSuccess)
export const getGetNewsPageConfigGetHierarchicalUrl = (params?: GetNewsPageConfigGetHierarchicalParams,) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : value.toString())
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0 ? `/NewsPageConfig/getHierarchical?${stringifiedParams}` : `/NewsPageConfig/getHierarchical`
}
export const getNewsPageConfigGetHierarchical = async (params?: GetNewsPageConfigGetHierarchicalParams, options?: RequestInit): Promise<getNewsPageConfigGetHierarchicalResponse> => {
return useCustomClient<getNewsPageConfigGetHierarchicalResponse>(getGetNewsPageConfigGetHierarchicalUrl(params),
{
...options,
method: 'GET'
}
);}
export const getGetNewsPageConfigGetHierarchicalInfiniteQueryKey = (params?: GetNewsPageConfigGetHierarchicalParams,) => {
return [
'infinite', `/NewsPageConfig/getHierarchical`, ...(params ? [params]: [])
] as const;
}
export const getGetNewsPageConfigGetHierarchicalQueryKey = (params?: GetNewsPageConfigGetHierarchicalParams,) => {
return [
`/NewsPageConfig/getHierarchical`, ...(params ? [params]: [])
] as const;
}
export const getGetNewsPageConfigGetHierarchicalInfiniteQueryOptions = <TData = InfiniteData<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>, TError = ErrorType<unknown>>(params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetNewsPageConfigGetHierarchicalInfiniteQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>> = ({ signal }) => getNewsPageConfigGetHierarchical(params, { signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions} as UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetNewsPageConfigGetHierarchicalInfiniteQueryResult = NonNullable<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>
export type GetNewsPageConfigGetHierarchicalInfiniteQueryError = ErrorType<unknown>
export function useGetNewsPageConfigGetHierarchicalInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>, TError = ErrorType<unknown>>(
params: undefined | GetNewsPageConfigGetHierarchicalParams, options: { query:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>,
TError,
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): DefinedUseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetNewsPageConfigGetHierarchicalInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>, TError = ErrorType<unknown>>(
params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>,
TError,
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetNewsPageConfigGetHierarchicalInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>, TError = ErrorType<unknown>>(
params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetNewsPageConfigGetHierarchicalInfinite<TData = InfiniteData<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>, TError = ErrorType<unknown>>(
params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetNewsPageConfigGetHierarchicalInfiniteQueryOptions(params,options)
const query = useInfiniteQuery(queryOptions, queryClient) as UseInfiniteQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
export const prefetchGetNewsPageConfigGetHierarchicalInfiniteQuery = async <TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(
queryClient: QueryClient, params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseInfiniteQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
): Promise<QueryClient> => {
const queryOptions = getGetNewsPageConfigGetHierarchicalInfiniteQueryOptions(params,options)
await queryClient.prefetchInfiniteQuery(queryOptions);
return queryClient;
}
export const getGetNewsPageConfigGetHierarchicalQueryOptions = <TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetNewsPageConfigGetHierarchicalQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>> = ({ signal }) => getNewsPageConfigGetHierarchical(params, { signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetNewsPageConfigGetHierarchicalQueryResult = NonNullable<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>>
export type GetNewsPageConfigGetHierarchicalQueryError = ErrorType<unknown>
export function useGetNewsPageConfigGetHierarchical<TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(
params: undefined | GetNewsPageConfigGetHierarchicalParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>,
TError,
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetNewsPageConfigGetHierarchical<TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(
params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>,
TError,
Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>
> , 'initialData'
>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetNewsPageConfigGetHierarchical<TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(
params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetNewsPageConfigGetHierarchical<TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(
params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetNewsPageConfigGetHierarchicalQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
export const prefetchGetNewsPageConfigGetHierarchicalQuery = async <TData = Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError = ErrorType<unknown>>(
queryClient: QueryClient, params?: GetNewsPageConfigGetHierarchicalParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getNewsPageConfigGetHierarchical>>, TError, TData>>, request?: SecondParameter<typeof useCustomClient>}
): Promise<QueryClient> => {
const queryOptions = getGetNewsPageConfigGetHierarchicalQueryOptions(params,options)
await queryClient.prefetchQuery(queryOptions);
return queryClient;
}
/** /**
* Get * Get
*/ */
......
...@@ -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-7">
<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="p-7.5 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
...@@ -6,7 +6,7 @@ import AppEditorContent from '@/components/shared/editor-content'; ...@@ -6,7 +6,7 @@ import AppEditorContent from '@/components/shared/editor-content';
function CardEvent({ event }: { event: EventItem }) { function CardEvent({ event }: { event: EventItem }) {
return ( return (
<a <a
href={`${event.id}`} href={`hoat-dong/su-kien/${event.id}`}
className='flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3 p-2 sm:p-3 border border-gray-200 bg-white rounded-md' className='flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3 p-2 sm:p-3 border border-gray-200 bg-white rounded-md'
> >
<img <img
......
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,32 +14,68 @@ import CardNews from "./components/card-news"; ...@@ -14,32 +14,68 @@ 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);
// query
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: '999' } {
pageSize: '5',
filters: tab === "all" ? `` : `category @=${tab}`,
}
);
const { data: newsAll, isLoading: isLoadingNewsAll } = useGetNews<GetNewsResponseType>(
{
pageSize: '10',
}
);
const { data: businessOpportunities, isLoading: isLoadingBusinessOpportunities } = useGetNews<GetNewsResponseType>(
{
pageSize: '5',
filters: `category @=Cơ hội kinh doanh`,
}
);
const { data: policyAndLegalInformation, isLoading: isLoadingPolicyAndLegalInformation } = useGetNews<GetNewsResponseType>(
{
pageSize: '5',
filters: `category @=Thông tin chính sách và pháp luật`,
}
); );
const { data: eventData, isLoading: isLoadingEvent } = useGetEvents<EventApiResponse>(); const { data: eventData, isLoading: isLoadingEvent } = useGetEvents<EventApiResponse>();
// filter category // helpers
const rows = newsData?.responseData?.rows ?? []; const stripImagesAndHtml = (html?: string) => {
const filteredRows = if (!html) return ''
tab === "all" ? rows : rows.filter((n) => n.category === tab); // 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",
...@@ -72,7 +108,7 @@ const Page = () => { ...@@ -72,7 +108,7 @@ const Page = () => {
]; ];
return ( return (
(isLoadingNews || isLoadingCategory || isLoadingEvent) ? ( (isLoadingBusinessOpportunities || isLoadingPolicyAndLegalInformation || isLoadingCategory || isLoadingEvent) ? (
<div className="container w-full h-[80vh] flex justify-center items-center"> <div className="container w-full h-[80vh] flex justify-center items-center">
<Spinner /> <Spinner />
</div> </div>
...@@ -131,16 +167,20 @@ const Page = () => { ...@@ -131,16 +167,20 @@ const Page = () => {
}} }}
className="pb-5" className="pb-5"
> >
{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">
...@@ -180,12 +220,12 @@ const Page = () => { ...@@ -180,12 +220,12 @@ const Page = () => {
<hr className="border-[#063e8e] mb-4" /> <hr className="border-[#063e8e] mb-4" />
<div className="flex flex-col md:flex-row gap-5"> <div className="flex flex-col md:flex-row gap-5">
{newsData?.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">
...@@ -193,20 +233,21 @@ const Page = () => { ...@@ -193,20 +233,21 @@ 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>
<div className="flex-1 p-5"> <div className="flex-1 p-5 pt-0">
<p className="text-[#063E8E] font-bold text-xl line-clamp-2"> <p className="text-[#063E8E] font-bold text-xl line-clamp-2">
{news.title} {news.title}
</p> </p>
<p className="text-gray-500 text-sm my-1"> <p className="text-gray-500 text-sm">
{dayjs(news.release_at).format("DD/MM/YYYY")} {dayjs(news.release_at).format("DD/MM/YYYY")}
</p> </p>
<AppEditorContent <p className="line-clamp-4">{stripImagesAndHtml(news.description)}</p>
className="line-clamp-4"
value={news.description}
/>
</div> </div>
</a> </a>
))} ))}
...@@ -238,7 +279,7 @@ const Page = () => { ...@@ -238,7 +279,7 @@ const Page = () => {
))} ))}
</div> </div>
{filteredRows.slice(0, 4).map((news) => ( {newsData?.responseData?.rows.slice(0, 4).map((news) => (
<CardNews key={news.id} news={news} /> <CardNews key={news.id} news={news} />
))} ))}
</div> </div>
...@@ -299,7 +340,7 @@ const Page = () => { ...@@ -299,7 +340,7 @@ const Page = () => {
<h2 className="text-[18px] sm:text-[20px] font-bold uppercase text-[#e8c518]"> <h2 className="text-[18px] sm:text-[20px] font-bold uppercase text-[#e8c518]">
Sự kiện sắp diễn ra Sự kiện sắp diễn ra
</h2> </h2>
<a href="#" className="text-[#e8c518] text-sm sm:text-base"> <a href="/hoat-dong/su-kien" className="text-[#e8c518] text-sm sm:text-base">
<ChevronsRight /> <ChevronsRight />
</a> </a>
</div> </div>
...@@ -311,7 +352,7 @@ const Page = () => { ...@@ -311,7 +352,7 @@ const Page = () => {
.map((event: EventItem) => ( .map((event: EventItem) => (
<a <a
key={event.id} key={event.id}
href={`${event.id}`} href={`hoat-dong/su-kien/${event.id}`}
className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3" className="flex flex-col w-full md:w-1/2 min-h-[180px] sm:min-h-[220px] gap-3 mb-3 border border-gray-200 bg-white rounded-md p-3"
> >
<div className="w-full aspect-3/2 overflow-hidden"> <div className="w-full aspect-3/2 overflow-hidden">
...@@ -319,6 +360,10 @@ const Page = () => { ...@@ -319,6 +360,10 @@ const Page = () => {
src={`${BASE_URL.imageEndpoint}${event.image}`} src={`${BASE_URL.imageEndpoint}${event.image}`}
alt={event.name} alt={event.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).src =
"/img-error.png";
}}
/> />
</div> </div>
...@@ -329,10 +374,7 @@ const Page = () => { ...@@ -329,10 +374,7 @@ const Page = () => {
<p className="text-gray-500 text-sm my-1"> <p className="text-gray-500 text-sm my-1">
{dayjs(event.start_time).format("DD/MM/YYYY")} {dayjs(event.start_time).format("DD/MM/YYYY")}
</p> </p>
<AppEditorContent <p className="line-clamp-3">{stripImagesAndHtml(event.description)}</p>
className="line-clamp-3"
value={event.description}
/>
</div> </div>
</a> </a>
))} ))}
...@@ -350,7 +392,7 @@ const Page = () => { ...@@ -350,7 +392,7 @@ const Page = () => {
Lịch sự kiện Lịch sự kiện
</h2> </h2>
<a <a
href="#" href="/hoat-dong/su-kien"
className="text-[#e8c518] hover:underline text-sm sm:text-base" className="text-[#e8c518] hover:underline text-sm sm:text-base"
> >
<ChevronsRight /> <ChevronsRight />
...@@ -388,15 +430,19 @@ const Page = () => { ...@@ -388,15 +430,19 @@ const Page = () => {
</div> </div>
<hr className="border-[#063e8e] mb-4" /> <hr className="border-[#063e8e] mb-4" />
<div className="pt-2"> <div className="pt-2">
{newsData?.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">
...@@ -407,7 +453,7 @@ const Page = () => { ...@@ -407,7 +453,7 @@ const Page = () => {
</a> </a>
))} ))}
{rows.slice(0, 3).map((news) => ( {businessOpportunities?.responseData.rows.slice(0, 3).map((news) => (
<CardNews key={news.id} news={news} /> <CardNews key={news.id} news={news} />
))} ))}
</div> </div>
...@@ -429,15 +475,19 @@ const Page = () => { ...@@ -429,15 +475,19 @@ const Page = () => {
</div> </div>
<hr className="border-[#063e8e] mb-4" /> <hr className="border-[#063e8e] mb-4" />
<div className="pt-2"> <div className="pt-2">
{newsData?.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">
...@@ -448,7 +498,7 @@ const Page = () => { ...@@ -448,7 +498,7 @@ const Page = () => {
</a> </a>
))} ))}
{rows.slice(0, 3).map((news) => ( {policyAndLegalInformation?.responseData.rows.slice(0, 3).map((news) => (
<CardNews key={news.id} news={news} /> <CardNews key={news.id} news={news} />
))} ))}
</div> </div>
......
"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>
);
}
...@@ -21,11 +21,10 @@ export default function ScrollToTopButton() { ...@@ -21,11 +21,10 @@ export default function ScrollToTopButton() {
return ( return (
<button <button
onClick={scrollToTop} onClick={scrollToTop}
className={`fixed bottom-25 right-6 bg-[#e8c518] hover:text-[#063e8e] text-white p-3 rounded-lg shadow-lg transition-all duration-500 cursor-pointer ${ className={`fixed z-50 bottom-20 right-8 bg-[#e8c518] hover:text-[#063e8e] text-white p-3 rounded-lg shadow-lg transition-all duration-500 cursor-pointer ${visible
visible ? "opacity-100 translate-y-0"
? "opacity-100 translate-y-0" : "opacity-0 translate-y-3 pointer-events-none"
: "opacity-0 translate-y-3 pointer-events-none" }`}
}`}
> >
<ChevronsUp size={24} /> <ChevronsUp size={24} />
</button> </button>
......
...@@ -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,163 +85,19 @@ function Header() { ...@@ -80,163 +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}
{ title: "Về VCCI-HCM", link: "ve-vcci-hcm" }, items={[
{ ...category.children.map((child) => ({
title: "Chức Năng Và Nhiệm Vụ", title: child.name,
link: "chuc-nang-va-nhiem-vu", link: child.static_link,
}, })),
{ 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 */}
...@@ -251,26 +112,19 @@ function Header() { ...@@ -251,26 +112,19 @@ function Header() {
{/* Mobile Menu */} {/* Mobile Menu */}
<div <div
className={`lg:hidden bg-white shadow-lg transition-all duration-300 overflow-hidden ${ className={`lg:hidden bg-white shadow-lg transition-all duration-300 overflow-hidden ${toggleMenu ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"
toggleMenu ? "max-h-96 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 { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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] = 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,
filters:'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?.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 />
<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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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] = 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 />
<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";
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams();
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(
id as string
);
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={OWNER_REPRESENTATIVES_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className="pb-5 text-primary text-2xl leading-normal font-medium">
{data?.responseData?.title}
</div>
<hr className="py-2" />
<div className="p-7.5 prose tiptap overflow-hidden">
{parse(data?.responseData?.description ?? "")}
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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?.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 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/chuc-nang-nhiem-vu/qddt_01.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_02.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_03.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_04.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_05.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_06.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_07.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_08.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_09.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_10.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_11.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_12.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_13.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_14.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_15.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_16.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_17.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_18.jpg',
'/gioi-thieu/chuc-nang-nhiem-vu/qddt_19.jpg',
];
return (
<div className="min-h-screen container mx-auto px-4 sm:px-6 lg:px-0 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]">Chức năng và Nhiệm vụ</h1>
<hr className="my-5" />
<div className="flex flex-col justify-center items-center">
<div className="max-w-5xl mb-5 mx-auto space-y-10">
{/* chức năng */}
<section>
<h2 className="text-xl sm:text-2xl mb-4">Chức năng</h2>
<p className="mb-4">
Liên đoàn Thương mại và Công nghiệp Việt Nam có các chức năng sau:
</p>
<ol className="list-decimal list-inside pl-5 sm:pl-6 md:pl-8 space-y-2">
<li>Đại diện để thúc đẩy và bảo vệ quyền lợi hợp pháp, chính đáng của cộng đồng doanh nghiệp Việt Nam trong các quan hệ trong nước và quốc tế;</li>
<li>Thúc đẩy sự phát triển của cộng đồng doanh nghiệp; xúc tiến và hỗ trợ các hoạt động thương mại, đầu tư, hợp tác khoa học – công nghệ và các hoạt động kinh doanh khác của cộng đồng doanh nghiệp ở Việt Nam và nước ngoài; xúc tiến, thúc đẩy xây dựng mối quan hệ lao động hài hòa trong các doanh nghiệp.</li>
</ol>
</section>
{/* nhiệm vụ */}
<section>
<h2 className="text-xl sm:text-2xl mb-4">Nhiệm vụ</h2>
<p className="mb-4">
Liên đoàn Thương mại và Công nghiệp Việt Nam có các nhiệm vụ sau:
</p>
<ol className="list-decimal list-inside pl-5 sm:pl-6 md:pl-8 space-y-2">
<li>Tập hợp, nghiên cứu thực trạng và kiến nghị với Đảng và Nhà nước các vấn đề về pháp luật, chính sách, chiến lược phát triển kinh tế – xã hội nhằm cải thiện môi trường kinh doanh và xây dựng quan hệ lao động hài hòa; tổ chức các diễn đàn, đối thoại, các cuộc tiếp xúc, làm đầu mối liên kết các doanh nghiệp, làm cầu nối giữa cộng đồng doanh nghiệp với các cơ quan Đảng, Nhà nước và với các tổ chức hữu quan khác ở trong và ngoài nước để trao đổi thông tin, ý kiến và đề xuất các giải pháp xử lý vướng mắc, hoàn thiện chính sách, pháp luật liên quan đến doanh nghiệp, môi trường kinh doanh và quan hệ lao động.</li>
<li>Đại diện cho cộng đồng doanh nghiệp tham gia vào quá trình xây dựng ban hành văn bản quy phạm pháp luật có liên quan đến doanh nghiệp, hoạt động kinh doanh và quan hệ lao động dưới các hình thức khác nhau theo quy định hiện hành.</li>
<li>Là đầu mối tập hợp thông tin, ý kiến của cộng đồng doanh nghiệp, tổ chức và tham gia quá trình tham vấn với các đoàn đàm phán về kinh tế, thương mại; tham gia với cơ quan Nhà nước có thẩm quyền trong quá trình đàm phán, ký kết, gia nhập, phê chuẩn, thực thi các điều ước quốc tế có liên quan tới kinh tế, thương mại; hỗ trợ cộng đồng doanh nghiệp trong hội nhập kinh tế quốc tế, đặc biệt là các vấn đề liên quan đến thực thi các điều ước quốc tế về kinh tế, thương mại mà Việt Nam là thành viên; tham gia tổ chức các đoàn doanh nghiệp tháp tùng lãnh đạo Đảng, Nhà nước; tổ chức các Diễn đàn doanh nghiệp Việt Nam, Hội đồng doanh nghiệp Việt Nam với doanh nghiệp các nước và các hoạt động xúc tiến khác nhằm mở rộng quan hệ thương mại, đầu tư quốc tế.</li>
<li>Thực hiện vai trò của tổ chức đại diện ở Trung ương của người sử dụng lao động Việt Nam tham gia vào các thiết chế ba bên về quan hệ lao động, hướng dẫn, hỗ trợ xây dựng và liên kết tổ chức của người sử dụng lao động ở cấp ngành và địa phương; phối hợp với tổ chức đại diện người lao động và các cơ quan, đơn vị hữu quan để hỗ trợ doanh nghiệp, giới sử dụng lao động xây dựng quan hệ lao động hài hòa, ổn định và tiến bộ theo quy định hiện hành.</li>
<li>Tiến hành những hoạt động để bảo vệ quyền lợi hợp pháp của cộng đồng doanh nghiệp trong các quan hệ kinh doanh trong nước và quốc tế; tư vấn và tham gia hỗ trợ giải quyết các vướng mắc, kiến nghị của cộng đồng 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>Tổ chức tuyên truyền, phổ biến, tư vấn thực thi chính sách, pháp luật; phổ biến, cung cấp, hỗ trợ thông tin kinh doanh, khoa học kỹ thuật cho cộng đồng doanh nghiệp.</li>
<li>Tổ chức vận động cộng đồng doanh nghiệp nâng cao trách nhiệm xã hội, xây dựng đạo đức và văn hóa kinh doanh, bảo vệ môi trường và tham gia các hoạt động xã hội khác liên quan tới hoạt động của Phòng Thương mại và Công nghiệp Việt Nam phù hợp với quy định của pháp luật.</li>
<li>Hỗ trợ việc thành lập, phối hợp nâng cao năng lực hoạt động và liên kết hệ thống các hiệp hội doanh nghiệp trong cả nước.</li>
<li>Hợp tác với các tổ chức, đơn vị hữu quan trong nước; hợp tác với các tổ chức của cộng đồng doanh nghiệp ở nước ngoài, ký, thực hiện các thỏa thuận hợp tác quốc tế, tham gia các tổ chức quốc tế phù hợp với quy định của pháp luật.</li>
<li>Tổ chức đào tạo để phát triển, bồi dưỡng nguồn nhân lực cho các doanh nghiệp, hiệp hội doanh nghiệp, nâng cao năng lực quản lý, kinh doanh cho các doanh nhân, xây dựng đội ngũ doanh nhân năng động, hiệu quả.</li>
<li>Tiến hành các hoạt động nhằm xây dựng, quảng bá thương hiệu và nâng cao uy tín hàng hóa, dịch vụ, cộng đồng doanh nghiệp và môi trường kinh doanh tại Việt Nam.</li>
<li>Tổ chức các hoạt động hỗ trợ phát triển kinh doanh; hỗ trợ doanh nghiệp tiếp cận các nguồn lực phát triển; hỗ trợ doanh nghiệp phát triển quan hệ kinh doanh và đầu tư ở trong và ngoài nước thông qua các biện pháp như: kết nối và giới thiệu bạn hàng, cung cấp thông tin, hướng dẫn và tư vấn cho doanh nghiệp, tổ chức nghiên cứu, khảo sát thị trường, hội thảo, hội nghị, hội chợ, triển lãm, quảng cáo và các hoạt động xúc tiến thương mại và đầu tư khác ở trong nước và nước ngoài theo quy định của pháp luật.</li>
<li>Tổ chức nghiên cứu, thử nghiệm, triển khai, chuyển giao các mô hình kinh doanh mới hỗ trợ phát triển doanh nghiệp, hiệp hội doanh nghiệp; thực hiện các đề tài, nghiên cứu, điều tra… về năng lực cạnh tranh, lao động và các nội dung khác nhằm nâng cao năng lực cạnh tranh và phát triển doanh nghiệp, hiệp hội doanh nghiệp.</li>
<li>Chủ trì hoặc phối hợp tổ chức thực hiện các hoạt động tôn vinh, khen thưởng doanh nghiệp, doanh nhân, các đơn vị, cá nhân có đóng góp lớn vào sự phát triển của cộng đồng doanh nghiệp và nền kinh tế theo quy định của pháp luật.</li>
<li>Hỗ trợ đăng ký và bảo hộ quyền sở hữu trí tuệ và chuyển giao công nghệ ở Việt Nam và ở nước ngoài theo quy định.</li>
<li>Cấp giấy chứng nhận xuất xứ cho hàng hóa xuất khẩu của Việt Nam theo ủy quyền của cơ quan Nhà nước có thẩm quyền; xác nhận các trường hợp bất khả kháng và chứng nhận, xác nhận các giấy tờ cần thiết khác trong hoạt động thương mại theo yêu cầu tự nguyện của các bên trong giao dịch hoặc theo yêu cầu, ủy quyền của các cơ quan, tổ chức có thẩm quyền ở trong và ngoài nước.</li>
<li>Hỗ trợ các doanh nghiệp trong và ngoài nước giải quyết bất đồng, tranh chấp thông qua thương lượng, hòa giải hoặc trọng tài phù hợp với quy định của pháp luật.</li>
<li>Thực hiện các nhiệm vụ khác mà cơ quan Nhà nước giao hoặc ủy quyền.</li>
</ol>
</section>
</div>
<p className="mb-4 font-bold">
ĐIỀU LỆ VCCI:
</p>
<div className="pb-5 w-full">
<ImageGallery images={images} />
</div>
</div>
</main>
</div>
</div>
);
};
export default Page;
"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-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
"use client";
import React from "react";
import ReactCountryFlag from "react-country-flag";
type Region = { id: string; name: string; image?: string };
export const DEFAULT_REGIONS: Region[] = [
{ id: "dong-nam-a", name: "Đông Nam Á", image: "/Dong-Nam-A-scaled.webp" },
{ id: "dong-bac-a", name: "Đông Bắc Á", image: "/Dong-Bac-A-scaled.webp" },
{ id: "nam-a", name: "Nam Á", image: "/Nam-a-scaled.webp" },
{ id: "tay-a", name: "Tây Á", image: "/Tay-a-scaled.webp" },
{ id: "bac-my", name: "Bắc Mỹ", image: "/Bac-My-1-scaled.webp" },
{ id: "nam-my", name: "Nam Mỹ", image: "/Nam-My-1-scaled.webp" },
{ id: "chau-au", name: "Châu Âu", image: "/Chau-Au-scaled.jpg.webp" },
{ id: "chau-uc", name: "Châu Úc", image: "/Chau-Uc-scaled.jpg.webp" },
{ id: "chau-phi", name: "Châu Phi", image: "/Chau-Phi-1-scaled.jpg.webp" },
];
type Props = {
imageSrc?: string; // relative to /public — default `images/map-se-asia.png`
regions?: Region[];
// controlled props (optional)
active?: string | null;
onSelect?: (id: string) => void;
};
export default function MapRegion({
imageSrc = "/images/map-se-asia.png",
regions = DEFAULT_REGIONS,
active: activeProp,
}: Props) {
// controlled usage: prefer activeProp; fallback to first region id
const active = activeProp ?? regions[0]?.id ?? null;
const activeRegion = regions.find((r) => r.id === active);
const displayedImage = activeRegion?.image ?? imageSrc;
return (
<div className="max-w-6xl mx-auto">
<div className="bg-white shadow-sm rounded-md overflow-hidden p-6">
<div className="flex flex-col gap-6">
<div>
<h3 className="text-2xl font-semibold text-primary mb-0">
{activeRegion?.name ?? ""}
</h3>
</div>
<div className="w-full flex items-center justify-center">
<div className="w-full max-w-3xl">
<div className="bg-gray-50 p-4 flex items-center justify-center">
<div className="relative">
<img
src={displayedImage}
alt={activeRegion?.name ?? "Map"}
className="w-full h-auto object-contain block mx-auto"
style={{ maxHeight: 520 }}
/>
{/* Country Flags*/}
{active == "dong-nam-a" && (
<>
<div className="absolute top-[17%] left-[29%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="MM"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[23%] left-[40%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="LA"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[20%] left-[43%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="VN"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[38%] left-[43%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="KH"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[27%] left-[68%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="PH"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[55%] left-[40%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="MY"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[55%] left-[57%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="BN"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[62%] left-[47%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="SG"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[65%] left-[57%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="ID"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[32%] left-[36%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="TH"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
</>
)}
{active == "dong-bac-a" && (
<>
<div className="absolute top-[40%] left-[60%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="CN"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[37%] left-[77%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="KR"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[34%] left-[87%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="JP"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[60%] left-[73%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="TW"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
</>
)}
{active == "nam-a" && (
<>
<div className="absolute top-[40%] left-[60%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="IN"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
</>
)}
{active == "bac-my" && (
<>
<div className="absolute top-[60%] left-[40%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="US"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
<div className="absolute top-[40%] left-[40%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="CA"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
</>
)}
{active == "chau-uc" && (
<>
<div className="absolute top-[30%] left-[40%] w-4 h-4 md:w-6 md:h-6 rounded-full">
<ReactCountryFlag
countryCode="AU"
svg
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
className="rounded-full"
/>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
"use client"
import MapRegion, { DEFAULT_REGIONS } from "./components/map-region"
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
// ...existing code...
export default function Page() {
const [active, setActive] = useState<string | null>(DEFAULT_REGIONS[0]?.id ?? null)
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<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">
<MapRegion active={active} />
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<div className="bg-white border rounded-md p-6">
<h3 className="text-2xl font-semibold text-primary mb-3">KHU VỰC</h3>
<nav aria-label="Khu vực" className="space-y-2">
{DEFAULT_REGIONS.map((r) => {
const isActive = r.id === active
return (
<button
key={r.id}
onClick={() => setActive(r.id)}
className={`w-full text-left px-3 py-2 flex items-center justify-between transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-primary-foreground ${
isActive
? "border-2 border-primary-foreground"
: "hover:bg-muted hover:text-primary-foreground"
}`}
aria-current={isActive ? "true" : undefined}
>
<span className={`text-sm ${isActive ? "text-primary-foreground font-semibold" : "text-primary"}`}>
{r.name}
</span>
</button>
)
})}
</nav>
</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 { EVENT_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import { useGetNewsId } from '@/api/endpoints/news';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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 pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch}` : '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?.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 />
<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 ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { EVENT_CATEGORIES } from "@constants/categories";
import ListFilter from "@app/dai-dien-gioi-chu/components/list-filter";
import {useGetEventsId} from '@/api/endpoints/event';
import parse from "html-react-parser";
import { useParams } from 'next/navigation'
import { GetNewsDetailResponseType } from '@lib/types/news-detail-response-data';
import {GetEventsIdQueryResponseType} from '@api/types/event';
import { Spinner } from "@components/ui/spinner";
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetEventsId<GetEventsIdQueryResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={EVENT_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
{isLoading ? (
<div className="flex justify-center items-center py-12">
<Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tải chi tiết sự kiện...</span>
</div>
) : (
<>
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.name}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</>
)}
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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(`title @=${payload.query}`)
const nowIso = new Date().toISOString()
// upcoming / past
if (payload.upcoming && !payload.past) {
parts.push(`start_time>${nowIso}`)
} else if (payload.past && !payload.upcoming) {
parts.push(`start_time<=${nowIso}`)
}
if (payload.fromDate) {
const fromIso = new Date(payload.fromDate).toISOString()
parts.push(`created_at>=${fromIso}`)
}
if (payload.toDate) {
const toIso = new Date(payload.toDate).toISOString()
parts.push(`created_at<=${toIso}`)
}
const filters = parts.length > 0 ? parts.join(',') : undefined
setFiltersString(filters)
setPage(1)
}}
onReset={() => {
setFiltersString(undefined)
setPage(1)
}}
/>
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<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] = 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`,
});
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?.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 />
</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] = 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',
})
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?.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 />
</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>
......
...@@ -5,14 +5,14 @@ export type SiteMapItem = { ...@@ -5,14 +5,14 @@ export type SiteMapItem = {
}; };
export const MOCK_SITEMAP: SiteMapItem[] = [ export const MOCK_SITEMAP: SiteMapItem[] = [
{ label: "Trang chủ", url: "/homepage", children: [] }, { label: "Trang chủ", url: "", children: [] },
{ {
label: "Giới thiệu", label: "Giới thiệu",
url: "/gioi-thieu", url: "/gioi-thieu",
children: [ children: [
{ label: "Về VCCI-HCM", url: "/gioi-thieu/ve-vcci-hcm", children: [] }, { label: "Về VCCI-HCM", url: "/gioi-thieu", children: [] },
{ label: "Chức năng và nhiệm vụ", url: "/gioi-thieu/chuc-nang-nhiem-vu", children: [] }, { label: "Chức năng và nhiệm vụ", url: "/gioi-thieu/chuc-nang-va-nhiem-vu", children: [] },
{ label: "Sơ đồ tổ chức", url: "/gioi-thieu/so-do-to-chuc", children: [] }, { label: "Sơ đồ tổ chức", url: "/gioi-thieu/so-do-to-chuc", children: [] },
{ label: "Dịch vụ cung cấp", url: "/gioi-thieu/dich-vu-cung-cap", children: [] }, { label: "Dịch vụ cung cấp", url: "/gioi-thieu/dich-vu-cung-cap", children: [] },
], ],
...@@ -22,7 +22,7 @@ export const MOCK_SITEMAP: SiteMapItem[] = [ ...@@ -22,7 +22,7 @@ export const MOCK_SITEMAP: SiteMapItem[] = [
label: "Hội viên", label: "Hội viên",
url: "/hoi-vien", url: "/hoi-vien",
children: [ children: [
{ label: "Lợi ích của hội viên VCCI", url: "/hoi-vien/loi-ich-hoi-vien", children: [] }, { label: "Lợi ích của hội viên VCCI", url: "/hoi-vien", children: [] },
{ label: "Đăng ký hội viên", url: "/hoi-vien/dang-ky-hoi-vien", children: [] }, { label: "Đăng ký hội viên", url: "/hoi-vien/dang-ky-hoi-vien", children: [] },
{ label: "Kết nối hội viên", url: "/hoi-vien/ket-noi-hoi-vien", children: [] }, { label: "Kết nối hội viên", url: "/hoi-vien/ket-noi-hoi-vien", children: [] },
{ label: "Tin hội viên", url: "/hoi-vien/tin-hoi-vien", children: [] }, { label: "Tin hội viên", url: "/hoi-vien/tin-hoi-vien", children: [] },
...@@ -43,13 +43,13 @@ export const MOCK_SITEMAP: SiteMapItem[] = [ ...@@ -43,13 +43,13 @@ export const MOCK_SITEMAP: SiteMapItem[] = [
url: "/xuat-xu-hang-hoa", url: "/xuat-xu-hang-hoa",
children: [ children: [
{ label: "Định nghĩa chung", url: "/xuat-xu-hang-hoa", children: [] }, { label: "Định nghĩa chung", url: "/xuat-xu-hang-hoa", children: [] },
{ label: "Mục đích của C/O", url: "/xuat-xu-hang-hoa/muc-dich-co", children: [] }, { label: "Mục đích của C/O", url: "/xuat-xu-hang-hoa/muc-dich", children: [] },
{ label: "Luật áp dụng về C/O", url: "/xuat-xu-hang-hoa/luat-ap-dung-co", children: [] }, { label: "Luật áp dụng về C/O", url: "/xuat-xu-hang-hoa/luat-ap-dung", children: [] },
{ label: "Thủ tục cấp C/O", url: "/xuat-xu-hang-hoa/thu-tuc-cap-co", children: [] }, { label: "Thủ tục cấp C/O", url: "/xuat-xu-hang-hoa/thu-tuc-cap", children: [] },
{ label: "Biểu mẫu C/O và cách khai", url: "/xuat-xu-hang-hoa/bieu-mau-co", children: [] }, { label: "Biểu mẫu C/O và cách khai", url: "/xuat-xu-hang-hoa/bieu-mau-c-o-va-cach-khai", children: [] },
{ label: "Phí và lệ phí cấp C/O", url: "/xuat-xu-hang-hoa/phi-le-phi-cap-co", children: [] }, { label: "Phí và lệ phí cấp C/O", url: "/xuat-xu-hang-hoa/phi-le-phi-cap", children: [] },
{ label: "Điểm cấp và thời gian cấp C/O", url: "/xuat-xu-hang-hoa/diem-cap-thoi-gian", children: [] }, { label: "Điểm cấp và thời gian cấp C/O", url: "/xuat-xu-hang-hoa/diem-cap-va-thoi-gian-cap", children: [] },
{ label: "Thông tin liên hệ", url: "/xuat-xu-hang-hoa/lien-he", children: [] }, { label: "Thông tin liên hệ", url: "/xuat-xu-hang-hoa/thong-tin-lien-he", children: [] },
], ],
}, },
...@@ -57,8 +57,8 @@ export const MOCK_SITEMAP: SiteMapItem[] = [ ...@@ -57,8 +57,8 @@ export const MOCK_SITEMAP: SiteMapItem[] = [
label: "Đại diện giới chủ", label: "Đại diện giới chủ",
url: "/dai-dien-gioi-chu", url: "/dai-dien-gioi-chu",
children: [ children: [
{ label: "Chức năng đại diện người sử dụng lao động", url: "/dai-dien-gioi-chu/chuc-nang", children: [] }, { label: "Chức năng đại diện người sử dụng lao động", url: "/dai-dien-gioi-chu/chuc-nang-dai-dien-nguoi-su-dung-lao-dong", children: [] },
{ label: "Sự kiện - tập huấn NSDLĐ", url: "/dai-dien-gioi-chu/su-kien-tap-huan", children: [] }, { label: "Sự kiện - tập huấn NSDLĐ", url: "/dai-dien-gioi-chu/tap-huan-nsdld", children: [] },
{ label: "Tin liên quan", url: "/dai-dien-gioi-chu/tin-lien-quan", children: [] }, { label: "Tin liên quan", url: "/dai-dien-gioi-chu/tin-lien-quan", children: [] },
{ label: "Chủ đề", url: "/dai-dien-gioi-chu/chu-de", children: [] }, { label: "Chủ đề", url: "/dai-dien-gioi-chu/chu-de", children: [] },
], ],
...@@ -69,7 +69,7 @@ export const MOCK_SITEMAP: SiteMapItem[] = [ ...@@ -69,7 +69,7 @@ export const MOCK_SITEMAP: SiteMapItem[] = [
url: "/xuc-tien-thuong-mai", url: "/xuc-tien-thuong-mai",
children: [ children: [
{ label: "Hồ sơ thị trường", url: "/xuc-tien-thuong-mai/ho-so-thi-truong", children: [] }, { label: "Hồ sơ thị trường", url: "/xuc-tien-thuong-mai/ho-so-thi-truong", children: [] },
{ label: "Môi trường kinh doanh", url: "/xuc-tien-thuong-mai/doi-song-kinh-doanh", children: [] }, { label: "Môi trường kinh doanh", url: "/xuc-tien-thuong-mai/moi-truong-kinh-doanh", children: [] },
{ label: "Cơ hội kinh doanh", url: "/xuc-tien-thuong-mai/co-hoi-kinh-doanh", children: [] }, { label: "Cơ hội kinh doanh", url: "/xuc-tien-thuong-mai/co-hoi-kinh-doanh", children: [] },
{ label: "Hỗ trợ kinh doanh", url: "/xuc-tien-thuong-mai/ho-tro-kinh-doanh", children: [] }, { label: "Hỗ trợ kinh doanh", url: "/xuc-tien-thuong-mai/ho-tro-kinh-doanh", children: [] },
], ],
...@@ -83,7 +83,7 @@ export const MOCK_SITEMAP: SiteMapItem[] = [ ...@@ -83,7 +83,7 @@ export const MOCK_SITEMAP: SiteMapItem[] = [
{ label: "Tin kinh tế", url: "/thong-tin-truyen-thong/tin-kinh-te", children: [] }, { label: "Tin kinh tế", url: "/thong-tin-truyen-thong/tin-kinh-te", children: [] },
{ label: "Tin doanh nghiệp", url: "/thong-tin-truyen-thong/tin-doanh-nghiep", children: [] }, { label: "Tin doanh nghiệp", url: "/thong-tin-truyen-thong/tin-doanh-nghiep", children: [] },
{ label: "Chuyên đề", url: "/thong-tin-truyen-thong/chuyen-de", children: [] }, { label: "Chuyên đề", url: "/thong-tin-truyen-thong/chuyen-de", children: [] },
{ label: "Thông tin chính sách và pháp luật", url: "/thong-tin-truyen-thong/chinh-sach-phap-luat", children: [] }, { label: "Thông tin chính sách và pháp luật", url: "/thong-tin-truyen-thong/thong-tin-chinh-sach-va-phap-luat", children: [] },
{ label: "Ấn phẩm", url: "/thong-tin-truyen-thong/an-pham", children: [] }, { label: "Ấn phẩm", url: "/thong-tin-truyen-thong/an-pham", children: [] },
{ label: "Thư viện tài liệu", url: "/thong-tin-truyen-thong/thu-vien-tai-lieu", children: [] }, { label: "Thư viện tài liệu", url: "/thong-tin-truyen-thong/thu-vien-tai-lieu", children: [] },
], ],
......
"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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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] = 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 />
<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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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";
export default function Page() {
const [submitSearch] = 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">
{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 />
<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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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] = 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 />
<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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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] = 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 />
<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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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 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 pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
filters: submitSearch ? `title @=${submitSearch},category @=Tin kinh tế` : 'category @=Tin kinh 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 tin kinh tế...</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-kinh-te/${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 /> */}
<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';
// ...existing code...
const Page: React.FC = () => {
const { id } = useParams()
const { data, isLoading } = useGetNewsId<GetNewsDetailResponseType>(id as string)
return (
<div className="min-h-screen w-full container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={MEDIA_INFORMATION_CATEGORIES} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content */}
<main className="lg:col-span-2 bg-white border rounded-md p-6">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
"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 EventCalendar from "@/components/base/event-calendar";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import { useGetNews } from "@api/endpoints/news";
import { PATHS } from "@constants/paths";
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}` : undefined,
});
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 VCCI...</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-vcci/${news.id}`}
/>
))}
<div className="w-full flex justify-center mt-4">
<Pagination
pageCount={Number(allData?.responseData.totalPages ?? 1)}
page={Number(allData?.responseData.currentPage ?? page)}
onChangePage={(p) => setPage(p)}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() => setPage(Math.min(Number(allData?.responseData.totalPages ?? 1), page + 1))}
/>
</div>
</>
)}
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter onSearch={setSubmitSearch} />
<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>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Biểu mẫu C/O và cách khai
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
<section className="mb-10">
<ol className="list-decimal pl-6 space-y-6 text-[16px]">
{/* 1. Đăng ký hồ sơ thương nhân */}
<li className="leading-relaxed">
<strong>Đăng ký hồ sơ thương nhân:</strong>
<br />
<strong>Đăng ký Hồ sơ thương nhân</strong> (
<em>
áp dụng đối với thương nhân đề nghị cấp C/O lần đầu hoặc
bổ sung khi có thay đổi thông tin của thương nhân hoặc cập
nhật 2 năm/lần theo quy định
</em>
).
</li>
{/* 2. Biểu mẫu về C/O */}
<li className="leading-relaxed">
<strong>Biểu mẫu về C/O:</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Đơn đề nghị cấp C/O</strong>;
</li>
<li>
<strong>C/O mẫu A</strong> (
<em>
C/O ưu đãi thuế quan phổ cập (GSP) theo quy định của
nước nhập khẩu
</em>
);
</li>
<li>
<strong>C/O mẫu B</strong> (
<em>
C/O không ưu đãi theo quy định tại Thông tư
05/2018/TT-BCT ngày 3/4/2018
</em>
);
</li>
<li>
<strong>C/O mẫu ICO</strong> (
<em>
Cấp cho hàng cà phê theo quy định của Tổ chức Cà phê
Quốc tế
</em>
);
</li>
<li>
<strong>C/O mẫu Turkey</strong> (
<em>Thổ Nhĩ Kỳ – Tương đương C/O mẫu B</em>);
</li>
<li>
<strong>C/O mẫu DA59</strong> (
<em>Nam Phi – Tương đương C/O mẫu B</em>);
</li>
<li>
<strong>C/O mẫu Peru</strong> (
<em>Peru – Tương đương C/O mẫu B</em>);
</li>
<li>
<strong>
Giấy chứng nhận hàng hóa không thay đổi xuất xứ (CNM)
</strong>
;
</li>
<li>
<strong>
Mẫu các bảng kê khai nguyên vật liệu sử dụng trong sản
xuất hàng hóa xuất khẩu
</strong>{" "}
khi đề nghị cấp C/O;
</li>
<li>
<strong>
Mẫu đơn đề nghị xét giảm chứng từ nguyên vật liệu
</strong>{" "}
trong hồ sơ đề nghị cấp C/O;
</li>
<li>
<strong>Mẫu đơn đề nghị cấp mã số REX</strong>.
</li>
</ul>
</li>
{/* 3. Mẫu khác */}
<li className="leading-relaxed">
<strong>Mẫu khác:</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Mẫu GCN</strong> (
<em>
Giấy chứng nhận cho hàng hóa không đáp ứng quy định về
xuất xứ
</em>
);
</li>
<li>
<strong>Mẫu đề nghị thay đổi nơi cấp C/O</strong>.
</li>
</ul>
</li>
</ol>
</section>
</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 { usePathname } from "next/navigation";
import React from "react";
import { MenuItem } from "../menu-category";
import { PATHS } from "@constants/paths";
// Local Menu shape compatible with MenuItem
type Menu = {
id: string | number;
name: string;
link?: string;
};
type Category = {
title: string;
href: string;
};
const CATEGORIES: Category[] = [
{ title: "Xuất Xứ Hàng Hóa (C/O)", href: "/xuat-xu-hang-hoa" },
{
title: "Mục đích của C/O",
href: `${PATHS.originOfGoods}/muc-dich`,
},
{
title: "Luật áp dụng về C/O",
href: `${PATHS.originOfGoods}/luat-ap-dung`,
},
{ title: "Thủ tục cấp C/O", href: `${PATHS.originOfGoods}/thu-tuc-cap` },
{
title: "Biểu mẫu C/O và cách khai",
href: `${PATHS.originOfGoods}/bieu-mau-c-o-va-cach-khai`,
},
{
title: "Phí và Lệ phí cấp C/O",
href: `${PATHS.originOfGoods}/phi-va-le-phi-cap`,
},
{
title: "Điểm cấp và Thời gian cấp C/O",
href: `${PATHS.originOfGoods}/diem-cap-va-thoi-gian-cap`,
},
{
title: "Thông tin liên hệ",
href: `${PATHS.originOfGoods}/thong-tin-lien-he`,
},
];
const ListCategory: React.FC<{ categories?: Category[] }> = ({
categories = 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;
'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'
}
})
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Điểm cấp và Thời gian cấp C/O
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
<section className="mb-10">
<ol className="list-decimal pl-6 space-y-6 text-[16px]">
{/* 1. CÁC TỔ CẤP C/O THUỘC CHI NHÁNH VCCI TẠI TP. HCM */}
<li className="leading-relaxed">
<strong>
CÁC TỔ CẤP C/O THUỘC CHI NHÁNH PHÒNG THƯƠNG MẠI & CÔNG
NGHIỆP TẠI TP. HCM
</strong>
<sup className="text-[10px] align-super ml-0.5">1</sup>:
<ol className="list-decimal pl-6 mt-3 space-y-2">
<li>
<strong>Tổ cấp C/O tại TP. HCM:</strong>
<br />
<strong>
Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, Q. 3, Tp. HCM
</strong>
<br />
<strong>Điện thoại:</strong> 028-3932 6498 / 3932 2806
</li>
<li>
<strong>
Tổ cấp C/O tại Bình Dương (DN tại Tỉnh Bình Dương):
</strong>
<br />
<strong>
Lầu 3, Tòa nhà Công ty CP ICD Tân Cảng Sóng Thần, Số
7/20, Đường ĐT 743, KP. Bình Đáng, P. Bình Hòa, TX.
Thuận An, T. Bình Dương
</strong>
<br />
<strong>Điện thoại:</strong> 0274-380 0048
</li>
<li>
<strong>
Tổ cấp C/O tại Đồng Nai (DN tại Tỉnh Đồng Nai):
</strong>
<br />
<strong>
Tòa nhà Sonadezi, số 1 đường 3A, KCN Biên Hòa 2, T.
Đồng Nai
</strong>
<br />
<strong>Điện thoại:</strong> 0251-383 1383
</li>
</ol>
</li>
{/* 2. GIỜ TIẾP NHẬN HỒ SƠ */}
<li className="leading-relaxed">
<strong>GIỜ TIẾP NHẬN HỒ SƠ:</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Từ thứ Hai đến thứ Sáu:</strong>
<br />
<strong>Buổi sáng:</strong> 7h30 – 11h30
<br />
<strong>Buổi chiều:</strong> 13h30 – 16h30
</li>
<li>
<strong>Thứ Bảy:</strong> 7h30 – 11h30 (
<em>
Riêng Tổ cấp C/O tại Đồng Nai không làm việc sáng thứ
7
</em>
)
</li>
</ul>
<p className="mt-2 font-medium text-[#363636]">
* Từ <strong>28/11/2020</strong> các Tổ cấp C/O thuộc VCCI
tại <strong>Tp.HCM, Bình Dương và Đồng Nai</strong>{" "}
<strong>tạm nghỉ làm việc sáng thứ 7</strong>. Đề nghị đại
diện Doanh nghiệp thu xếp thời gian đến liên hệ cấp C/O
phù hợp.
</p>
</li>
{/* 3. THỜI GIAN CẤP C/O */}
<li className="leading-relaxed">
<strong>THỜI GIAN CẤP C/O</strong>
<sup className="text-[10px] align-super ml-0.5">2</sup>:
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Tổ cấp C/O tại TP. HCM:</strong>{" "}
<strong>Không quá 08 giờ làm việc</strong>
</li>
<li>
<strong>Tổ cấp C/O tại Bình Dương:</strong>{" "}
<strong>Không quá 08 giờ làm việc</strong>
</li>
<li>
<strong>Tổ cấp C/O tại Đồng Nai:</strong>{" "}
<strong>Không quá 08 giờ làm việc</strong>
</li>
</ul>
</li>
</ol>
{/* Chú thích cuối trang */}
<div className="mt-6 pt-4 text-sm text-gray-700">
<div className="h-[1px] w-[170px] bg-black mb-3"></div>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">1</sup> Doanh
nghiệp thuộc tỉnh Bình Dương hoặc Đồng Nai có quyền lựa chọn
điểm cấp C/O tại địa phương hoặc tại TP. HCM theo đề nghị
của doanh nghiệp. Lưu ý một doanh nghiệp chỉ được quyền đề
nghị cấp C/O tại một điểm cấp theo đề nghị của doanh nghiệp,
không áp dụng cùng lúc cho một doanh nghiệp đề nghị cấp C/O
tại nhiều điểm cấp.
</p>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">2</sup> Tại tất cả
các tổ cấp C/O, khi doanh nghiệp cần C/O gấp có thể đề nghị,
đăng ký ký gấp. Thời gian xét cấp gấp không quá 01 giờ làm
việc.
</p>
</div>
</section>
</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>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Luật áp dụng về C/O
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
<section className="mb-10">
<p className="mb-6 leading-relaxed text-[16px]">
<strong>Xuất xứ hàng hóa</strong> là các quy tắc và yêu cầu
liên quan để xác định hàng hóa có nguồn gốc tại một nước hoặc
vùng lãnh thổ cụ thể theo từng{" "}
<strong>quy tắc xuất xứ cụ thể</strong>. Các quy tắc xuất xứ
chỉ áp dụng đối với <strong>hàng hóa hữu hình</strong> được
phân loại trong <strong>danh mục mã HS</strong> hàng hóa của{" "}
<strong>Tổ chức Hải quan Thế giới (WCO)</strong>,{" "}
<strong>không áp dụng</strong> để xác định hàng hóa như{" "}
<em>
dịch vụ, quyền sở hữu trí tuệ, và nguồn gốc của con người
</em>
.
</p>
<ol className="list-decimal pl-6 space-y-4 text-[16px]">
<li className="leading-relaxed">
<strong>Quy định chung:</strong>
<br />
<strong>Nghị định số 31/2018/NĐ-CP</strong> ngày 08 tháng 3
năm 2018 quy định chi tiết{" "}
<strong>Luật Quản lý ngoại thương</strong> về xuất xứ hàng
hóa.
</li>
<li className="leading-relaxed">
<strong>
Quy tắc xuất xứ không ưu đãi của Việt Nam (C/O mẫu B, mẫu
DA59, mẫu Peru, Turkey, …):
</strong>
<br />
<strong>Thông tư 05/2018/TT-BCT</strong> ngày 03 tháng 4 năm
2018 của <strong>Bộ Công Thương</strong> (Quy định về xuất
xứ hàng hóa).
</li>
<li className="leading-relaxed">
<strong>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập – GSP (C/O mẫu
A):
</strong>
<br />
<strong>Hệ thống Ưu đãi Thuế quan Phổ cập</strong> hay gọi
tắt là{" "}
<strong>GSP (Generalized System of Preferences)</strong>
hệ thống ưu đãi thuế quan được các nước giàu hay còn gọi là{" "}
<strong>các nước phát triển</strong> dành cho{" "}
<strong>
các nước đang phát triển và các nước kém phát triển (nước
thụ hưởng)
</strong>{" "}
hưởng ưu đãi về <strong>miễn hoặc giảm thuế</strong>. Hàng
hóa xuất khẩu từ các nước thụ hưởng phải đáp ứng đầy đủ các
yêu cầu về <strong>quy tắc xuất xứ ưu đãi</strong> theo quy
định của nước cho hưởng (EU, Thụy Sỹ, Canada, Nhật Bản, …).
<ul className="list-disc pl-6 mt-2 space-y-1 text-[16px]">
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của
Liên minh châu Âu
</em>{" "}
(
<strong>
Handbook on The Scheme of European Union
</strong>
);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của Na
Uy
</em>{" "}
(<strong>Handbook on The Scheme of Norway</strong>);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của
Thụy Sỹ
</em>{" "}
(<strong>Handbook on The Scheme of Switzerland</strong>
);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của
Nhật Bản
</em>{" "}
(<strong>Handbook on The Scheme of Japan</strong>);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của
Canada
</em>{" "}
(<strong>Handbook on The Scheme of Canada</strong>);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của New
Zealand
</em>{" "}
(<strong>Handbook on The Scheme of New Zealand</strong>
);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của
Australia
</em>{" "}
(<strong>Handbook on The Scheme of Australia</strong>);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của Hoa
Kỳ
</em>{" "}
(<strong>Handbook on The Scheme of US</strong>);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của Thổ
Nhĩ Kỳ
</em>{" "}
(<strong>Handbook on The Scheme of Turkey</strong>);
</li>
<li>
<em>
Quy tắc xuất xứ ưu đãi thuế quan phổ cập (GSP) của
Nga, Belarus, Kazakhstan
</em>{" "}
(
<strong>
Handbook on The Scheme of Russia-Belarus-Kazakhstan
</strong>
).
</li>
</ul>
</li>
<li className="leading-relaxed">
<strong>
Quy tắc xuất xứ trong các Hiệp định thương mại tự do
(FTA):
</strong>
<br />
<a
href="https://ecosys.gov.vn/Homepage/DocumentView.aspx"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800"
>
https://ecosys.gov.vn/Homepage/DocumentView.aspx
</a>
</li>
<li className="leading-relaxed">
<strong>Quy định khác liên quan:</strong>
<ul className="list-disc pl-6 mt-2 space-y-1 text-[16px]">
<li>
<em>
Danh mục hàng hóa xuất, nhập khẩu có hiệu lực từ ngày
01/01/2018 – Phân loại HS
</em>{" "}
(<strong>Thông tư 65/2017/TT-BTC</strong> ngày 26/6/2017
của <strong>Bộ Tài Chính</strong>);
</li>
<li>
<em>
Quy định về xử phạt vi phạm hành chính trong hoạt động
thương mại
</em>{" "}
(<strong>Nghị định 98/2020/NĐ-CP</strong> ngày 26/8/2020
của <strong>Chính phủ</strong>);
</li>
<li>
<em>Kiểm dịch động vật, sản phẩm động vật trên cạn</em>{" "}
(<strong>Thông tư 25/2016/TT-BNNPTNT</strong> ngày 30
tháng 6 năm 2016);
</li>
<li>
<em>Quy định về kinh doanh xuất khẩu gạo</em> (
<strong>Nghị định số 107/2018/ND-CP</strong> ngày
15/8/2018 của <strong>Chính phủ</strong>);
</li>
<li>
<em>
Quy định Giấy chứng nhận lưu hành tự do (CFS) đối với
sản phẩm, hàng hóa xuất khẩu và nhập khẩu
</em>{" "}
(<strong>Nghị định số 69/2018/NĐ-CP</strong> ngày
15/5/2018 của <strong>Chính phủ</strong>);
</li>
<li>
<em>
Sửa đổi, bổ sung Thông tư số 28/2015/TT-BCT ngày 20
tháng 8 năm 2015 của Bộ trưởng Bộ Công Thương quy định
việc thực hiện thí điểm tự chứng nhận xuất xứ hàng hóa
trong Hiệp định thương mại hàng hóa ASEAN
</em>{" "}
(<strong>Thông tư 19/2020/TT-BCT</strong> ngày 14 tháng
8 năm 2020);
</li>
<li>
<em>
Hướng dẫn chung (Annex II-B) về khai Giấy chứng nhận
xuất xứ ICO
</em>{" "}
(<strong>Quy định số EB 3775/01</strong> ngày 12/4/2001
của <strong>Tổ chức Cà phê Quốc tế ICO</strong>);
<br />
<em>Mã quốc gia xuất khẩu và nhập khẩu cà phê</em> (
<strong>Quy định số ICC 102-9</strong> ngày 27/4/2009
của ICO);
<br />
<em>Mã cảng xuất khẩu cà phê</em> (
<strong>Quy định số WP Council 174/08 Rev. 1</strong>{" "}
ngày 9/9/2009 của ICO);
<br />
<em>
Quy định liên quan khác đối với mặt hàng cà phê
</em>{" "}
(<strong>Quy định số ED 1918/04</strong> ngày
24/5/2004);
</li>
<li>
<em>
Giấy chứng nhận cấp cho hàng hóa không đáp ứng quy
định xuất xứ
</em>{" "}
(
<strong>
Quy định về nội dung xác nhận trên mẫu Giấy chứng
nhận-GCN
</strong>
).
</li>
</ul>
</li>
</ol>
{/* Lưu ý – chỉ phần này dùng text-sm */}
<div className="mt-6 pt-4 text-sm text-gray-700 italic">
<p className="font-medium text-[#363636]">
*Lưu ý: Cập nhật thường xuyên các quy định có thể được thay
đổi bởi cơ quan chức năng.
</p>
</div>
</section>
</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>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Mục đích của C/O
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
{/* I. MỤC ĐÍCH CỦA GIẤY CHỨNG NHẬN XUẤT XỨ (C/O) */}
<section className="mb-10">
<h2 className="text-[16px] font-[600] text-[#363636] uppercase mb-4 pb-1">
1. MỤC ĐÍCH CỦA GIẤY CHỨNG NHẬN XUẤT XỨ (C/O)
<sup className="text-[10px] align-super ml-0.5">1</sup>:
</h2>
<ol className="list-decimal pl-6 space-y-3">
<li>
Để thiết lập biện pháp và là{" "}
<strong>công cụ của chính sách thương mại</strong>;
</li>
<li>
Để xác định sản phẩm nhập khẩu{" "}
<strong>
được hưởng quy chế tối huệ quốc (MFN) hoặc ưu đãi khác hay
không
</strong>
;
</li>
<li>
<strong>Mục đích thống kê thương mại</strong> của một quốc
gia;
</li>
<li>
Áp dụng quy định về{" "}
<strong>yêu cầu gắn nhãn, mác đối với hàng hóa</strong>; và
</li>
<li>
Dùng cho việc <strong>mua bán của chính phủ</strong>.
</li>
</ol>
</section>
{/* II. VAI TRÒ CỦA QUY TẮC XUẤT XỨ (ROO) */}
<section>
<h2 className="text-[16px] font-[600] text-[#363636] uppercase mb-4 pb-1">
2. VAI TRÒ CỦA QUY TẮC XUẤT XỨ (ROO)
<sup className="text-[10px] align-super ml-0.5">2</sup>:
</h2>
<p className="mb-4 leading-relaxed">
Vai trò cơ bản của <strong>Quy tắc Xuất xứ</strong> là việc
xác định <strong>quốc gia xuất xứ</strong> của một mặt hàng cụ
thể. Các yêu cầu pháp lý hoặc hành chính{" "}
<strong>bắt buộc phải tuân thủ</strong> khi hàng hóa được giao
dịch trên thị trường quốc tế. Điều này là cần thiết cho việc
thực hiện các công cụ chính sách thương mại khác nhau như áp
đặt thuế nhập khẩu, phân bổ hạn ngạch hoặc thống kê thương
mại.
</p>
<p className="leading-relaxed">
Các bước đầu tiên là <strong>phân loại hàng hóa</strong>{" "}
<strong>xác định trị giá</strong> của hàng hóa, bước tiếp theo
và cuối cùng là việc xác định <strong>nước xuất xứ</strong>{" "}
của một sản phẩm cụ thể. Việc phân loại hàng hóa và xác định
trị giá là rất quan trọng trong công việc làm thủ tục hải
quan, nhưng đây là những công cụ cơ bản để xác định nước xuất
xứ của hàng hóa theo nghĩa các{" "}
<strong>quy tắc xuất xứ</strong> là những quy tắc áp dụng cụ
thể cho một sản phẩm nhất định liên quan đến các{" "}
<strong>mã HS cụ thể</strong>, và nếu quy tắc xuất xứ áp dụng
theo trị giá gia tăng thì cần phải xác định{" "}
<strong>
trị giá hải quan của các thành phần cấu thành sản phẩm
</strong>
.
</p>
{/* Phần chú thích */}
<div className="mt-6 pt-4 text-sm text-gray-700">
<div className="h-[1px] w-[170px] bg-black mb-3"></div>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">1</sup> Theo WTO
</p>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">2</sup> Theo WCO
</p>
</div>
</section>
</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>
);
}
import React from "react";
import ListCategory from "./components/list-category";
import EventCalendar from "@/components/base/event-calendar";
import Image from "next/image";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Xuất Xứ Hàng Hóa (C/O)
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
{/* I. ĐỊNH NGHĨA CHUNG */}
<section className="mb-10">
<h2 className="text-[16px] font-[600] text-[#363636] uppercase mb-4 pb-1">
I. ĐỊNH NGHĨA CHUNG
<sup className="text-[10px] align-super ml-0.5">1</sup>:
</h2>
<ol className="list-decimal pl-6 space-y-3">
<li>
<strong>Xuất xứ hàng hóa</strong> là nước, nhóm nước, hoặc
vùng lãnh thổ nơi sản xuất ra toàn bộ hàng hóa hoặc nơi thực
hiện công đoạn chế biến cơ bản cuối cùng đối với hàng hóa
trong trường hợp có nhiều nước, nhóm nước, hoặc vùng lãnh
thổ tham gia vào quá trình sản xuất ra hàng hóa đó.
</li>
<li>
<strong>Quy tắc xuất xứ ưu đãi</strong> là các quy định về
xuất xứ áp dụng cho hàng hóa có cam kết hoặc thỏa thuận ưu
đãi về thuế quan và ưu đãi về phi thuế quan.
</li>
<li>
<strong>Quy tắc xuất xứ không ưu đãi</strong> là các quy
định về xuất xứ áp dụng cho hàng hóa ngoài quy định tại
Khoản 2 Điều này và trong các trường hợp áp dụng các biện
pháp thương mại không ưu đãi về đối xử tối huệ quốc, chống
bán phá giá, chống trợ cấp, tự vệ, hạn chế số lượng hay hạn
ngạch thuế quan, mua sắm chính phủ và thống kê thương mại.
</li>
<li>
<strong>Giấy chứng nhận xuất xứ hàng hóa</strong> là văn bản
hoặc các hình thức có giá trị pháp lý tương đương do cơ
quan, tổ chức thuộc nước, nhóm nước, hoặc vùng lãnh thổ xuất
khẩu hàng hóa cấp dựa trên quy định và yêu cầu liên quan về
xuất xứ, chỉ rõ nguồn gốc xuất xứ của hàng hóa đó.
</li>
<li>
<strong>Giấy chứng nhận xuất xứ hàng hóa giáp lưng</strong>{" "}
là Giấy chứng nhận xuất xứ hàng hóa theo quy định tại Điều
ước quốc tế mà Việt Nam ký kết hoặc gia nhập, được cấp bởi
nước thành viên xuất khẩu trung gian dựa trên Giấy chứng
nhận xuất xứ hàng hóa của nước thành viên xuất khẩu đầu
tiên.
</li>
<li>
<strong>
{" "}
Giấy chứng nhận hàng hóa không thay đổi xuất xứ
</strong>{" "}
là Giấy chứng nhận cấp cho hàng hóa nước ngoài được đưa vào
kho ngoại quan của Việt Nam, sau đó xuất khẩu đi nước khác,
đưa vào nội địa trên cơ sở Giấy chứng nhận xuất xứ hàng hóa
đã được cấp đầu tiên.
</li>
<li>
<strong>Tự chứng nhận xuất xứ hàng hóa</strong> là hình thức
thương nhân tự khai báo và cam kết về xuất xứ của hàng hóa
theo quy định của pháp luật.
</li>
<li>
<strong>Chứng từ tự chứng nhận xuất xứ hàng hóa</strong>
văn bản hoặc các hình thức có giá trị pháp lý tương đương do
thương nhân tự phát hành theo quy định tại Khoản 7 Điều này.
</li>
<li>
<strong>Chuyển đổi mã số hàng hóa</strong> là sự thay đổi về
mã số HS (trong Biểu thuế xuất khẩu, Biểu thuế nhập khẩu)
của hàng hóa được tạo ra ở một nước, nhóm nước, hoặc vùng
lãnh thổ trong quá trình sản xuất từ nguyên liệu không có
xuất xứ của nước, nhóm nước, hoặc vùng lãnh thổ này.
</li>
<li>
<strong>Tỷ lệ Phần trăm giá trị</strong> là hàm lượng giá
trị có được đủ để coi là có xuất xứ tại một nước, nhóm nước,
hoặc vùng lãnh thổ nơi diễn ra công đoạn sản xuất, gia công,
chế biến cuối cùng. Tỷ lệ này được xác định là Phần giá trị
gia tăng có được tính trên tổng giá trị của hàng hóa được
sản xuất, gia công, chế biến tại một nước, nhóm nước, hoặc
vùng lãnh thổ sau khi trừ đi giá nguyên liệu đầu vào nhập
khẩu không thuộc nước, nhóm nước, hoặc vùng lãnh thổ đó hoặc
giá trị nguyên liệu đầu vào không xác định được xuất xứ dùng
để sản xuất ra hàng hóa.
</li>
<li>
<strong>Công đoạn gia công, chế biến hàng hóa</strong>
quá trình sản xuất chính tạo ra đặc điểm cơ bản của hàng
hóa.
</li>
<li>
<strong>Thay đổi cơ bản</strong> là việc hàng hóa được biến
đổi qua quá trình sản xuất, để hình thành vật phẩm thương
mại mới, khác biệt về hình dạng, tính năng, đặc điểm cơ bản,
hoặc mục đích sử dụng so với hàng hóa ban đầu.
</li>
<li>
<strong>Đơn giản</strong> là hoạt động không cần sử dụng các
kỹ năng đặc biệt, máy móc, dây chuyền hoặc các thiết bị
chuyên dụng.
</li>
<li>
<strong>Sản xuất</strong> là các phương thức để tạo ra hàng
hóa bao gồm trồng trọt, khai thác, thu hoạch, chăn nuôi, gây
giống, chiết xuất, thu lượm, thu nhặt, săn bắt, đánh bắt,
đánh bẫy, săn bắn, chế tạo, chế biến, gia công hay lắp ráp.
</li>
<li>
<strong>Nguyên liệu</strong> là bất cứ vật liệu hay chất
liệu nào được sử dụng hoặc tiêu tốn trong quá trình sản xuất
ra hàng hóa, hoặc kết hợp tự nhiên thành một hàng hóa khác,
hoặc tham gia vào quy trình sản xuất ra một hàng hóa khác.
</li>
<li>
<strong>
Hàng hóa có xuất xứ hoặc nguyên liệu có xuất xứ
</strong>{" "}
là hàng hóa hoặc nguyên liệu đáp ứng quy tắc xuất xứ ưu đãi
theo quy định tại Chương II hoặc quy tắc xuất xứ không ưu
đãi theo quy định tại Chương III Nghị định này.
</li>
<li>
<strong>
Thương nhân đề nghị cấp Giấy chứng nhận xuất xứ hàng hóa
</strong>{" "}
là người xuất khẩu, nhà sản xuất, người đại diện hợp pháp
của người xuất khẩu hoặc nhà sản xuất.
</li>
</ol>
</section>
{/* II. ĐỊNH NGHĨA LIÊN QUAN KHÁC */}
<section>
<h2 className="text-[16px] font-[600] text-[#363636] uppercase mb-4 pb-1">
II. ĐỊNH NGHĨA LIÊN QUAN KHÁC:
</h2>
<ol className="list-decimal pl-6 space-y-3">
<li>
<strong>Giấy chứng nhận xuất xứ</strong> thường được viết
tắt là <strong>C/O</strong> (Certificate of Origin) là chứng
từ quan trọng trong thương mại quốc tế chứng nhận lô hàng cụ
thể được xuất khẩu có xuất xứ thuần túy, hoặc được sản xuất
hoặc được chế biến tại một quốc gia cụ thể.
<sup className="text-[10px] align-super ml-0.5">2</sup>
</li>
<li>
<strong>Giấy chứng nhận xuất xứ</strong> là văn bản nêu cụ
thể quốc gia mà tại đó hàng hóa được trồng, được sản xuất,
được chế biến hay lắp ráp.
<sup className="text-[10px] align-super ml-0.5">3</sup>
</li>
<li>
<strong>Hệ thống hài hòa hóa</strong> hay gọi đơn giản là{" "}
<strong>HS</strong> (Harmonized System) là một danh pháp
thuế quan toàn cầu, một hệ thống được tiêu chuẩn hóa quốc tế
về tên gọi (mô tả) và mã số (mã HS) được sử dụng để phân
loại mọi mặt hàng thương mại trên toàn thế giới bao gồm cả
hàng hóa xuất khẩu và nhập khẩu. Hệ thống này có hiệu lực
năm 1988 và được quản lý bởi Tổ chức Hải quan Quốc tế (WCO).
Danh mục này bao gồm khoảng 5.000 nhóm hàng, được xác định
bằng 6 chữ số, được sắp xếp theo một cấu trúc pháp lý và hợp
lý và chúng được giải thích bằng những quy tắc được định
nghĩa rõ ràng để đảm bảo việc phân loại hàng hóa theo 1 cách
thống nhất.
<sup className="text-[10px] align-super ml-0.5">4</sup>
</li>
</ol>
{/* Phần chú thích */}
<div className="mt-6 pt-4 text-sm text-gray-700">
<div className="h-[1px] w-[170px] bg-black mb-3"></div>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">1</sup> Theo điều
3, Nghị định 31/2018/NĐ-CP ngày 08 tháng 3 năm 2018
</p>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">2</sup> Theo ICC
</p>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">3</sup> Theo USAID
</p>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">4</sup> Theo WCO
</p>
</div>
</section>
</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>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Phí và Lệ phí cấp C/O
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
<section className="mb-10">
<ol className="list-decimal pl-6 space-y-6 text-[16px]">
{/* 1. Lệ phí cấp C/O */}
<li className="leading-relaxed">
<strong>Lệ phí cấp C/O:</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Phí cấp C/O:</strong>{" "}
<strong>60.000 VND/bộ cấp mới</strong>{" "}
<strong>30.000 VND/bộ cấp lại</strong> (
<em>
TT36/2023/TT-BTC ngày 06/6/2023 của Bộ Tài chính quy
định mức thu, chế độ thu, nộp, quản lý và sử dụng phí
chứng nhận xuất xứ hàng hóa - C/O
</em>
).
</li>
</ul>
</li>
{/* 2. Phí cấp chứng từ thương mại */}
<li className="leading-relaxed">
<strong>Phí cấp chứng từ thương mại:</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>100.000 VND/bộ/4 trang</strong> theo{" "}
<strong>
Quy định số 2706/2009/PTM-TT ngày 9 tháng 9 năm 2010
của Phòng Thương mại và Công nghiệp Việt Nam về việc
Ban hành phí xác nhận chứng từ thương mại
</strong>
.
</li>
</ul>
</li>
</ol>
</section>
</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>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Điểm cấp và Thời gian cấp C/O
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
<section className="mb-10">
<div className="space-y-6 text-[16px] leading-relaxed">
<div>
<p className="font-bold text-[#363636]">PHÒNG PHÁP CHẾ</p>
<p>
<strong>
Chi nhánh Phòng Thương mại và Công nghiệp Việt Nam tại
Tp. HCM (VCCI HCM)
</strong>
<br />
<strong>
Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, P. 7, Quận 3,
TP. HCM
</strong>
<br />
<strong>Điện thoại:</strong> 028-3932 6498 / 3932 2806
<br />
<strong>Email:</strong>{" "}
<a
href="mailto:co@vcci-hcm.org.vn"
className="text-blue-600 underline hover:text-blue-800"
>
co@vcci-hcm.org.vn
</a>
</p>
</div>
{/* Xử lý vướng mắc, phản ánh, góp ý */}
<div>
<p className="font-bold text-[#363636]">
XỬ LÝ VƯỚNG MẮC, PHẢN ÁNH, GÓP Ý TRONG QUÁ TRÌNH LÀM THỦ
TỤC CẤP C/O:
</p>
<ul className="list-disc pl-6 mt-2 space-y-3">
<li>
<strong>Tổ cấp C/O tại Tp. HCM:</strong>
<br />
<strong>Điện thoại:</strong> 028-3232 5176
<br />
<strong>Email:</strong>{" "}
<a
href="mailto:co@vcci-hcm.org.vn"
className="text-blue-600 underline hover:text-blue-800"
>
co@vcci-hcm.org.vn
</a>
</li>
<li>
<strong>Điểm cấp C/O tại Bình Dương:</strong>
<br />
<strong>Phó trưởng phòng Pháp chế:</strong> Nguyễn Văn
Đức
<br />
<strong>Mobile:</strong> 090 949 7155
<br />
<strong>Email:</strong>{" "}
<a
href="mailto:nvduc1980@gmail.com"
className="text-blue-600 underline hover:text-blue-800"
>
nvduc1980@gmail.com
</a>
</li>
<li>
<strong>Tổ cấp C/O tại Đồng Nai:</strong>
<br />
<strong>Phó trưởng phòng Pháp chế:</strong> Ông Bùi Mạnh
Hùng
<br />
<strong>Mobile:</strong> 087 789 2442
<br />
<strong>Email:</strong>{" "}
<a
href="mailto:hungbui@vcci-hcm.org.vn"
className="text-blue-600 underline hover:text-blue-800"
>
hungbui@vcci-hcm.org.vn
</a>
</li>
<li>
<strong>
HƯỚNG DẪN KHAI HỒ SƠ THƯƠNG NHÂN, CHỮ KÝ SỐ VÀ IT;
GIẢI ĐÁP VỀ REX:
</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Tổ cấp C/O tại Tp.HCM:</strong> 028-3932
6498 / 3932 2806
</li>
<li>
<strong>Tổ cấp C/O tại Bình Dương:</strong> 0274-380
0048
</li>
<li>
<strong>Tổ cấp C/O tại Đồng Nai:</strong> 0251-383
1383
</li>
</ul>
</li>
<li>
<strong>
TIẾP THU, GIẢI QUYẾT PHẢN ÁNH, KHIẾU NẠI, GÓP Ý:
</strong>
<br />
<strong>Trưởng phòng Pháp chế:</strong> Ông Vũ Xuân Hưng
<br />
<strong>Điện thoại:</strong> 028-3932 6929 hoặc{" "}
<strong>Mobile:</strong> 0909 170 171{" "}
<em>(Đường dây nóng)</em>
<br />
<strong>Email:</strong>{" "}
<a
href="mailto:vuxuanhung@vcci-hcm.org.vn"
className="text-blue-600 underline hover:text-blue-800"
>
vuxuanhung@vcci-hcm.org.vn
</a>
</li>
</ul>
</div>
</div>
</section>
</div>
<div className="relative w-full mt-4 h-[185px] aspect-video rounded-lg overflow-hidden">
<Image
src="/VCCI-thongTinLienHeBanner.jpg.webp"
alt="Quảng cáo"
fill
className="object-contain"
/>
</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>
);
}
import React from "react";
import Image from "next/image";
import ListCategory from "../components/list-category";
import EventCalendar from "@/components/base/event-calendar";
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 />
</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-[#063e8e] text-[25px] font-semibold">
Thủ tục cấp C/O
</h1>
<div className="w-full h-[1px] bg-[#eeeeee]"></div>
<div className="text-[#363636]">
<section className="mb-10">
<ol className="list-decimal pl-6 space-y-6 text-[16px]">
{/* 1. Các bước cần thiết trước khi đề nghị cấp C/O */}
<li className="leading-relaxed">
<strong>
Các bước cần thiết thực hiện trước khi đề nghị cấp C/O:
</strong>
<ol className="list-decimal pl-6 mt-3 space-y-2">
<li>
<strong>Bước 1:</strong> Kiểm tra xem sản phẩm có{" "}
<strong>xuất xứ thuần túy (xuất xứ toàn bộ)</strong>{" "}
theo quy định phù hợp hay không. Nếu không, chuyển sang
bước 2;
</li>
<li>
<strong>Bước 2:</strong> Xác định chính xác{" "}
<strong>mã số HS của sản phẩm xuất khẩu</strong> (
<strong>4 hoặc 6 số H.S đầu</strong> là cơ sở để xác
định xuất xứ hàng hóa theo quy định)
<sup className="text-[10px] align-super ml-0.5">1</sup>;
</li>
<li>
<strong>Bước 3:</strong> Xác định nước nhập khẩu hàng
hóa mà quốc gia đó đã ký{" "}
<strong>Hiệp định thương mại tự do (FTA)</strong> với
Việt Nam/ASEAN và/hoặc cho Việt Nam hưởng{" "}
<strong>ưu đãi thuế quan GSP</strong> hay không. Nếu có,
chuyển sang bước 4;
</li>
<li>
<strong>Bước 4:</strong> Kiểm tra xem sản phẩm xuất khẩu
có thuộc{" "}
<strong>
danh mục các công đoạn chế biến đơn giản (không đầy
đủ)
</strong>{" "}
theo quy định phù hợp hay không. Nếu có, sản phẩm đó{" "}
<strong>sẽ không có xuất xứ</strong> theo quy định. Nếu
không, chuyển tiếp sang bước 5.
</li>
<li>
<strong>Bước 5:</strong> So sánh{" "}
<strong>thuế suất</strong> để chọn{" "}
<strong>mẫu C/O (nếu có)</strong> để đề nghị cấp nhằm
đảm bảo hàng hóa xuất khẩu được hưởng{" "}
<strong>mức ưu đãi thuế nhập khẩu thấp nhất</strong>;
</li>
<li>
<strong>Bước 6:</strong> Kiểm tra xem sản phẩm xuất khẩu{" "}
<strong>đáp ứng quy định xuất xứ phù hợp</strong> hay
không.
<br />
<em>VD:</em> C/O mẫu A XK sang EU –{" "}
<strong>Annex 22-03</strong>, Thụy Sỹ –{" "}
<strong>Annex 4</strong>, Japan –{" "}
<strong>Annex 5</strong>, … hoặc C/O mẫu B –{" "}
<strong>Phụ lục I – Thông tư 05/2018/TT-BCT</strong>
</li>
<li>
<strong>Bước 7:</strong> Nếu sản phẩm{" "}
<strong>
chưa đáp ứng quy định phù hợp tại bước 6
</strong>
, vận dụng các{" "}
<strong>điều khoản ngoại lệ/đặc biệt</strong> sau:
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>
Quy định vi phạm cho phép (Derogation/Tolerance/De
Minimis)
</strong>{" "}
đối với các nguyên vật liệu hoặc bộ phận không có
xuất xứ áp dụng theo tiêu chí “
<strong>Chuyển đổi mã số hàng hóa</strong>”;
</li>
<li>
<strong>Quy định cộng gộp song phương</strong>;
</li>
<li>
<strong>Quy định cộng gộp khu vực</strong>;
</li>
<li>
<strong>Quy định cộng gộp khác</strong> và/hoặc các
quy định mở rộng liên quan khác.
</li>
</ul>
</li>
</ol>
</li>
{/* 2. Đăng ký hồ sơ thương nhân */}
<li className="leading-relaxed">
<strong>
Đăng ký hồ sơ thương nhân (Điều 13 của NĐ 31/2018/NĐ-CP)
</strong>
<sup className="text-[10px] align-super ml-0.5">2</sup>:
<br />
Lập, nộp <strong>1 bộ hồ sơ thương nhân</strong> cho{" "}
<strong>Tổ chức cấp C/O</strong> đối với thương nhân đề nghị
cấp C/O lần đầu. Hồ sơ thương nhân bao gồm:
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Thông tin của thương nhân</strong> (
<em>Mẫu VCCI HCM</em>);
</li>
<li>
<strong>
Đăng ký mẫu chữ ký của người được ủy quyền ký đơn đề
nghị cấp C/O và mẫu dấu
</strong>{" "}
(<em>Mẫu số 01 của NĐ 31/2018/NĐ-CP</em>);
</li>
<li>
<strong>
Danh mục các cơ sở sản xuất của thương nhân
</strong>{" "}
(<em>Mẫu số 02 của NĐ 31/2018/NĐ-CP</em>);
</li>
<li>
<strong>
Bản sao Giấy chứng nhận đăng ký doanh nghiệp
</strong>{" "}
(<em>Ký, đóng dấu sao y bản chính của thương nhân</em>);
</li>
<li>
<strong>
Giấy chứng nhận đã đăng ký mẫu dấu với cơ quan công an
</strong>{" "}
(<em>nếu có</em>).
</li>
</ul>
</li>
{/* 3. Hồ sơ đề nghị cấp C/O mới */}
<li className="leading-relaxed">
<strong>
Hồ sơ đề nghị cấp C/O mới (Điều 15, NĐ 31/2018/NĐ-CP):
</strong>
<div className="mt-2">
<strong>Chứng từ xuất khẩu:</strong>
<ul className="list-disc pl-6 mt-1 space-y-1">
<li>
<strong>Đơn đề nghị cấp C/O</strong> (
<em>Mẫu số 4 của NĐ 31/2018/NĐ-CP</em>);
</li>
<li>
<strong>Phiếu ghi chép</strong> (<em>Mẫu VCCI HCM</em>
);
</li>
<li>
<strong>
Mẫu C/O tương ứng đã được khai hoàn chỉnh
</strong>{" "}
(<em>thông thường là 1 bản chính và 3 bản copy</em>);
</li>
<li>
<strong>Bản in tờ khai hải quan xuất khẩu</strong> (
<em>Có xác nhận của thương nhân</em>);
</li>
<li>
<strong>Bản sao hóa đơn thương mại</strong> (
<em>Ký, đóng dấu sao y bản chính của thương nhân</em>
);
</li>
<li>
<strong>
Bản sao B/L hoặc AWB hoặc chứng từ vận tải tương
đương
</strong>{" "}
(<em>Ký, đóng dấu sao y bản chính của thương nhân</em>
).
</li>
</ul>
</div>
<div className="mt-3">
<strong>Chứng từ chứng minh nguồn gốc:</strong>
<ul className="list-disc pl-6 mt-1 space-y-1">
<li>
<strong>
Bảng kê khai chi tiết hàng hóa xuất khẩu đạt tiêu
chí xuất xứ ưu đãi hoặc xuất xứ không ưu đãi
</strong>{" "}
(
<em>
Chọn mẫu Bảng kê khai NVL phù hợp: 8 mẫu khác nhau
</em>
);
</li>
<li>
<strong>
Bản khai báo xuất xứ của nhà sản xuất hoặc nhà cung
cấp nguyên liệu có xuất xứ hoặc hàng hóa có xuất xứ
</strong>{" "}
(<em>Phụ lục X của TT 05/2018/TT-BCT</em>);
</li>
<li>
<strong>
Tờ khai hải quan nhập khẩu nguyên liệu, phụ liệu
dùng để sản xuất ra hàng hóa xuất khẩu
</strong>{" "}
(
<em>
trong trường hợp có sử dụng nguyên liệu, phụ liệu
nhập khẩu trong quá trình sản xuất
</em>
); hợp đồng mua bán hoặc hóa đơn giá trị gia tăng mua
bán nguyên liệu, phụ liệu trong nước (
<em>
trong trường hợp có sử dụng nguyên liệu, phụ liệu
mua trong nước trong quá trình sản xuất
</em>
); giấy phép xuất khẩu (<em>nếu có</em>); chứng từ,
tài liệu cần thiết khác.
</li>
</ul>
</div>
</li>
{/* 4. Hồ sơ đề nghị cấp lại C/O */}
<li className="leading-relaxed">
<strong>
Hồ sơ đề nghị cấp lại C/O (Điều 18, NĐ 31/2018/NĐ-CP):
</strong>
<ul className="list-disc pl-6 mt-2 space-y-1">
<li>
<strong>Đơn đề nghị cấp C/O</strong> (
<em>Mẫu số 4 của NĐ 31/2018/NĐ-CP</em>);
</li>
<li>
<strong>Phiếu ghi chép</strong> (<em>Mẫu VCCI HCM</em>);
</li>
<li>
<strong>
Mẫu C/O tương ứng đã được khai hoàn chỉnh
</strong>{" "}
(<em>thông thường là 1 bản chính và 3 bản copy</em>);
</li>
</ul>
<p className="mt-2">
Các trường hợp cấp lại C/O căn cứ vào{" "}
<strong>Điều 18 của NĐ 31/2018/NĐ-CP</strong>.
</p>
</li>
{/* 5. Khai C/O qua mạng */}
<li className="leading-relaxed">
<strong>Khai C/O qua mạng:</strong>
<br />
Doanh nghiệp khai C/O trên website:{" "}
<a
href="http://comis.covcci.com.vn"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800"
>
http://comis.covcci.com.vn
</a>{" "}
và thực hiện theo từng bước theo yêu cầu/hướng dẫn tại
website.
<br />
<strong>Lưu ý:</strong> Trước khi khai C/O qua mạng, doanh
nghiệp cần{" "}
<strong>
đăng ký hồ sơ thương nhân với chữ ký số được kích hoạt
</strong>{" "}
trên trang web:{" "}
<a
href="http://comis.covcci.com.vn"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800"
>
http://comis.covcci.com.vn
</a>
</li>
</ol>
{/* Chú thích cuối trang */}
<div className="mt-6 pt-4 text-sm text-gray-700">
<div className="h-[1px] w-[170px] bg-black mb-3"></div>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">1</sup> Việc khai
báo thiếu chính xác mã HS của sản phẩm sẽ dẫn đến khả năng
từ chối cấp C/O tại Việt Nam hoặc bị từ chối tiếp nhận C/O
của hải quan nước nhập khẩu.
</p>
<p className="text-[16px] italic">
<sup className="text-[10px] align-super">2</sup> Doanh
nghiệp đề nghị cấp C/O lần đầu hoặc thông tin của doanh
nghiệp được thay đổi hoặc cập nhật 2 năm/lần theo quy định.
</p>
</div>
</section>
</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>
);
}
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_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 { Spinner } from "@components/ui/spinner";
// ...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={TRADE_PROMOTION_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 nội dung...</span>
</div>
) : (
<>
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{data?.responseData?.title}
</div>
<hr className="py-2"/>
<div className="p-7.5 prose tiptap overflow-hidden">{parse(data?.responseData?.description ?? '')}</div>
</>
)}
</main>
{/* Sidebar */}
<aside className="space-y-6">
<ListFilter />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
export default Page;
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_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 { Spinner } from "@components/ui/spinner";
// ...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={TRADE_PROMOTION_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 cơ hội kinh doanh...</span>
</div>
) : data?.responseData ? (
<>
<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>
</>
) : (
<p className="text-center py-4">Không có dữ liệu</p>
)}
</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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import EventCalendar from "@/components/base/event-calendar";
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 pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters: 'category@=Cơ hội kinh doanh'
});
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<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 cơ hội kinh doanh...</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.tradePromotion}/${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">
<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>
);
}
// Core
"use client";
import Image from "next/image";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_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 { Spinner } from "@components/ui/spinner";
// ...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={TRADE_PROMOTION_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 hỗ trợ kinh doanh...</span>
</div>
) : data?.responseData ? (
<>
<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>
</>
) : (
<p className="text-center py-4">Không có dữ liệu</p>
)}
</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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { 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 pageSize = 5;
const { data: allData, isLoading } = useGetNews<GetNewsResponseType>({
pageSize: String(pageSize),
currentPage: String(page),
// filters: submitSearch ? `title @=${submitSearch}` : undefined,
filters:'category@=Hỗ trợ kinh doanh'
});
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<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 hỗ trợ kinh doanh...</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.tradePromotion}/${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 /> */}
<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 { TRADE_PROMOTION_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 { Spinner } from "@components/ui/spinner";
// ...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={TRADE_PROMOTION_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 môi trường kinh doanh...</span>
</div>
) : data?.responseData ? (
<>
<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>
</>
) : (
<p className="text-center py-4">Không có dữ liệu</p>
)}
</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;
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { Pagination } from "@components/base/pagination";
import Image from "next/image";
import EventCalendar from "@/components/base/event-calendar";
import { useGetNews } from "@api/endpoints/news";
import { GetNewsResponseType } from "@api/types/NewsPage.type";
import { PATHS } from "@constants/paths";
import { Spinner } from "@components/ui/spinner";
export default function Page() {
const [submitSearch] = 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,
filters: 'category@=Môi trường kinh doanh'
});
return (
<div className="min-h-screen container mx-auto p-4">
<div className="w-full flex flex-col gap-5">
<ListCategory categories={TRADE_PROMOTION_CATEGORIES} />
<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 môi trường kinh doanh...</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.tradePromotion}/${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">
<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>
);
}
"use client";
import React, { useState } from "react";
import ListCategory from "@app/dai-dien-gioi-chu/components/list-category";
import { TRADE_PROMOTION_CATEGORIES } from "@constants/categories";
import NewsContent from "@app/dai-dien-gioi-chu/components/card-news";
import { 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 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={TRADE_PROMOTION_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 xúc tiến thương mại...</span>
</div>
) : (
<>
{allData?.responseData.rows.map((news) => (
<NewsContent key={news.id} news={news} link={`${PATHS.tradePromotion}/${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 /> */}
<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>
);
}
...@@ -4,32 +4,30 @@ import { Providers } from "./_providers"; ...@@ -4,32 +4,30 @@ import { Providers } from "./_providers";
import React from "react"; import React from "react";
import links from "@links/index"; import links from "@links/index";
export const metadata: Metadata = {
title: 'Chức năng Đại diện Người sử dụng lao động - Liên đoàn Thương mại và Công nghiệp Việt Nam, CN TP.HCM',
export const metadata: Metadata = { description: 'Chức năng Đại diện Người sử dụng lao động (NSDLĐ):',
title: 'Chức năng Đại diện Người sử dụng lao động - Liên đoàn Thương mại và Công nghiệp Việt Nam, CN TP.HCM', metadataBase: new URL(links.siteURL),
description: 'Chức năng Đại diện Người sử dụng lao động (NSDLĐ):', alternates: { canonical: links.siteURL },
metadataBase: new URL(links.siteURL), openGraph: {
alternates: { canonical: links.siteURL }, title: 'Liên đoàn Thương mại và Công nghiệp Việt Nam, CN TP.HCM',
openGraph: { description: 'Phòng 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...',
title: 'Liên đoàn Thương mại và Công nghiệp Việt Nam, CN TP.HCM', url: links.siteURL,
description: 'Phòng 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...', siteName: 'Liên đoàn Thương mại và Công nghiệp Việt Nam, CN TP.HCM',
url: links.siteURL, images: [
siteName: 'Liên đoàn Thương mại và Công nghiệp Việt Nam, CN TP.HCM', {
images: [ url: `${links.siteURL}/thumbnail.png`,
{ width: 1200,
url: `${links.siteURL}/thumbnail.png`, height: 630,
width: 1200, alt: 'VCCI HCM'
height: 630, }
alt: 'VCCI HCM' ],
} locale: 'vi_VN',
], type: 'website'
locale: 'vi_VN', },
type: 'website' twitter: { card: 'summary_large_image', title: 'VCCI HCM', description: 'Tin tức và sự kiện từ VCCI HCM', creator: '@vcci_hcm' },
}, robots: { index: true, follow: true, googleBot: { index: true, follow: true } }
twitter: { card: 'summary_large_image', title: 'VCCI HCM', description: 'Tin tức và sự kiện từ VCCI HCM', creator: '@vcci_hcm' }, };
robots: { index: true, follow: true, googleBot: { index: true, follow: true } }
};
export default function RootLayout({ export default function RootLayout({
children, children,
......
export interface NewsItem { export interface NewsDetailItem {
id: string id: string
title: string title: string
thumbnail: string thumbnail: string
...@@ -14,18 +14,18 @@ export interface NewsItem { ...@@ -14,18 +14,18 @@ export interface NewsItem {
category: string category: string
} }
export interface NewsResponseData { export interface NewsDetailResponseData {
count: number count: number
rows: NewsItem[] rows: NewsDetailItem[]
totalPages: number totalPages: number
currentPage: number currentPage: number
} }
export interface GetNewsResponseType { export interface GetNewsDetailResponseType {
message: string message: string
message_en: string message_en: string
responseData: NewsResponseData responseData: NewsDetailItem
status: 'success' | 'error' status: 'success' | 'error'
timeStamp: string timeStamp: string
violations: string | null violations: any | null
} }
\ No newline at end of file
import { NewsItem } from '@app/dai-dien-gioi-chu/lib/types/NewsPage.type'; import { NewsDetailItem } from './CardNews.type';
import Links from '@links/index' import Links from '@links/index'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
...@@ -19,8 +19,8 @@ const stripImagesAndHtml = (html?: string) => { ...@@ -19,8 +19,8 @@ const stripImagesAndHtml = (html?: string) => {
} }
return withoutImgs.replace(/<[^>]*>/g, '') return withoutImgs.replace(/<[^>]*>/g, '')
} }
function NewsContent({ news ,link}: { news: NewsItem ,link:string}) {
const CardNews = ({ news, link }: { news: NewsDetailItem, link: string }) => {
return ( return (
<a <a
href={`${link}`} href={`${link}`}
...@@ -30,14 +30,14 @@ function NewsContent({ news ,link}: { news: NewsItem ,link:string}) { ...@@ -30,14 +30,14 @@ function NewsContent({ news ,link}: { news: NewsItem ,link:string}) {
src={`${Links.imageEndpoint}${news.thumbnail}`} src={`${Links.imageEndpoint}${news.thumbnail}`}
alt={news.title} alt={news.title}
className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0" className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0"
onError={(e) => { onError={(e) => {
e.currentTarget.src = "/img-error.png" e.currentTarget.src = "/img-error.png"
}} }}
/> />
<div className="flex-1 min-w-0 pl-0 sm:pl-4"> <div className="flex-1 min-w-0 pl-0 sm:pl-4">
<p className="text-primary font-semibold text-base md:text-lg hover:underline line-clamp-2 wrap-break-word"> <p className="text-primary font-semibold text-base md:text-lg hover:underline line-clamp-2 wrap-break-word">
{news.title} {news.title}
</p> </p>
...@@ -51,4 +51,4 @@ function NewsContent({ news ,link}: { news: NewsItem ,link:string}) { ...@@ -51,4 +51,4 @@ function NewsContent({ news ,link}: { news: NewsItem ,link:string}) {
) )
} }
export default NewsContent; export default CardNews;
\ No newline at end of file \ No newline at end of file
...@@ -3,16 +3,12 @@ ...@@ -3,16 +3,12 @@
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import React from "react"; import React from "react";
import { MenuItem } from "../menu-category"; import { MenuItem } from "../menu-category";
// Local Menu shape compatible with MenuItem
type Menu = {
id: string | number;
name: string;
link?: string;
};
type Category = { type Category = {
title: string; id: string;
href: string; name: string;
static_link: string;
}; };
// Default categories removed — component now accepts `categories` via props. // Default categories removed — component now accepts `categories` via props.
...@@ -23,21 +19,19 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ ...@@ -23,21 +19,19 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({
const pathname = usePathname() || ""; const pathname = usePathname() || "";
const isActive = (href: string) => { 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 pathname === href;
}; };
return ( return (
<div className="border-t border-gray-200 bg-white p-2.5"> <div className="border-t border-gray-200 bg-white py-2">
<div className="w-full px-4 sm:px-6 lg:px-8"> <div className="w-full px-4 sm:px-6 lg:px-8">
<div className="py-3"> <div className="py-3">
<div className="flex flex-wrap items-center max-w-full overflow-x-auto"> <div className="flex flex-wrap items-center max-w-full overflow-x-auto">
{categories.map((c) => { {categories.map((c) => {
const menu: Menu = { id: c.href, name: c.title, link: c.href }; const menu = { id: c.id, name: c.name, link: c.static_link };
const active = isActive(c.href); const active = isActive(c.static_link);
return ( return (
<div key={c.href} className="shrink-0"> <div key={c.id} className="shrink-0">
<MenuItem menu={menu} active={active} /> <MenuItem menu={menu} active={active} />
</div> </div>
); );
......
...@@ -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()
......
...@@ -10,18 +10,18 @@ const MenuItem = ({ title, link, items }: MenuItemProps) => { ...@@ -10,18 +10,18 @@ const MenuItem = ({ title, link, items }: MenuItemProps) => {
return ( return (
<div className="group relative"> <div className="group relative">
<Link <Link
href={`/${link || ""}`} href={`${link || ""}`}
className="px-3 py-5 text-[16px] font-[600] text-[#124588] hover:text-[#E8C518] transition block" className="px-3 py-5 text-[16px] font-semibold text-[#124588] hover:text-[#E8C518] transition block"
> >
{title} {title}
</Link> </Link>
{/* Dropdown */} {/* Dropdown */}
<div className="absolute left-0 top-full hidden group-hover:block bg-[#124588]/98 text-white text-[14px] font-[500] min-w-[220px] shadow-lg"> <div className="absolute left-0 top-full hidden group-hover:block bg-[#124588]/98 text-white text-[14px] font-medium min-w-[220px] shadow-lg">
{items.map((item, i) => ( {items.map((item, i) => (
<Link <Link
key={i} key={i}
href={`/${link}/${item.link}`} href={`${item.link}`}
className="block px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer whitespace-nowrap transition" className="block px-5 py-3 hover:bg-[#e8c518]/80 cursor-pointer whitespace-nowrap transition"
> >
{item.title} {item.title}
......
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
{/* <span>Previous</span> */}
</PaginationLink>
)
PaginationPrevious.displayName = "«"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
{/* <span>Next</span> */}
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "»"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}
import { PATHS } from "@constants/paths";
export const TRADE_PROMOTION_CATEGORIES = [
{
title: "Hồ sơ thị trường",
href: `${PATHS.marketProfile}`,
},
{
title: "Môi trường kinh doanh",
href: `${PATHS.tradePromotion}/moi-truong-kinh-doanh`,
},
{
title: "Cơ hội kinh doanh",
href: `${PATHS.tradePromotion}/co-hoi-kinh-doanh`,
},
{
title: "Hỗ trợ kinh doanh",
href: `${PATHS.tradePromotion}/ho-tro-kinh-doanh`,
},
];
export const OWNER_REPRESENTATIVES_CATEGORIES = [
{
title: "Chức năng Đại diện Người sử dụng lao động",
href: `${PATHS.ownerRepresentatives}/chuc-nang-dai-dien-nguoi-su-dung-lao-dong`,
},
{
title: "Sự kiện – Tập huấn NSDLĐ",
href: `${PATHS.ownerRepresentatives}/tap-huan-nsdld`,
},
{
title: "Tin liên quan",
href: `${PATHS.ownerRepresentatives}/tin-lien-quan`,
},
{
title: "Chủ đề",
href: `${PATHS.ownerRepresentatives}/chu-de`,
},
];
export const EVENT_CATEGORIES = [
{
title: "Sự kiện",
href: `${PATHS.event}/su-kien`,
},
{
title: "Đào tạo",
href: `${PATHS.event}/dao-tao`,
},
];
export const MEDIA_INFORMATION_CATEGORIES = [
{
title: "Tin VCCI",
href: `${PATHS.mediaInformation}/tin-vcci`,
},
{
title: "Tin kinh tế",
href: `${PATHS.mediaInformation}/tin-kinh-te`,
},
{
title: "Tin doanh nghiệp",
href: `${PATHS.mediaInformation}/tin-doanh-nghiep`,
},
{
title: "Chuyên đề",
href: `${PATHS.mediaInformation}/chuyen-de`,
},
{
title: "Thông tin chính sách và pháp luật",
href: `${PATHS.mediaInformation}/thong-tin-chinh-sach-va-phap-luat`,
},
{
title: "Ấn phẩm",
href: `${PATHS.mediaInformation}/an-pham`,
},
{
title: "Thư viện tài liệu",
href: `${PATHS.mediaInformation}/thu-vien-tai-lieu`,
},
];
export type CategoryItem = {
title: string;
href: string;
};
export const PATHS = {
home: '/',
general: '/gioi-thieu',
ownerRepresentatives: '/dai-dien-gioi-chu',
event:'/hoat-dong',
marketProfile: '/ho-so-thi-truong',
tradePromotion: '/xuc-tien-thuong-mai',
mediaInformation:'/thong-tin-truyen-thong',
search: '/tim-kiem',
originOfGoods: '/xuat-xu-hang-hoa',
// With slugs
post: '/bai-viet',
postSlug: '/bai-viet/[slug]',
news: '/tin-tuc-su-kien',
newsSlug: '/tin-tuc-su-kien/[slug]',
internal: '/noi-bo',
internalAnnouncement: '/noi-bo/thong-bao',
internalAnnouncementSlug: '/noi-bo/thong-bao/[slug]',
internalWorkSchedule: '/noi-bo/lich-cong-tac',
internalWorkScheduleSlug: '/noi-bo/lich-cong-tac/[slug]'
}
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