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

[tag]0.1-vcci

parents 68d6ad4f 21c709a6
Pipeline #44111 passed with stages
in 11 minutes and 11 seconds
export interface NewsItem {
id: string
title: string
thumbnail: string
external_link: string
description: string
release_at: string
is_active: boolean
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
mode: 'NOW' | string
category: string
}
export interface NewsResponseData {
count: number
rows: NewsItem[]
totalPages: number
currentPage: number
}
export interface GetNewsResponseType {
message: string
message_en: string
responseData: NewsResponseData
status: 'success' | 'error'
timeStamp: string
violations: string | null
}
\ No newline at end of file
......@@ -155,11 +155,11 @@ const Page = () => {
<section>
<div className="flex items-center justify-center py-8 px-4">
<div className="flex items-center w-full max-w-4xl">
<div className="flex-1 h-[1px] bg-gradient-to-r from-transparent via-gray-300 to-gray-400"></div>
<div className="flex-1 h-px bg-linear-to-r from-transparent via-gray-300 to-gray-400"></div>
<h1 className="px-6 text-[20px] sm:text-[24px] md:text-[28px] uppercase font-bold text-[#063e8e] whitespace-nowrap">
Tin Nổi Bật
</h1>
<div className="flex-1 h-[1px] bg-gradient-to-l from-transparent via-gray-300 to-gray-400"></div>
<div className="flex-1 h-px bg-linear-to-l from-transparent via-gray-300 to-gray-400"></div>
</div>
</div>
......@@ -186,7 +186,7 @@ const Page = () => {
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"
e.currentTarget.src = "/VCCI-Chung-300x200-1.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">
......@@ -242,7 +242,7 @@ const Page = () => {
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
e.currentTarget.src = "/VCCI-Chung-300x200-1.png"
}}
/>
</div>
......@@ -459,7 +459,7 @@ const Page = () => {
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
e.currentTarget.src = "/VCCI-Chung-300x200-1.png"
}}
/>
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
......@@ -504,7 +504,7 @@ const Page = () => {
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = "/fallback.png"
e.currentTarget.src = "/VCCI-Chung-300x200-1.png"
}}
/>
<div className="absolute bg-white opacity-80 bottom-5 left-5 right-5 p-5">
......
This diff is collapsed.
export interface NewsDetailItem {
id: string
title: string
thumbnail: string
external_link: string
description: string
release_at: string
is_active: boolean
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
mode: 'NOW' | string
category: string
}
export interface NewsDetailResponseData {
count: number
rows: NewsDetailItem[]
totalPages: number
currentPage: number
}
export interface GetNewsDetailResponseType {
message: string
message_en: string
responseData: NewsDetailItem
status: 'success' | 'error'
timeStamp: string
violations: any | null
}
\ No newline at end of file
'use client';
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import ListCategory from "@/components/base/list-category";
import { useParams } from "next/dist/client/components/navigation";
import { useGetNews } from "@/api/endpoints/news";
import { GetNewsResponseType } from "@/api/types/news";
import EventCalendar from "@/components/base/event-calendar";
import dayjs from "dayjs";
import parse from "html-react-parser";
import { Spinner } from "@/components/ui";
export default function ArticleDetailPage() {
// get url
const params = useParams();
const slug = Array.isArray(params.slug) ? params.slug : [params.slug];
const path = slug.join("/");
//query
const { data: categoriesPage } = useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>({
code: slug[0],
});
const { data, isLoading } = useGetNews<GetNewsResponseType>({
filters: `external_link==/${path}`,
});
return (
<div className='container w-full flex justify-center items-center pb-10'>
{isLoading ? (
<div className='flex justify-center items-center w-full h-64'>
<Spinner />
</div>
) : (
<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 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?.rows[0]?.title}
</div>
<div className='flex items-center gap-2 text-sm mb-4'>
<span className='text-base text-blue-700'>
{dayjs(data?.responseData?.rows[0]?.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?.rows[0]?.description ?? '')}
</div>
</div>
</main>
<aside className="space-y-6">
<EventCalendar />
</aside>
</div>
</div>
)}
</div>
);
}
\ No newline at end of file
'use client';
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import ListCategory from "@/components/base/list-category";
import { useParams } from "next/dist/client/components/navigation";
import { useGetNews } from "@/api/endpoints/news";
import { GetNewsResponseType } from "@/api/types/news";
import CardNews from "@/components/base/card-news";
import { Pagination } from "@/components/base/pagination";
import ListFilter from "@/components/base/list-filter";
import EventCalendar from "@/components/base/event-calendar";
import { useState } from "react";
import { Spinner } from "@/components/ui";
export default function ArticlePage() {
// get url
const params = useParams();
const slug = Array.isArray(params.slug) ? params.slug : [params.slug];
const path = slug.join("/");
// states
const [submitSearch, setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
// query
const { data: categoriesPage } = useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>({
code: slug[0],
});
const { data, isLoading } = useGetNews<GetNewsResponseType>({
filters: `page_config.static_link==/${path}` + (submitSearch ? `,title@=${submitSearch}` : ""),
pageSize: String(pageSize),
currentPage: String(page),
});
return (
<div className="min-h-screen container mx-auto">
{isLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<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 className="lg:col-span-2 bg-background">
<div className="pb-5 overflow-hidden">
{data?.responseData?.rows.map((item) => (
<CardNews
key={item.id}
news={item}
link={`${item.external_link}`}
/>
))}
<div className="w-full flex justify-center mt-4">
<Pagination
pageCount={Number(data?.responseData?.totalPages ?? 1)}
page={Number(data?.responseData?.currentPage ?? page)}
onChangePage={setPage}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() =>
setPage(Math.min(Number(data?.responseData?.totalPages ?? 1), page + 1))
}
/>
</div>
</div>
</main>
<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>
);
}
\ No newline at end of file
'use client';
import { useState } from "react";
import Image from "next/image";
import { useParams } from "next/navigation";
import dayjs from "dayjs";
import parse from "html-react-parser";
import BASE_URL from "@/links";
import { useGetEvents } from "@/api/endpoints/event";
import { EventApiResponse } from "@/api/types/event";
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import { Spinner } from "@/components/ui/spinner";
import ListCategory from "@/components/base/list-category";
import EventCalendar from "@/components/base/event-calendar";
import { CreditCard, MapPin, Clock } from "lucide-react";
export default function EventDetailPage() {
// get url
const params = useParams();
const slug = Array.isArray(params.slug) ? params.slug : [params.slug];
const lastpath = slug[slug.length - 1];
// query
const { data: categoriesPage } = useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>({
code: `${slug[0]}`,
});
const { data: eventsDetail, isLoading } = useGetEvents<EventApiResponse>({
filters: `id==${lastpath}`,
});
return (
<div className='container w-full flex justify-center items-center pb-10'>
{isLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<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 className="lg:col-span-2 bg-white border rounded-md p-8">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{eventsDetail?.responseData?.rows[0].name}
</div>
<hr className="py-2" />
{/* Top summary with image + details */}
<div className="flex flex-col lg:flex-row gap-6 my-6">
<div className="w-full lg:w-1/2 bg-gray-50 rounded-md overflow-hidden">
{eventsDetail?.responseData?.rows[0].image ? (
<div className="w-full h-52 relative ">
<EventImage
src={`${BASE_URL.imageEndpoint}${eventsDetail.responseData.rows[0].image}`}
alt={eventsDetail.responseData.rows[0].name || "image"}
/>
</div>
) : (
<div className="w-full h-52 bg-gray-200" />
)}
</div>
<div className="w-full lg:w-1/2 bg-white border rounded-md p-6">
<div className="flex flex-col gap-3">
<div className="text-sm text-gray-500 flex flex-row items-center gap-2">
<Clock className="h-5 w-5 text-yellow-500" />
<div>
<div className="text-sm font-medium text-gray-800">
Bắt đầu: {eventsDetail?.responseData?.rows[0].start_time
? dayjs(
eventsDetail.responseData.rows[0].start_time
).format('HH:mm DD/MM/YYYY')
: "-"}
</div>
<div className="text-sm font-medium text-gray-800">
Kết thúc: {eventsDetail?.responseData?.rows[0].end_time
? dayjs(
eventsDetail.responseData.rows[0].end_time
).format('HH:mm DD/MM/YYYY')
: "-"}
</div>
</div>
</div>
<div className="text-sm text-gray-500 flex items-center gap-2">
<MapPin className="h-5 w-5 text-blue-600" />
<div className="text-sm font-medium text-gray-800">
Địa điểm: {eventsDetail?.responseData?.rows[0].location ??
eventsDetail?.responseData?.rows[0].province ??
"-"}
</div>
</div>
<div className="text-sm text-gray-500 flex items-center gap-2">
<CreditCard className="h-5 w-5 text-yellow-400" />
<div className="text-sm font-medium text-gray-800">
Phí tham dự: {eventsDetail?.responseData?.rows[0].table_cost
? `${eventsDetail.responseData.rows[0].table_count
} Bàn : ${eventsDetail.responseData.rows[0].table_cost.toLocaleString()} đ`
: "Vui lòng xem chi tiết trong bài"}
</div>
</div>
</div>
</div>
</div>
{/* Full description */}
<div className="p-7.5 prose tiptap overflow-hidden">
{parse(eventsDetail?.responseData?.rows[0].description ?? "")}
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<EventCalendar />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-56 relative bg-gray-100">
<Image
src="/banner.webp"
alt="Quảng cáo"
fill
className="object-cover"
/>
</div>
</div>
</aside>
</div>
</div>
)}
</div >
);
}
// Local small component to safely handle Image src fallback without mutating DOM
type EventImageProps = {
src: string;
alt?: string;
};
function EventImage({ src, alt }: EventImageProps) {
const [imgSrc, setImgSrc] = useState<string>(src);
return (
<Image
src={imgSrc}
alt={alt ?? "image"}
fill
className="object-cover"
onError={() => {
// swap to local fallback file when Next/Image fails to load the provided URL
if (imgSrc !== "/VCCI-Chung-300x200-1.png") setImgSrc("/VCCI-Chung-300x200-1.png");
}}
/>
);
}
\ No newline at end of file
'use client';
import { useState } from "react";
import { useParams } from "next/navigation";
import { useGetEvents } from "@/api/endpoints/event";
import { EventApiResponse } from "@/api/types/event";
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import { Spinner } from "@/components/ui/spinner";
import ListCategory from "@/components/base/list-category";
import CardEvents from "@/components/base/card-events";
import { Pagination } from "@/components/base/pagination";
import ListFilter from "@/components/base/list-filter";
import EventCalendar from "@/components/base/event-calendar";
export default function EventPage() {
// get url
const params = useParams();
const slug = Array.isArray(params.slug) ? params.slug : [params.slug];
// states
const [submitSearch, setSubmitSearch] = useState("");
const [page, setPage] = useState(1);
const pageSize = 5;
// query
const { data: categoriesPage } = useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>({
code: `${slug[0]}`,
});
const { data: events, isLoading } = useGetEvents<EventApiResponse>({
filters: `name@=${submitSearch ? `title@=${submitSearch}` : ""}`,
pageSize: String(pageSize),
currentPage: String(page),
});
//template
return (
<>
<div className="min-h-screen container mx-auto">
{isLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<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 className="lg:col-span-2 bg-background">
<div className="pb-5 overflow-hidden">
{events?.responseData.rows.map((item) => (
<CardEvents
key={item.id}
event={item}
link={`su-kien/${item.id}`}
/>
))}
<div className="w-full flex justify-center mt-4">
<Pagination
pageCount={Number(events?.responseData.totalPages ?? 1)}
page={Number(events?.responseData.currentPage ?? page)}
onChangePage={setPage}
onGoToPreviousPage={() => setPage(Math.max(1, page - 1))}
onGoToNextPage={() =>
setPage(Math.min(Number(events?.responseData.totalPages ?? 1), page + 1))
}
/>
</div>
</div>
</main>
<aside className="space-y-6">
<ListFilter onSearch={setSubmitSearch} />
<EventCalendar />
</aside>
</div>
</div>
)}
</div>
</>
);
}
\ No newline at end of file
'use client';
import { GetNewsPageConfigResponseType } from "@/api/types/news-page-config";
import { useGetNewsPageConfigGetHierarchical } from "@/api/endpoints/news-page-config";
import ListCategory from "@/components/base/list-category";
import { useParams } from "next/dist/client/components/navigation";
import { Spinner } from "@/components/ui/spinner";
import { GetNewsResponseType } from "@/api/types/news";
import { useGetNews } from "@/api/endpoints/news";
import dayjs from "dayjs";
import parse from "html-react-parser";
export default function InformationPage() {
// get url
const params = useParams();
const slug = Array.isArray(params.slug) ? params.slug : [params.slug];
const path = slug.join("/");
// query
const { data: category } = useGetNewsPageConfigGetHierarchical<GetNewsPageConfigResponseType>({
static_link: `/${slug[0]}`,
});
const { data, isLoading } = useGetNews<GetNewsResponseType>({
filters: `page_config.static_link==/${path}`,
});
//template
return (
<div className='container w-full flex justify-center items-center pb-10'>
{isLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<div className='flex flex-col gap-5 w-full'>
<ListCategory categories={category?.responseData?.children} />
<main className=" bg-white border rounded-md py-10 px-30">
<div className='text-primary text-2xl leading-normal font-bold'>
{data?.responseData?.rows[0].title}
</div>
{/* <div className='flex items-center gap-2 text-sm mb-4'>
<span className='text-base text-blue-700'>
{dayjs(data?.responseData?.rows[0].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?.rows[0].description ?? '')}
</div>
</div>
</main>
</div>
)}
</div>
);
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import React from "react";
import links from "@links/index";
export const metadata: Metadata = {
icons: { icon: '/favicon.ico', shortcut: '/favicon.ico' },
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',
description: 'Chức năng Đại diện Người sử dụng lao động (NSDLĐ):',
metadataBase: new URL(links.siteURL),
......
......@@ -18,6 +18,7 @@ export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; ac
const { menu, variant = 'main', active } = props
const pathname = usePathname()
const isActive = pathname.startsWith(menu.link ?? '');
const linkId = useMemo(() => `trigger_${menu.id}`, [menu.id])
const hoverCardRef = useCallback(
(element: HTMLDivElement) => {
......@@ -31,7 +32,7 @@ export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; ac
<HoverCard openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<Link
aria-selected={active || pathname == menu.link}
aria-selected={active || isActive}
id={linkId}
target={(menu.link ?? '').startsWith('/') ? '_self' : '_blank'}
href={menu.link ?? '/'}
......
......@@ -29,7 +29,7 @@ function Calendar({
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
"bg-background group/calendar p-3 [--cell-size:2rem] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
......
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