Commit 51ad7fac authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

fix: dynamic page and add lightbox

parent f1642091
import type { Metadata } from "next";
import links from "@links/index";
import {
fetchDynamicPostById,
fetchDynamicPostBySlug,
getDynamicPostSeoImage,
getDynamicPostExcerpt,
stripHtml,
} from "./templates/data";
import { getApiV10Post, getApiV10PostId } from "@/api/vcci-news/endpoints/post";
import type { DynamicPostItem } from "./templates/types";
import { fetchCmsCategories } from "@/lib/api/cms-admin";
import DynamicPageClient from "./DynamicPageClient";
const STATIC_PAGE_SLUGS = new Set([
"an-pham", "thu-vien-tai-lieu", "ve-vcci-hcm", "dich-vu-cung-cap",
"dang-ky-hoi-vien", "ho-so-thi-truong", "loi-ich-hoi-vien-vcci",
"phap-che", "giay-chung-nhan-gcn-va-chung-tu-thuong-mai-cttm",
"quy-trinh-tiep-nhan-ho-so-cap-gcn-va-xac-nhan-cttm",
"bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm", "phi-cap-gcn-va-xac-nhan-cttm",
"diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm", "thong-tin-lien-he",
"danh-ba-hoi-vien", "search", "site-map", "video",
]);
const SITE_NAME = "VCCI HCM";
const SITE_TAGLINE = "Liên đoàn Thương mại và Công nghiệp Việt Nam - Chi nhánh khu vực TP.HCM";
const STATIC_PAGE_METADATA: Record<string, { title: string; description: string }> = {
"an-pham": {
title: "Ấn phẩm",
description:
"Tổng hợp các ấn phẩm, báo cáo và ấn phẩm định kỳ do VCCI-HCM phát hành, cập nhật thông tin doanh nghiệp và thị trường.",
},
"thu-vien-tai-lieu": {
title: "Thư viện tài liệu",
description:
"Thư viện tài liệu VCCI-HCM: tài liệu nghiên cứu, báo cáo thị trường, văn bản pháp lý và ấn phẩm hỗ trợ doanh nghiệp.",
},
"ve-vcci-hcm": {
title: "Về VCCI HCM",
description:
"Giới thiệu về Liên đoàn Thương mại và Công nghiệp Việt Nam - Chi nhánh khu vực TP.HCM: tầm nhìn, sứ mệnh, giá trị cốt lõi và lĩnh vực hoạt động.",
},
"dich-vu-cung-cap": {
title: "Dịch vụ cung cấp",
description:
"Các dịch vụ VCCI-HCM cung cấp cho hội viên và doanh nghiệp: sự kiện, đào tạo, xúc tiến thương mại, cho thuê văn phòng và hỗ trợ pháp lý.",
},
"dang-ky-hoi-vien": {
title: "Đăng ký hội viên",
description:
"Hướng dẫn hồ sơ, điều kiện và phí đăng ký trở thành hội viên chính thức của VCCI, cùng các biểu mẫu đính kèm.",
},
"ho-so-thi-truong": {
title: "Hồ sơ thị trường",
description:
"Hồ sơ thị trường các khu vực Đông Nam Á, Đông Bắc Á, Âu - Mỹ, Trung Đông - Châu Phi hỗ trợ doanh nghiệp tiếp cận cơ hội xuất khẩu và đối tác.",
},
"loi-ich-hoi-vien-vcci": {
title: "Lợi ích hội viên VCCI",
description:
"Các lợi ích khi trở thành hội viên VCCI-HCM: tiếng nói đại diện, nhận diện thương hiệu, hỗ trợ pháp lý và ưu đãi dịch vụ.",
},
"phap-che": {
title: "Pháp chế",
description:
"Dịch vụ pháp chế của VCCI-HCM: góp ý xây dựng pháp luật, tư vấn chuyên sâu, dịch vụ thương mại và đào tạo nghiệp vụ pháp lý.",
},
"giay-chung-nhan-gcn-va-chung-tu-thuong-mai-cttm": {
title: "Giấy chứng nhận GCN và chứng từ thương mại CTTM",
description:
"Giới thiệu về Giấy chứng nhận của VCCI và xác nhận Chứng từ thương mại (CTTM): định nghĩa, đơn vị cấp và phạm vi áp dụng.",
},
"quy-trinh-tiep-nhan-ho-so-cap-gcn-va-xac-nhan-cttm": {
title: "Quy trình tiếp nhận hồ sơ cấp GCN và xác nhận CTTM",
description:
"Quy trình 5 bước tiếp nhận hồ sơ cấp Giấy chứng nhận (GCN) và xác nhận Chứng từ thương mại (CTTM) trên hệ thống COVCCI.",
},
"bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm": {
title: "Biểu mẫu GCN và nội dung khai báo GCN, CTTM",
description:
"Biểu mẫu và hướng dẫn nội dung khai báo Giấy chứng nhận (GCN), Chứng từ thương mại (CTTM) do VCCI cấp.",
},
"phi-cap-gcn-va-xac-nhan-cttm": {
title: "Phí cấp GCN và xác nhận CTTM",
description:
"Bảng giá dịch vụ cấp Giấy chứng nhận (GCN) và xác nhận Chứng từ thương mại (CTTM) của VCCI.",
},
"diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm": {
title: "Điểm cấp và thời gian cấp GCN và xác nhận CTTM",
description:
"Địa chỉ, điện thoại và thời gian làm việc của các điểm cấp Giấy chứng nhận (GCN) và xác nhận Chứng từ thương mại (CTTM).",
},
"thong-tin-lien-he": {
title: "Thông tin liên hệ",
description:
"Thông tin liên hệ Phòng Pháp chế và xác nhận Chứng từ thương mại - VCCI-HCM: địa chỉ, điện thoại và email.",
},
"danh-ba-hoi-vien": {
title: "Danh bạ hội viên",
description:
"Danh bạ hội viên VCCI-HCM: tra cứu thông tin doanh nghiệp hội viên và kết nối đối tác kinh doanh.",
},
"search": {
title: "Tìm kiếm",
description:
"Tìm kiếm tin tức, bài viết và nội dung trên website VCCI-HCM theo từ khóa.",
},
"site-map": {
title: "Sơ đồ site",
description:
"Sơ đồ tổng quan các chuyên mục và trang trên website VCCI-HCM, hỗ trợ điều hướng nhanh.",
},
"video": {
title: "Video",
description:
"Thư viện video VCCI-HCM: các phóng sự, sự kiện, hội thảo và hoạt động xúc tiến thương mại.",
},
};
/**
* Build an absolute og:image URL. Relative paths (e.g. "/thumbnail.png") are
* resolved against the site origin so social crawlers always receive an
......@@ -61,20 +168,59 @@ export async function generateMetadata({
const path = `/${(slug ?? []).join("/")}`;
const postId = id?.trim() ?? "";
const endingSlug = slug?.length ? String(slug[slug.length - 1] ?? "") : "";
let post = null;
if (STATIC_PAGE_SLUGS.has(endingSlug)) {
const pageMeta = STATIC_PAGE_METADATA[endingSlug] ?? {
title: SITE_NAME,
description: SITE_TAGLINE,
};
return {
title: pageMeta.title,
description: pageMeta.description,
alternates: { canonical: `${links.siteURL.replace(/\/+$/, "")}${path}` },
};
}
// Try to find a matching category for non-post routes
let categoryTitle = "";
if (!postId) {
try {
const categories = await fetchCmsCategories();
const matched = categories.find((item) => item.url === path || `/${item.slug}` === path);
if (matched) categoryTitle = matched.name;
} catch {
// ignore
}
}
let post: DynamicPostItem | null = null;
try {
post = postId
? await fetchDynamicPostById(postId)
: await fetchDynamicPostBySlug(path);
if (postId) {
const response = await getApiV10PostId(postId).catch(() => null);
post = (response?.responseData ?? null) as unknown as DynamicPostItem | null;
} else {
const slugFromPath = path.split("/").filter(Boolean).pop() ?? "";
if (slugFromPath) {
const response = await getApiV10Post({
page: 1,
pageSize: 1,
filters: `slug==${slugFromPath},is_hidden==false,is_active==true,type==news`,
}).catch(() => null);
const rows = response?.responseData?.rows ?? [];
post = (rows[0] as unknown as DynamicPostItem) ?? null;
}
}
} catch {
post = null;
}
if (!post || !post.title) {
const title = categoryTitle || "VCCI HCM";
return {
title: "Bài viết không tìm thấy",
robots: { index: false, follow: false },
title,
description: "Liên đoàn Thương mại và Công nghiệp Việt Nam - Chi nhánh khu vực TP.HCM",
alternates: { canonical: `${links.siteURL.replace(/\/+$/, "")}${path}` },
};
}
......@@ -121,4 +267,4 @@ export async function generateMetadata({
export default function Page() {
return <DynamicPageClient />;
}
}
\ No newline at end of file
This diff is collapsed.
'use client';
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
......@@ -8,24 +8,24 @@ import { Pagination } from "@/components/base/pagination";
import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category";
import EventsCalendar from "@/app/(main)/(home)/components/events-calendar";
import SidebarAdvertisements from "@/components/shared/sidebar-advertisements";
import ListCategory from "@/components/base/list-category";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import {
buildDynamicPostHref,
buildDynamicCategoryMenu,
buildVisibleNewsFilters,
useDynamicPostList,
resolveDynamicPostImage,
} from "./data";
import type { DynamicCategoryRouteItem } from "./types";
mapPost,
} from "../templates/data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../templates/types";
type CatalogPageProps = {
category: DynamicCategoryRouteItem;
type AnPhamProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function CatalogPage({ category, allCategories }: CatalogPageProps) {
export default function AnPham({ category, allCategories }: AnPhamProps) {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
......@@ -64,25 +64,39 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
}
}, [page, pathname, router, searchParamsString]);
const postsQuery = useDynamicPostList({
const filters = [
category?.id ? `category.id==${category.id}` : null,
keyword ? `title@=${keyword}` : null,
"is_hidden==false",
"is_active==true",
"type==news",
]
.map((item) => item?.trim())
.filter(Boolean)
.join(",");
const { data: postsData, isLoading: postsLoading } = useGetApiV10Post({
page,
pageSize,
filters: buildVisibleNewsFilters([
`category.id==${category.id}`,
keyword ? `title@=${keyword}` : null,
]),
staleTime: 60 * 1000,
sortField: "release_at",
sortOrder: "desc",
filters: filters || undefined,
});
const totalPages = postsQuery.data?.totalPages ?? 1;
const responseData = postsData?.responseData;
const count = Number(responseData?.count ?? 0);
const totalPages = pageSize > 0 ? Math.max(1, Math.ceil(count / pageSize)) : 1;
const currentPage = Math.min(page, totalPages);
const paginatedPosts = postsQuery.data?.rows ?? [];
const categoryMenu = buildDynamicCategoryMenu(category, allCategories);
const paginatedPosts = ((responseData?.rows ?? []) as unknown as Parameters<typeof mapPost>[0][])
.map(mapPost)
.filter((item: DynamicPostItem) => item.id && item.title);
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
{postsQuery.isLoading ? (
{postsLoading ? (
<div className="flex h-64 w-full items-center justify-center">
<Spinner />
</div>
......@@ -90,7 +104,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
<div className="container mx-auto px-4 py-4 lg:pb-6 sm:px-6 lg:px-10">
<div className="mb-8">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-4xl">
{category.name}
{category?.name ?? "Ấn phẩm"}
</h1>
<div className="mt-2 h-[3px] w-16 rounded-full bg-[#f5a400]" />
</div>
......@@ -103,7 +117,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
return (
<Link
key={item.id}
href={buildDynamicPostHref(item.slug, item.id, category.id)}
href={buildDynamicPostHref(item.slug, item.id, category?.id)}
className="group block"
>
<div className="overflow-hidden bg-white shadow-[0_10px_24px_rgba(17,24,39,0.08)]">
......
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { Mail, MapPin, Phone, UserRound } from "lucide-react";
type ContactProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Contact({ category, allCategories }: ContactProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="py-2">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Thông tin liên hệ
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 space-y-5">
<article className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">
Phòng Pháp chế và xác nhận Chứng từ thương mại
</h2>
<div className="mt-5 space-y-4 text-[16px] leading-8 text-[#5f6f86]">
<p>Liên đoàn Thương mại và Công nghiệp Việt Nam – Chi nhánh khu vực Thành phố Hồ Chí Minh (VCCI-HCM)</p>
<div className="flex items-start gap-3">
<MapPin className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>Phòng 103, Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, P. Xuân Hòa, TP. HCM</span>
</div>
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>028-3932 6498</span>
</div>
<div className="flex items-start gap-3">
<Mail className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>co@vcci-hcm.org.vn</span>
</div>
</div>
</article>
<article className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">
Xử lý vướng mắc, phản ánh, góp ý trong quá trình làm thủ tục cấp GCN và xác nhận CTTM
</h2>
<div className="mt-5 space-y-5 text-[16px] leading-8 text-[#5f6f86]">
<div>
<p className="font-semibold text-[#1f2a44]">Điểm cấp số 1</p>
<p>Điện thoại: 028-3932 6498</p>
<p>Email: co@vcci-hcm.org.vn</p>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Điểm cấp số 2</p>
<p>Phó Trưởng phòng: Nguyễn Văn Đức</p>
<p>Mobile: 090 949 7155</p>
<p>Email: nvduc1980@gmail.com</p>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Điểm cấp số 3</p>
<p>Trưởng phòng (Cơ sở 2): Bà Ma Thị Hương</p>
<p>Mobile: 039 512 2922</p>
<p>Email: huongmtvccivt@gmail.com</p>
</div>
</div>
</article>
<article className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">
Hướng dẫn hồ sơ và tiếp nhận phản ánh
</h2>
<div className="mt-5 space-y-5 text-[16px] leading-8 text-[#5f6f86]">
<div>
<p className="font-semibold text-[#1f2a44]">Hướng dẫn khai hồ sơ thương nhân, chữ ký số và IT</p>
<p>Điểm cấp số 1, điện thoại: 028-3932 6498</p>
<p>Điểm cấp số 2, điện thoại: 0274-380 0048</p>
<p>Điểm cấp số 3, điện thoại: 025-4385 2710</p>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Tiếp thu, giải quyết phản ánh, khiếu nại, góp ý</p>
<p>Trưởng phòng (Trụ sở chính): Ông Vũ Xuân Hưng</p>
<p>Điện thoại: 028-3932 6929 hoặc Mobile: 0909 170 171 (Đường dây nóng)</p>
<p>Email: vuxuanhung@vcci-hcm.org.vn</p>
</div>
</div>
</article>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Đầu mối hỗ trợ</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<Mail className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>co@vcci-hcm.org.vn</span>
</div>
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>Đường dây nóng: 0909 170 171</span>
</div>
<div className="flex items-start gap-3">
<UserRound className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>Ông Vũ Xuân Hưng</span>
</div>
</div>
</div>
</aside>
</div>
</section>
</div>
</div>
);
}
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { CircleDollarSign, FileStack, Layers3 } from "lucide-react";
const FEE_ITEMS = [
{
title: "Một bộ GCN, CTTM (4 bản)",
value: "100.000đ/bộ",
icon: FileStack,
},
{
title: "Bản làm thêm tính từ bản thứ 5 trở lên",
value: "10.000đ/bản",
icon: Layers3,
},
{
title: "Phôi Giấy chứng nhận",
value: "20.000đ/tờ",
icon: CircleDollarSign,
},
] as const;
type FeesProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Fees({ category, allCategories }: FeesProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Phí cấp GCN và xác nhận CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-3xl border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-[#2450b5]">Biểu phí hiện hành</p>
<p className="mt-3 text-[16px] leading-8 text-[#5f6f86]">
Mức phí áp dụng cho việc cấp Giấy chứng nhận và xác nhận Chứng từ thương mại được tính theo từng loại hồ sơ và số lượng bản phát hành.
</p>
</div>
<div className="mt-8 grid gap-4 md:grid-cols-3">
{FEE_ITEMS.map((item, index) => {
const Icon = item.icon;
return (
<article
key={item.title}
className={[
"rounded-[26px] border px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]",
index === 0 ? "border-[#dbe7ff] bg-[#f8fbff]" : "border-[#edf1f6] bg-white",
].join(" ")}
>
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<Icon className="h-5 w-5" />
</div>
<h2 className="mt-4 text-[20px] font-bold leading-tight text-[#1f2a44]">{item.title}</h2>
<p className="mt-4 text-[30px] font-bold leading-none text-[#2450b5]">{item.value}</p>
</article>
);
})}
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Tóm tắt chi phí</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>01 bộ tiêu chuẩn gồm 4 bản</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Có phụ phí cho bản làm thêm từ bản thứ 5</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Phôi Giấy chứng nhận được tính riêng theo từng tờ</span>
</div>
</div>
</div>
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Lưu ý khi chuẩn bị</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<p>Kiểm tra trước số lượng bản cần cấp để chuẩn bị đúng chi phí thực hiện.</p>
<p>Chuẩn bị lệ phí đầy đủ sẽ giúp quá trình tiếp nhận và xử lý hồ sơ diễn ra nhanh hơn.</p>
</div>
</div>
</aside>
</div>
</section>
</div>
</div>
);
}
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { Download, FileText } from "lucide-react";
type FormsProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Forms({ category, allCategories }: FormsProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="py-2">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Biểu mẫu GCN và nội dung khai báo GCN, CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-[26px] border border-[#edf1f6] bg-white px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]">
<div className="flex items-start gap-4">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<FileText className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<h2 className="text-[22px] font-bold leading-tight text-[#1f2a44]">
bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm.docx
</h2>
<p className="mt-3 text-[16px] leading-8 text-[#5f6f86]">
Nhấn tải xuống để xem toàn bộ biểu mẫu và nội dung khai báo.
</p>
<a
href="/bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm.docx"
download
className="mt-5 inline-flex items-center gap-2 rounded-[4px] bg-[#2450b5] px-5 py-3 text-[15px] font-semibold text-white transition-colors hover:bg-[#173f9f]"
>
<Download className="h-4 w-4" />
Tải biểu mẫu
</a>
</div>
</div>
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Lưu ý</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Người dùng có thể tải về để xem biểu mẫu đầy đủ trên máy của mình.</span>
</div>
</div>
</div>
</aside>
</div>
</section>
</div>
</div>
);
}
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { Building2, Clock3, MapPin, Phone } from "lucide-react";
const LOCATIONS = [
{
title: "Điểm cấp số 1",
address:
"Phòng 103, Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, Phường Xuân Hòa, Thành phố Hồ Chí Minh",
phone: "028-3932 6498",
},
{
title: "Điểm cấp số 2",
address:
"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, Phường Bình Hòa, Thành phố Hồ Chí Minh",
phone: "0274-380 0048",
},
{
title: "Điểm cấp số 3",
address: "155 Nguyễn Thái Học, Phường Tam Thắng, Thành phố Hồ Chí Minh",
phone: "025-4385 2710",
},
] as const;
type LocationsProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Locations({ category, allCategories }: LocationsProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Điểm cấp và cấp GCN và xác nhận CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-3xl border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Building2 className="h-5 w-5" />
<p className="text-sm font-semibold uppercase tracking-[0.18em]">
1. Các điểm cấp GCN và xác nhận CTTM thuộc VCCI-HCM
</p>
</div>
</div>
<div className="mt-8 grid gap-4">
{LOCATIONS.map((item, index) => (
<article
key={item.title}
className={[
"rounded-[26px] border px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]",
index === 0 ? "border-[#dbe7ff] bg-[#f8fbff]" : "border-[#edf1f6] bg-white",
].join(" ")}
>
<div className="flex items-start gap-4">
<span className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-lg font-bold text-[#2450b5]">
{index + 1}
</span>
<div className="min-w-0 flex-1">
<h2 className="text-[22px] font-bold leading-tight text-[#1f2a44]">{item.title}</h2>
<div className="mt-4 space-y-3 text-[16px] leading-8 text-[#5f6f86]">
<div className="flex items-start gap-3">
<MapPin className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>{item.address}</span>
</div>
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>{item.phone}</span>
</div>
</div>
</div>
</div>
</article>
))}
</div>
<div className="mt-8 rounded-3xl border border-[#dbe7ff] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Clock3 className="h-5 w-5" />
<p className="text-sm font-semibold uppercase tracking-[0.18em]">2. Giờ tiếp nhận hồ sơ</p>
</div>
<div className="mt-4 space-y-2 text-[16px] leading-8 text-[#5f6f86]">
<p>– Từ thứ Hai đến thứ Sáu</p>
<p>Buổi sáng: 7h30 – 11h30</p>
<p>Buổi chiều: 13h30 – 16h30</p>
<p>Thời gian cấp: không quá 08 giờ làm việc</p>
</div>
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Liên hệ nhanh</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
{LOCATIONS.map((item) => (
<div key={item.title} className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>
{item.title}: {item.phone}
</span>
</div>
))}
</div>
</div>
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Khung giờ làm việc</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<p>Từ thứ Hai đến thứ Sáu</p>
<p>Buổi sáng: 7h30 – 11h30</p>
<p>Buổi chiều: 13h30 – 16h30</p>
<p>Thời gian cấp: không quá 08 giờ làm việc</p>
</div>
</div>
</aside>
</div>
</section>
</div>
</div>
);
}
'use client';
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import {
BadgeCheck,
Bell,
......@@ -82,89 +85,101 @@ const MEMBER_BENEFITS = [
},
] as const;
export default function MemberBenefitsPage() {
return (
<section className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div>
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Lợi ích của hội viên VCCI
</h1>
<div className="mt-3 h-[3px] w-14 rounded-full bg-[#f5a400]" />
type MemberBenefitsProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
<div className="mt-7 space-y-7">
{MEMBER_BENEFITS.map((section) => {
const Icon = section.icon;
export default function MemberBenefits({ category, allCategories }: MemberBenefitsProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<article key={section.key} className="flex items-start gap-4">
<div
className={`mt-1 flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl text-white shadow-[0_12px_30px_rgba(17,24,39,0.12)] ${section.iconClassName}`}
>
<Icon className="h-5 w-5" />
</div>
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div>
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Lợi ích của hội viên VCCI
</h1>
<div className="mt-3 h-[3px] w-14 rounded-full bg-[#f5a400]" />
<div className="min-w-0">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44]">
{section.title}
</h2>
<ul className="mt-3 space-y-2.5">
{section.bullets.map((bullet) => (
<li
key={bullet}
className="flex items-start gap-3 text-[16px] leading-8 text-[#5f6f86]"
>
<span className="mt-3 h-2 w-2 shrink-0 rounded-full bg-[#f5a400]" />
<span>{bullet}</span>
</li>
))}
</ul>
</div>
</article>
);
})}
</div>
</div>
<div className="mt-7 space-y-7">
{MEMBER_BENEFITS.map((section) => {
const Icon = section.icon;
<aside className="rounded-[28px] bg-[#1c56a1] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)] lg:sticky lg:top-24">
<h2 className="text-[28px] font-bold leading-tight">Liên hệ</h2>
<div className="mt-6 space-y-5 text-[15px] leading-7 text-white/90">
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<Phone className="h-4 w-4" />
<span className="font-semibold">Phòng Hội viên Đào tạo và Truyền thông</span>
</div>
<p className="mt-1">C. Thanh Thủy</p>
<p>ĐT: 0903 909 796</p>
</div>
return (
<article key={section.key} className="flex items-start gap-4">
<div
className={`mt-1 flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl text-white shadow-[0_12px_30px_rgba(17,24,39,0.12)] ${section.iconClassName}`}
>
<Icon className="h-5 w-5" />
</div>
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<Mail className="h-4 w-4" />
<span className="font-semibold">Email</span>
<div className="min-w-0">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44]">
{section.title}
</h2>
<ul className="mt-3 space-y-2.5">
{section.bullets.map((bullet) => (
<li
key={bullet}
className="flex items-start gap-3 text-[16px] leading-8 text-[#5f6f86]"
>
<span className="mt-3 h-2 w-2 shrink-0 rounded-full bg-[#f5a400]" />
<span>{bullet}</span>
</li>
))}
</ul>
</div>
</article>
);
})}
</div>
<p className="mt-1 wrap-break-word">luuthanhthuy72@yahoo.com</p>
<p className="wrap-break-word">hoivien@vcci-hcm.org.vn</p>
</div>
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<WalletCards className="h-4 w-4" />
<span className="font-semibold">Điện thoại</span>
</div>
<p className="mt-1">028. 3932 0817 - Fax: 028. 3932 5472</p>
</div>
<aside className="rounded-[28px] bg-[#1c56a1] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)] lg:sticky lg:top-24">
<h2 className="text-[28px] font-bold leading-tight">Liên hệ</h2>
<div className="mt-6 space-y-5 text-[15px] leading-7 text-white/90">
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<Phone className="h-4 w-4" />
<span className="font-semibold">Phòng Hội viên Đào tạo và Truyền thông</span>
</div>
<p className="mt-1">C. Thanh Thủy</p>
<p>ĐT: 0903 909 796</p>
</div>
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<MapPin className="h-4 w-4" />
<span className="font-semibold">Địa chỉ</span>
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<Mail className="h-4 w-4" />
<span className="font-semibold">Email</span>
</div>
<p className="mt-1 wrap-break-word">luuthanhthuy72@yahoo.com</p>
<p className="wrap-break-word">hoivien@vcci-hcm.org.vn</p>
</div>
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<WalletCards className="h-4 w-4" />
<span className="font-semibold">Điện thoại</span>
</div>
<p className="mt-1">028. 3932 0817 - Fax: 028. 3932 5472</p>
</div>
<div>
<div className="flex items-center gap-2 text-[#f5c21b]">
<MapPin className="h-4 w-4" />
<span className="font-semibold">Địa chỉ</span>
</div>
<p className="mt-1">
P. 306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, Phường Xuân Hoà, TP. HCM
</p>
</div>
</div>
<p className="mt-1">
P. 306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, Phường Xuân Hoà, TP. HCM
</p>
</div>
</div>
</aside>
</section>
</aside>
</section>
</div>
</div>
);
}
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
type MemberDirectoryProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function MemberDirectory({ category, allCategories }: MemberDirectoryProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container flex justify-center items-center h-full py-20">
Danh bạ hội viên đang được xây dựng
</div>
</>
);
}
"use client";
import links from "@/links";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../templates/types";
const MEMBERSHIP_REQUIREMENTS = [
"Đơ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)",
"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)",
];
const MEMBERSHIP_FEES = [
"Doanh số dưới 10 tỉ đồng đóng 3 triệu đồng/năm",
"Doanh số từ 10 - 50 tỉ đồng đóng 7 triệu đồng/năm",
"Doanh số trên 50 tỉ đồng đóng 15 triệu đồng/năm",
];
const ATTACHED_FORMS = [
{
label: "Đơn đăng ký tham gia nhập hội viên VCCI (Mẫu Doanh nghiệp)",
href: "/Don-dang-ky-tham-gia-nhap-hoi-vien-VCCI_Mau-Doanh-nghiep-1.docx",
download: true,
},
{
label: "Đơn đăng ký tham gia nhập hội viên VCCI (Mẫu Hiệp hội)",
href: "/Don-dang-ky-tham-gia-nhap-hoi-vien-VCCI_Mau-Hiep-hoi.docx",
download: true,
},
{
label: "Hướng dẫn hồ sơ đăng ký Hội viên VCCI",
href: `${links.externalApiOrigin}/dang-ky`,
download: false,
},
] as const;
const DEFAULT_INTRO =
"Đ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.";
type MemberRegistrationProps = {
post: DynamicPostItem | null;
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function MemberRegistration({ post, category, allCategories }: MemberRegistrationProps) {
const introText = post?.content?.trim() || DEFAULT_INTRO;
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Đăng ký hội viên
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 space-y-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
{introText}
</p>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
Để 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="space-y-2 pl-6 text-[18px] leading-9 text-[#1f2a44]">
{MEMBERSHIP_REQUIREMENTS.map((item) => (
<li key={item} className="list-disc">
<strong>{item}</strong>
</li>
))}
</ul>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
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. Theo quyết định của Ban chấp hành VCCI, lệ phí hiện hành được tính như sau:
</p>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
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="space-y-2 pl-6 text-[18px] leading-9 text-[#1f2a44]">
{MEMBERSHIP_FEES.map((item) => (
<li key={item} className="list-disc">
{item}
</li>
))}
</ul>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
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>
<p className="font-semibold text-[#2450b5]">Để biết thêm thông tin chi tiết, vui lòng liên hệ:</p>
<div className="mt-4 space-y-1 text-[18px] leading-9 text-[#1f2a44]">
<p className="font-semibold">Phòng Hội viên Đào tạo và Truyền thông:</p>
<p>C. Thúy – ĐD: 0903 909 756</p>
<p>Email: luuthanhthuy72@yahoo.com; hoivien@vcci-hcm.org.vn</p>
<p>Điện thoại: 028. 3932 0611 – Fax: 028. 3932 5472</p>
<p>Đị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</p>
</div>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Biểu mẫu đính kèm:</p>
<ul className="mt-3 space-y-2 pl-6 text-[#2450b5]">
{ATTACHED_FORMS.map((item) => (
<li key={item.label} className="list-disc italic">
{item.download ? (
<a href={item.href} download className="hover:text-[#173f9f]">
{item.label}
</a>
) : (
<a href={item.href} target="_blank" rel="noreferrer" className="hover:text-[#173f9f]">
{item.label}
</a>
)}
</li>
))}
</ul>
</div>
<div className="flex justify-center pt-4">
<a
href={`https://vccihcm.vn/dang-ky`}
target="_blank"
rel="noreferrer"
className="inline-flex min-w-[220px] items-center justify-center rounded-[4px] bg-[#2450b5] px-6 py-4 text-[18px] font-semibold text-white transition-colors hover:bg-[#173f9f]"
>
Đăng ký Hội viên
</a>
</div>
</div>
</div>
</section>
</div>
</div>
);
}
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { BriefcaseBusiness, FileText, GraduationCap, Mail, Phone, Scale } from "lucide-react";
const PHAP_CHE_SERVICES = [
{
title: "Góp ý và hỗ trợ pháp lý",
description:
"Tập hợp ý kiến góp ý xây dựng pháp luật, tiếp nhận các khó khăn, vướng mắc trong hoạt động kinh doanh của doanh nghiệp.",
icon: Scale,
},
{
title: "Tư vấn chuyên sâu",
description:
"Tư vấn, kết nối và cung cấp dịch vụ pháp lý kinh doanh chuyên sâu (Luật sư/ Trọng tài viên).",
icon: FileText,
},
{
title: "Dịch vụ thương mại",
description:
"Dịch vụ xuất khẩu, nhập khẩu; Chứng nhận lãnh sự; Phân loại HS; C/O; Lộ trình thuế quan trong các FTA; …",
icon: BriefcaseBusiness,
},
{
title: "Đào tạo chuyên sâu",
description:
"Tổ chức tập huấn đào tạo chuyên sâu trong các lĩnh vực Thuế; Hải quan; Tài chính kế toán; Phân loại mã số hàng hóa (Mã HS); Xuất xứ hàng hóa; Những vấn đề pháp lý của hợp đồng mua bán hàng hóa trong nước và quốc tế; …",
icon: GraduationCap,
},
];
type PhapCheProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function PhapChe({ category, allCategories }: PhapCheProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Pháp chế
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-[24px] border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Phone className="h-5 w-5" />
<span className="text-sm font-semibold uppercase tracking-[0.18em]">Điện thoại</span>
</div>
<p className="mt-3 text-[28px] font-bold text-[#1f2a44]">028-3932 6498</p>
</div>
<div className="rounded-[24px] border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Mail className="h-5 w-5" />
<span className="text-sm font-semibold uppercase tracking-[0.18em]">Email</span>
</div>
<p className="mt-3 break-words text-[22px] font-bold text-[#1f2a44]">co@vcci-hcm.org.vn</p>
</div>
</div>
<div className="mt-8 grid gap-4 xl:grid-cols-2">
{PHAP_CHE_SERVICES.map((item) => {
const Icon = item.icon;
return (
<article
key={item.title}
className="rounded-[26px] border border-[#edf1f6] bg-white px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]"
>
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<Icon className="h-5 w-5" />
</div>
<h2 className="mt-4 text-[22px] font-bold leading-tight text-[#1f2a44]">{item.title}</h2>
<p className="mt-3 text-[16px] leading-8 text-[#5f6f86]">{item.description}</p>
</article>
);
})}
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Phạm vi hỗ trợ</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Góp ý xây dựng pháp luật và tiếp nhận vướng mắc doanh nghiệp</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Tư vấn và kết nối chuyên gia pháp lý kinh doanh</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Đào tạo chuyên sâu về thuế, hải quan, xuất xứ và hợp đồng</span>
</div>
</div>
</div>
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Liên hệ Pháp chế</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#f5c21b]" />
<span>028-3932 6498</span>
</div>
<div className="flex items-start gap-3">
<Mail className="mt-1 h-4 w-4 shrink-0 text-[#f5c21b]" />
<span>co@vcci-hcm.org.vn</span>
</div>
</div>
</div>
</aside>
</div>
</section>
</div>
</div>
);
}
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { Clock3, FileCheck2, FileUp, Landmark, ReceiptText, RotateCcw } from "lucide-react";
const REQUIREMENTS = [
"Đăng ký tài khoản Thương nhân và khai báo các trường thông tin của Thương nhân trên Hệ thống COVCCI.",
"Liên hệ với Đơn vị cấp Giấy chứng nhận, xác nhận Chứng từ thương mại của VCCI kích hoạt tài khoản cho Thương nhân.",
];
const STEPS = [
{
title: "Bước 1. Khai báo trực tuyến",
description:
"Thương nhân truy cập Hệ thống COVCCI, thực hiện khai báo các thông tin theo hướng dẫn, đính kèm hồ sơ dưới dạng điện tử đã được Thương nhân xác nhận bằng chữ ký số do cơ quan có thẩm quyền cấp và nhận số tham chiếu cho bộ hồ sơ.",
icon: FileUp,
},
{
title: "Bước 2. Thanh toán giá dịch vụ và hồ sơ giấy",
description:
"Thương nhân thanh toán giá dịch vụ và bộ hồ sơ giấy đầy đủ theo quy định tại bộ phận tiếp nhận của đơn vị VCCI có thẩm quyền.",
icon: ReceiptText,
},
{
title: "Bước 3. Phân công và thẩm định",
description:
"Bộ phận tiếp nhận hồ sơ kiểm tra tính đầy đủ, hợp lệ ban đầu và phân công cho cán bộ nghiệp vụ xử lý.",
icon: FileCheck2,
},
{
title: "Bước 4. Phê duyệt và cấp",
description:
"Nếu hồ sơ hợp lệ và đầy đủ, cán bộ nghiệp vụ trình hồ sơ lên người có thẩm quyền ký duyệt. Sau khi được ký duyệt, chứng từ sẽ được đóng dấu, tách, lưu trữ (hoặc vào hộp) và trả kết quả cho Thương nhân.",
icon: Landmark,
},
{
title: "Bước 5. Trả hồ sơ hoặc yêu cầu bổ sung",
description:
"Nếu hồ sơ có sai sót, không hợp lệ hoặc vi phạm các quy định, cán bộ nghiệp vụ thông báo rõ lý do cho Thương nhân (thông qua hệ thống hoặc trực tiếp) để yêu cầu sửa đổi, bổ sung hoặc từ chối cấp.",
icon: RotateCcw,
},
];
type ProcedureProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Procedure({ category, allCategories }: ProcedureProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Quy trình tiếp nhận hồ sơ cấp GCN và xác nhận CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-[24px] border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-[#2450b5]">
1. Yêu cầu đối với Thương nhân
</p>
<div className="mt-4 space-y-3 text-[16px] leading-8 text-[#5f6f86]">
{REQUIREMENTS.map((item) => (
<div key={item} className="flex items-start gap-3">
<span className="mt-3 h-2 w-2 shrink-0 rounded-full bg-[#f5a400]" />
<span>{item}</span>
</div>
))}
</div>
</div>
<div className="mt-8">
<div className="flex items-center gap-3">
<div className="h-11 w-11 rounded-2xl bg-[#eff4ff] text-[#2450b5] flex items-center justify-center">
<FileCheck2 className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-[#2450b5]">
2. Quy trình thực hiện
</p>
<h2 className="mt-1 text-[24px] font-bold leading-tight text-[#1f2a44]">
Các bước xử lý thống nhất
</h2>
</div>
</div>
<div className="mt-6 space-y-4">
{STEPS.map((step, index) => {
const Icon = step.icon;
return (
<article
key={step.title}
className="rounded-[24px] border border-[#edf1f6] bg-white px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]"
>
<div className="flex items-start gap-4">
<div className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<Icon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<span className="inline-flex h-7 min-w-7 items-center justify-center rounded-full bg-[#2450b5] px-2 text-sm font-bold text-white">
{index + 1}
</span>
<h3 className="text-[20px] font-bold leading-tight text-[#1f2a44]">{step.title}</h3>
</div>
<p className="mt-3 text-[16px] leading-8 text-[#5f6f86]">{step.description}</p>
</div>
</div>
</article>
);
})}
</div>
</div>
<div className="mt-8 rounded-[24px] border border-[#dbe7ff] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Clock3 className="h-5 w-5" />
<p className="text-sm font-semibold uppercase tracking-[0.18em]">3. Thời gian xử lý</p>
</div>
<p className="mt-4 text-[16px] leading-8 text-[#5f6f86]">
Trường hợp hồ sơ chưa hợp lệ, VCCI sẽ thông báo qua Hệ thống COVCCI các nội dung cần sửa đổi, bổ sung cho Thương nhân trong thời hạn 03 ngày làm việc kể từ ngày tiếp nhận hồ sơ. Thời gian xử lý cho một bộ hồ sơ hợp lệ là không quá 08 giờ làm việc kể từ thời điểm VCCI nhận đủ hồ sơ hợp lệ. Đối với trường hợp cần thẩm tra, xác minh hay trao đổi nội bộ và từ cơ quan chức năng trong và ngoài nước khác, thời gian giải quyết có thể kéo dài hơn so với quy định chung.
</p>
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Mốc thời gian</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Thông báo bổ sung: trong 03 ngày làm việc</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Xử lý hồ sơ hợp lệ: không quá 08 giờ làm việc</span>
</div>
</div>
</div>
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Kênh xử lý</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<p>Hệ thống COVCCI dùng để khai báo, tiếp nhận và theo dõi hồ sơ trực tuyến.</p>
<p>Thương nhân cần chuẩn bị đầy đủ hồ sơ điện tử, chữ ký số và hồ sơ giấy theo quy định.</p>
</div>
</div>
</aside>
</div>
</section>
</div>
</div>
);
}
......@@ -9,14 +9,17 @@ import { Pagination } from "@components/base/pagination";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Spinner } from "@components/ui/spinner";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import {
buildDynamicPostHref,
buildVisibleNewsFilters,
useDynamicPostList,
getDynamicPostExcerpt,
resolveDynamicPostImage,
} from "@/app/(main)/[...slug]/templates/data";
import type { DynamicPostItem } from "@/app/(main)/[...slug]/templates/types";
mapPost,
} from "../templates/data";
import type { DynamicPostItem } from "../templates/types";
const formatPostDate = (value?: string | null) => {
if (!value) return "";
......@@ -95,15 +98,32 @@ function SearchContent() {
const [searchInput, setSearchInput] = useState(query);
const pageSize = 10;
const postsQuery = useDynamicPostList({
const filters = [
query ? `title@=${query}` : null,
"is_hidden==false",
"is_active==true",
"type==news",
]
.map((item) => item?.trim())
.filter(Boolean)
.join(",");
const { data: postsData, isLoading: postsLoading } = useGetApiV10Post({
page,
pageSize,
filters: buildVisibleNewsFilters([
query ? `title@=${query}` : null,
]),
staleTime: 60 * 1000,
sortField: "release_at",
sortOrder: "desc",
filters: filters || undefined,
});
const responseData = postsData?.responseData;
const count = Number(responseData?.count ?? 0);
const totalPages = pageSize > 0 ? Math.max(1, Math.ceil(count / pageSize)) : 1;
const currentPage = Math.min(page, totalPages);
const rows = ((responseData?.rows ?? []) as unknown as Parameters<typeof mapPost>[0][])
.map(mapPost)
.filter((item: DynamicPostItem) => item.id && item.title);
useEffect(() => {
const nextPage = pageFromUrl ? Number(pageFromUrl) : 1;
if (Number.isFinite(nextPage)) {
......@@ -124,10 +144,6 @@ function SearchContent() {
router.push(`/search?${params.toString()}`, { scroll: false });
};
const rows = postsQuery.data?.rows ?? [];
const totalPages = Number(postsQuery.data?.totalPages ?? 1);
const currentPage = Number(postsQuery.data?.page ?? page);
return (
<div className="min-h-screen bg-white">
<div className="container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10">
......@@ -145,7 +161,7 @@ function SearchContent() {
<div className="flex flex-col gap-10 xl:flex-row xl:gap-14">
<main className="order-2 min-w-0 xl:order-1 xl:flex-1">
{postsQuery.isLoading ? (
{postsLoading ? (
<div className="flex items-center justify-center py-16">
<Spinner className="size-8" />
<span className="ml-2 text-gray-600">Đang tìm kiếm...</span>
......@@ -232,16 +248,26 @@ function SearchContent() {
);
}
export default function Page() {
type SearchProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Search({ category, allCategories }: SearchProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
<Spinner className="size-8" />
</div>
}
>
<SearchContent />
</Suspense>
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
<Spinner className="size-8" />
</div>
}
>
<SearchContent />
</Suspense>
</>
);
}
'use client';
"use client";
import {
BriefcaseBusiness,
......@@ -12,7 +12,11 @@ import {
Store,
Ticket,
} from "lucide-react";
import type { DynamicPostItem } from "../types";
import ListCategory from "@/components/base/list-category";
import {
buildDynamicCategoryMenu,
} from "../templates/data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../templates/types";
const SERVICE_SUPPORT_ITEMS = [
{
......@@ -67,68 +71,77 @@ const SERVICE_SUPPORT_ITEMS = [
},
] as const;
type ServicePageProps = {
post: DynamicPostItem;
type ServiceProps = {
post: DynamicPostItem | null;
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function ServicePage({ post }: ServicePageProps) {
export default function Service({ post, category, allCategories }: ServiceProps) {
const introText =
post.summary?.trim() ||
post?.summary?.trim() ||
"VCCI-HCM cung cấp đa dạng các dịch vụ hỗ trợ doanh nghiệp phát triển và hội nhập kinh tế quốc tế.";
return (
<section className="grid gap-8 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] lg:items-start">
<div className="">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Dịch vụ cung cấp
</h1>
<div className="mt-3 h-[3px] w-14 rounded-full bg-[#f5a400]" />
<p className="mt-5 max-w-[520px] text-[18px] leading-9 text-[#66758d]">
{introText}
</p>
</div>
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
<div className="overflow-hidden rounded-4xl border border-[#d9e3f2] bg-white shadow-[0_22px_46px_rgba(28,52,120,0.12)]">
<div className="bg-[#19519c] px-5 py-5 text-white md:px-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-[26px] font-bold leading-tight">Danh mục hỗ trợ</h2>
<p className="mt-1 text-sm text-white/80">
Dành cho hội viên, doanh nghiệp và đối tác thương mại
</p>
</div>
<span className="inline-flex rounded-full bg-white/12 px-4 py-1.5 text-xs font-semibold tracking-[0.24em] text-white/92">
VCCI-HCM
</span>
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<section className="grid gap-8 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] lg:items-start">
<div className="">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Dịch vụ cung cấp
</h1>
<div className="mt-3 h-[3px] w-14 rounded-full bg-[#f5a400]" />
<p className="mt-5 max-w-[520px] text-[18px] leading-9 text-[#66758d]">
{introText}
</p>
</div>
</div>
<div className="grid md:grid-cols-2">
{SERVICE_SUPPORT_ITEMS.map((item, index) => {
const Icon = item.icon;
const isRightColumn = index % 2 === 1;
const isLastRow = index >= SERVICE_SUPPORT_ITEMS.length - 2;
return (
<div
key={item.key}
className={[
"flex min-h-28 items-start gap-4 px-5 py-5 md:px-6",
!isLastRow ? "border-b border-[#e7edf7]" : "",
isRightColumn ? "md:border-l md:border-[#e7edf7]" : "",
].join(" ")}
>
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[#fff8ea] text-[#5c6f8d]">
<Icon className="h-5 w-5" />
<div className="overflow-hidden rounded-4xl border border-[#d9e3f2] bg-white shadow-[0_22px_46px_rgba(28,52,120,0.12)]">
<div className="bg-[#19519c] px-5 py-5 text-white md:px-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-[26px] font-bold leading-tight">Danh mục hỗ trợ</h2>
<p className="mt-1 text-sm text-white/80">
Dành cho hội viên, doanh nghiệp và đối tác thương mại
</p>
</div>
<p className="pt-1 text-[18px] leading-8 text-[#5f6f86]">
{item.title}
</p>
<span className="inline-flex rounded-full bg-white/12 px-4 py-1.5 text-xs font-semibold tracking-[0.24em] text-white/92">
VCCI-HCM
</span>
</div>
);
})}
</div>
</div>
<div className="grid md:grid-cols-2">
{SERVICE_SUPPORT_ITEMS.map((item, index) => {
const Icon = item.icon;
const isRightColumn = index % 2 === 1;
const isLastRow = index >= SERVICE_SUPPORT_ITEMS.length - 2;
return (
<div
key={item.key}
className={[
"flex min-h-28 items-start gap-4 px-5 py-5 md:px-6",
!isLastRow ? "border-b border-[#e7edf7]" : "",
isRightColumn ? "md:border-l md:border-[#e7edf7]" : "",
].join(" ")}
>
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[#fff8ea] text-[#5c6f8d]">
<Icon className="h-5 w-5" />
</div>
<p className="pt-1 text-[18px] leading-8 text-[#5f6f86]">
{item.title}
</p>
</div>
);
})}
</div>
</div>
</section>
</div>
</section>
</div>
);
}
"use client";
import React from "react";
import Link from "next/link";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { useGetApiV10PageConfig } from "@/api/vcci-news/endpoints/page-config";
import { GetNewsPageConfigResponseType } from "@/api/vcci-news/types/news-page-config";
type SiteMapProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function SiteMap({ category, allCategories }: SiteMapProps) {
const { data: categoriesData, isLoading, isError } = useGetApiV10PageConfig<GetNewsPageConfigResponseType>();
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
if (isLoading) {
return (
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="min-h-screen bg-gray-50 py-12 flex items-center justify-center">
<div className="text-[#063e8e] text-xl font-semibold">Đang tải...</div>
</div>
</>
);
}
if (isError || !categoriesData?.responseData) {
return (
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="min-h-screen bg-gray-50 py-12 flex items-center justify-center">
<div className="text-red-600 text-xl font-semibold">Không thể tải dữ liệu</div>
</div>
</>
);
}
const sections = categoriesData.responseData.children || [];
return (
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="min-h-screen bg-gray-50 py-12">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold text-center mb-12 text-[#063e8e]">
SƠ ĐỒ TRANG WEB
</h1>
{/* Sitemap Structure */}
<div className="relative flex flex-col items-center">
{/* Homepage - Top Level */}
<div className="relative mb-20">
<Link
href="/"
className="block bg-[#063e8e] text-white px-8 py-4 rounded-lg font-semibold text-center hover:bg-[#0a4fb5] transition shadow-lg min-w-[200px]"
>
TRANG CHỦ
</Link>
{/* Vertical line from homepage down */}
<div className="absolute left-[99px] -translate-x-1/2 top-full h-20 w-0.5 bg-gray-600"></div>
</div>
{/* Main Sections - Second Level */}
<div className="relative w-full max-w-[1400px]">
{/* Horizontal line connecting all sections */}
<div className="absolute top-0 left-[6.3%] right-[6.3%] h-0.5 bg-gray-600 z-0"></div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-6 relative pt-4">
{sections.map((section, idx) => (
<div key={section.id} className="relative flex flex-col items-center">
{/* Vertical line from horizontal bar down to section */}
<div className="absolute -top-4 left-1/2 -translate-x-1/2 h-4 w-0.5 bg-gray-600 z-10"></div>
{/* Section Box */}
<div className="relative z-20">
<Link
href={section.static_link || "#"}
className="flex bg-[#063e8e] text-white px-4 py-3 rounded-md font-medium text-center hover:bg-[#0a4fb5] transition shadow-md w-full text-sm min-h-20 items-center justify-center"
>
<span className="leading-tight">{section.name.toUpperCase()}</span>
</Link>
{/* Vertical line from section down to children */}
{section.children && section.children.length > 0 && (
<div className="absolute left-1/2 -translate-x-1/2 top-full h-6 w-0.5 bg-gray-600 z-10"></div>
)}
</div>
{/* Children - Third Level */}
{section.children && section.children.length > 0 && (
<div className="mt-6 flex flex-col gap-3 w-full relative">
{/* Vertical spine connecting all children */}
<div
className="absolute left-1/2 -translate-x-1/2 w-0.5 bg-gray-600"
style={{
top: '-24px',
bottom: '0',
}}
></div>
{section.children.map((child, childIdx) => (
<div key={child.id} className="relative">
{/* Horizontal line from spine to child box */}
<div className="absolute right-1/2 top-1/2 -translate-y-1/2 w-1/2 h-0.5 bg-gray-600"></div>
<Link
href={child.static_link || "#"}
className="block bg-gray-400 text-white px-3 py-2.5 rounded text-xs font-medium text-center hover:bg-gray-500 transition shadow-sm leading-tight relative z-10"
>
{child.name.toUpperCase()}
</Link>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
<style jsx>{`
@media (max-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
`}</style>
</div>
</>
);
}
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Spinner } from "@/components/ui";
import { Pagination } from "@/components/base/pagination";
import { SafeImage } from "@/components/shared/safe-image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import EventsCalendar from "@/app/(main)/(home)/components/events-calendar";
import SidebarAdvertisements from "@/components/shared/sidebar-advertisements";
import ListCategory from "@/components/base/list-category";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import {
buildDynamicPostHref,
buildDynamicCategoryMenu,
resolveDynamicPostImage,
mapPost,
} from "../templates/data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../templates/types";
type ThuVienTaiLieuProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function ThuVienTaiLieu({ category, allCategories }: ThuVienTaiLieuProps) {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const searchParamsString = searchParams.toString();
const initialPage = Number(searchParams.get("page") ?? "1");
const [searchInput, setSearchInput] = useState("");
const [submitSearch, setSubmitSearch] = useState("");
const [page, setPage] = useState(initialPage);
const pageSize = 8;
const keyword = submitSearch.trim();
// Auto-search with debounce
useEffect(() => {
const timer = setTimeout(() => {
setSubmitSearch(searchInput);
setPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchInput]);
useEffect(() => {
const params = new URLSearchParams(searchParamsString);
if (page > 1) {
params.set("page", String(page));
} else {
params.delete("page");
}
const qs = params.toString();
const nextUrl = qs ? `${pathname}?${qs}` : pathname;
const currentUrl = searchParamsString ? `${pathname}?${searchParamsString}` : pathname;
if (nextUrl !== currentUrl) {
router.replace(nextUrl, { scroll: false });
}
}, [page, pathname, router, searchParamsString]);
const filters = [
category?.id ? `category.id==${category.id}` : null,
keyword ? `title@=${keyword}` : null,
"is_hidden==false",
"is_active==true",
"type==news",
]
.map((item) => item?.trim())
.filter(Boolean)
.join(",");
const { data: postsData, isLoading: postsLoading } = useGetApiV10Post({
page,
pageSize,
sortField: "release_at",
sortOrder: "desc",
filters: filters || undefined,
});
const responseData = postsData?.responseData;
const count = Number(responseData?.count ?? 0);
const totalPages = pageSize > 0 ? Math.max(1, Math.ceil(count / pageSize)) : 1;
const currentPage = Math.min(page, totalPages);
const paginatedPosts = ((responseData?.rows ?? []) as unknown as Parameters<typeof mapPost>[0][])
.map(mapPost)
.filter((item: DynamicPostItem) => item.id && item.title);
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
{postsLoading ? (
<div className="flex h-64 w-full items-center justify-center">
<Spinner />
</div>
) : (
<div className="container mx-auto px-4 py-4 lg:pb-6 sm:px-6 lg:px-10">
<div className="mb-8">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-4xl">
{category?.name ?? "Thư viện tài liệu"}
</h1>
<div className="mt-2 h-[3px] w-16 rounded-full bg-[#f5a400]" />
</div>
<div className="flex flex-col gap-10 xl:flex-row xl:gap-14">
<main className="order-2 min-w-0 xl:order-1 xl:flex-1">
{paginatedPosts.length ? (
<div className="grid grid-cols-2 gap-5 sm:grid-cols-3 xl:grid-cols-4 xl:gap-6">
{paginatedPosts.map((item) => {
return (
<Link
key={item.id}
href={buildDynamicPostHref(item.slug, item.id, category?.id)}
className="group block"
>
<div className="overflow-hidden bg-white shadow-[0_10px_24px_rgba(17,24,39,0.08)]">
<div className="relative aspect-3/4 overflow-hidden bg-white">
<SafeImage
src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title}
width={520}
height={693}
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
/>
</div>
</div>
<div className="px-1 pt-3 text-center">
<h2 className="line-clamp-2 text-[14px] leading-[1.45] text-[#1f2f57]">
{item.title}
</h2>
</div>
</Link>
);
})}
</div>
) : (
<div className="rounded-2xl border border-[#edf1f5] bg-white px-6 py-12 text-center text-gray-600">
Chưa có tài liệu trong danh mục.
</div>
)}
<div className="flex w-full justify-center pt-8">
<Pagination
pageCount={totalPages}
page={currentPage}
onChangePage={setPage}
onGoToPreviousPage={() => setPage(Math.max(1, currentPage - 1))}
onGoToNextPage={() => setPage(Math.min(totalPages, currentPage + 1))}
/>
</div>
</main>
<aside className="contents xl:order-2 xl:block xl:w-[320px] xl:space-y-5 xl:pt-0">
<form
className="order-1 rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)] xl:order-0"
onSubmit={(event) => {
event.preventDefault();
setPage(1);
setSubmitSearch(searchInput);
}}
>
<h2 className="text-lg font-bold text-[#111827]">Tìm kiếm</h2>
<Input
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="Tên bài viết ..."
className="mt-4 h-11 rounded-xl border-[#edf1f5] bg-[#f8fafc] text-sm placeholder:text-gray-700"
/>
<div className="mt-4 grid grid-cols-2 gap-3">
<Button
type="submit"
className="h-11 rounded-xl bg-[#14519f] text-white hover:bg-[#0f4386]"
>
Tìm kiếm
</Button>
<Button
type="button"
variant="outline"
className="h-11 rounded-xl border-[#edf1f5] bg-white text-[#4b5563]"
onClick={() => {
setSearchInput("");
setPage(1);
setSubmitSearch("");
}}
>
Bỏ tìm
</Button>
</div>
</form>
<EventsCalendar compact className="xl:w-full xl:min-w-0" />
<SidebarAdvertisements count={5} startIndex={0} />
</aside>
</div>
</div>
)}
</div>
);
}
......@@ -6,57 +6,38 @@ import { Play } from "lucide-react";
import { SafeImage } from "@/components/shared/safe-image";
import { Pagination } from "@/components/base/pagination";
import { Spinner } from "@/components/ui/spinner";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import { useGetApiV10Video } from "@/api/vcci-news/endpoints/video";
import type { Video } from "@/api/vcci-news/models/video";
import { getVideoThumbnail, normalizeVideoUrl } from "@/lib/utils/video";
const PAGE_SIZE = 10;
type ClientVideoItem = Video & {
thumbnail: string;
watchUrl: string;
};
function VideoPageContent() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const pageFromUrl = Number(searchParams.get("page") ?? "1");
const page = Number.isFinite(pageFromUrl) && pageFromUrl > 0 ? Math.floor(pageFromUrl) : 1;
const PAGE_SIZE = 10;
const videosQuery = useGetApiV10Video(
{
page,
pageSize: PAGE_SIZE,
sortField: "created_at",
sortOrder: "desc",
},
{
query: {
staleTime: 60 * 1000,
select: (response) => {
const pageData = response?.responseData ?? {};
const pageSize = pageData.pageSize ?? PAGE_SIZE;
const count = pageData.count ?? 0;
return {
rows: ((pageData.rows ?? []) as unknown as Video[]).map((item) => ({
...item,
thumbnail: getVideoThumbnail(item.url ?? ""),
watchUrl: normalizeVideoUrl(item.url ?? ""),
})),
count,
page: pageData.page ?? page,
pageSize,
totalPages: Math.max(1, Math.ceil(count / pageSize)),
};
},
},
},
);
const { data: videosData, isLoading: videosLoading, isError: videosError } = useGetApiV10Video({
page,
pageSize: PAGE_SIZE,
sortField: "created_at",
sortOrder: "desc",
});
const videos = videosQuery.data?.rows ?? [];
const totalPages = videosQuery.data?.totalPages ?? 1;
const currentPage = videosQuery.data?.page ?? page;
const pageData = videosData?.responseData;
const count = pageData?.count ?? 0;
const videoPageSize = pageData?.pageSize ?? PAGE_SIZE;
const videos = ((pageData?.rows ?? []) as unknown as Video[]).map((item) => ({
...item,
thumbnail: getVideoThumbnail(item.url ?? ""),
watchUrl: normalizeVideoUrl(item.url ?? ""),
}));
const totalPages = Math.max(1, Math.ceil(count / videoPageSize));
const currentPage = pageData?.page ?? page;
const updatePage = (nextPage: number) => {
const params = new URLSearchParams(searchParams.toString());
......@@ -81,7 +62,7 @@ function VideoPageContent() {
<div className="mt-2 h-[3px] w-16 rounded-full bg-[#f5a400]" />
</div>
{videosQuery.isLoading ? (
{videosLoading ? (
<div className="grid gap-6 md:grid-cols-2">
{Array.from({ length: 4 }).map((_, index) => (
<div
......@@ -90,7 +71,7 @@ function VideoPageContent() {
/>
))}
</div>
) : videosQuery.isError ? (
) : videosError ? (
<div className="rounded-2xl border border-[#edf1f5] bg-white px-6 py-12 text-center text-gray-600">
Không thể tải danh sách video.
</div>
......@@ -152,16 +133,26 @@ function VideoPageContent() {
);
}
export default function Page() {
type VideoProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function Video({ category, allCategories }: VideoProps) {
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
<Spinner className="size-8" />
</div>
}
>
<VideoPageContent />
</Suspense>
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
<Spinner className="size-8" />
</div>
}
>
<VideoPageContent />
</Suspense>
</>
);
}
'use client';
import ListCategory from "@/components/base/list-category";
import parse from "html-react-parser";
import { buildDynamicCategoryMenu } from "./data";
import StructuredPostContent from "./StructuredPostContent";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
import {
ABOUT_VCCI_HCM_SLUG,
AboutVcciHcmPage,
DefaultInformationPage,
LEGAL_TRADE_PAGE_SLUGS,
LegalTradePages,
MARKET_PROFILE_PAGE_SLUG,
MarketProfilePage,
MEMBER_REGISTRATION_PAGE_SLUG,
MEMBER_BENEFITS_PAGE_SLUG,
MemberRegistrationPage,
MemberBenefitsPage,
SERVICE_PAGE_SLUG,
ServicePage,
} from "./information-pages";
type InformationPageProps = {
post: DynamicPostItem;
......@@ -25,120 +12,38 @@ type InformationPageProps = {
allCategories: DynamicCategoryRouteItem[];
};
const LEGAL_TRADE_CATEGORY_ID = "69b4c7e7-28ea-41f2-97f4-988fe702a8a3";
const LEGAL_TRADE_CHILD_SLUGS = new Set([
"xuat-xu-hang-hoa-co",
"thu-tuc-cap-co",
"bieu-mau-co-va-cach-khai",
"phi-va-le-phi-cap-co",
"diem-cap-va-thoi-gian-cap-co",
"diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm",
"thong-tin-lien-he-co",
]);
function resolveInformationVariant(post: DynamicPostItem, category: DynamicCategoryRouteItem) {
if (
category.slug === ABOUT_VCCI_HCM_SLUG ||
post.slug === ABOUT_VCCI_HCM_SLUG ||
post.categories.some((item) => item.url === "/gioi-thieu/ve-vcci-hcm")
) {
return "about-vcci-hcm" as const;
}
if (
category.slug === SERVICE_PAGE_SLUG ||
post.slug === SERVICE_PAGE_SLUG ||
post.categories.some((item) => item.url === "/gioi-thieu/dich-vu-cung-cap")
) {
return "service" as const;
}
if (
category.slug === MEMBER_BENEFITS_PAGE_SLUG ||
post.slug === MEMBER_BENEFITS_PAGE_SLUG ||
post.categories.some((item) => item.url === "/hoi-vien/loi-ich-hoi-vien-vcci")
) {
return "member-benefits" as const;
}
if (
category.slug === MEMBER_REGISTRATION_PAGE_SLUG ||
post.slug === MEMBER_REGISTRATION_PAGE_SLUG ||
post.categories.some((item) => item.url === "/hoi-vien/dang-ky-hoi-vien")
) {
return "member-registration" as const;
}
if (
category.slug === MARKET_PROFILE_PAGE_SLUG ||
post.slug === MARKET_PROFILE_PAGE_SLUG ||
post.categories.some((item) => item.url === "/xuc-tien-thuong-mai/ho-so-thi-truong")
) {
return "market-profile" as const;
}
if (
LEGAL_TRADE_PAGE_SLUGS.has(category.slug) ||
category.parent_id === LEGAL_TRADE_CATEGORY_ID ||
LEGAL_TRADE_CHILD_SLUGS.has(category.slug) ||
LEGAL_TRADE_PAGE_SLUGS.has(post.slug) ||
LEGAL_TRADE_CHILD_SLUGS.has(post.slug) ||
post.categories.some((item) => item.id === LEGAL_TRADE_CATEGORY_ID) ||
post.categories.some(
(item) =>
item.url === "/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai" ||
item.url === "/phap-che-va-cttm" ||
item.url.startsWith("/xuat-xu-hang-hoa/"),
)
) {
return "legal-trade" as const;
}
return "default" as const;
}
function hasRenderablePostData(post: DynamicPostItem) {
if (post.content.trim()) return true;
const sections = post.content_structure?.post_content ?? [];
return sections.some((section) => section.content.trim() || section.images.length > 0);
}
export default function InformationPage({
post,
category,
allCategories,
}: InformationPageProps) {
const categoryMenu = buildDynamicCategoryMenu(category, allCategories);
const variant = resolveInformationVariant(post, category);
const useSpecialUi =
variant === "about-vcci-hcm" ||
(variant !== "default" && !hasRenderablePostData(post));
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 sm:px-6 lg:px-10 lg:pb-6">
<main className="w-full">
{useSpecialUi ? (
variant === "about-vcci-hcm" ? (
<AboutVcciHcmPage post={post} />
) : variant === "service" ? (
<ServicePage post={post} />
) : variant === "member-benefits" ? (
<MemberBenefitsPage />
) : variant === "member-registration" ? (
<MemberRegistrationPage post={post} />
) : variant === "market-profile" ? (
<MarketProfilePage post={post} />
) : variant === "legal-trade" ? (
<LegalTradePages post={post} category={category} />
) : (
<DefaultInformationPage post={post} />
)
) : (
<DefaultInformationPage post={post} />
)}
<section className="block">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
{post.title}
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
{post.summary ? (
<p className="mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8">
{parse(post.summary)}
</p>
) : null}
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="page-detail-content prose tiptap max-w-none overflow-hidden">
<StructuredPostContent post={post} />
</div>
</div>
</div>
</section>
<div className="page-detail-styles">
<style jsx global>{`
......
......@@ -110,17 +110,17 @@ const EventInfoCard = ({ post }: { post: DynamicPostItem }) => {
);
};
type ArticleDetailPageProps = {
type NewsDetailPageProps = {
post: DynamicPostItem;
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function ArticleDetailPage({
export default function NewsDetailPage({
post,
category,
allCategories,
}: ArticleDetailPageProps) {
}: NewsDetailPageProps) {
const publishedDate = dayjs(
post.release_at ?? post.published_at ?? post.created_at,
).format("DD/MM/YYYY");
......
......@@ -11,18 +11,17 @@ import { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category";
import EventsCalendar from "@/app/(main)/(home)/components/events-calendar";
import SidebarAdvertisements from "@/components/shared/sidebar-advertisements";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import {
buildDynamicPostHref,
buildDynamicCategoryMenu,
buildVisibleNewsFilters,
useDynamicPostList,
findDisplayCategoryForPost,
getDynamicPostExcerpt,
resolveDynamicPostImage,
} from "./data";
import type { DynamicCategoryRouteItem } from "./types";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
type ArticlePageProps = {
type NewsPageProps = {
category: DynamicCategoryRouteItem;
allCategories: DynamicCategoryRouteItem[];
};
......@@ -51,7 +50,7 @@ const getTagClassName = (index: number) => {
return classes[index % classes.length];
};
export default function ArticlePage({ category, allCategories }: ArticlePageProps) {
export default function NewsPage({ category, allCategories }: NewsPageProps) {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
......@@ -90,19 +89,22 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
}
}, [page, pathname, router, searchParamsString]);
const postsQuery = useDynamicPostList({
const filters = `category.id==${category.id}${keyword ? `,title@=${keyword}` : ""},is_hidden==false,is_active==true,type==news`;
const { data: postsData, isLoading: postsLoading } = useGetApiV10Post({
page,
pageSize,
filters: buildVisibleNewsFilters([
`category.id==${category.id}`,
keyword ? `title@=${keyword}` : null,
]),
staleTime: 60 * 1000,
sortField: "release_at",
sortOrder: "desc",
filters: filters || undefined,
});
const totalPages = postsQuery.data?.totalPages ?? 1;
const responseData = postsData?.responseData;
const count = Number(responseData?.count ?? 0);
const totalPages = pageSize > 0 ? Math.max(1, Math.ceil(count / pageSize)) : 1;
const currentPage = Math.min(page, totalPages);
const paginatedPosts = postsQuery.data?.rows ?? [];
const paginatedPosts = ((responseData?.rows ?? []) as unknown as DynamicPostItem[])
.filter((item) => item.id && item.title);
const categoryIndexMap = useMemo(() => {
const entries = allCategories.map((item, index) => [item.id, index] as const);
......@@ -116,7 +118,7 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
return (
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
{postsQuery.isLoading ? (
{postsLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
......
"use client";
import { useState } from "react";
import parse from "html-react-parser";
import { ImageLightbox } from "@/components/shared/image-lightbox";
import { SafeImage } from "@/components/shared/safe-image";
import { getDynamicPostBodyHtml } from "./data";
import type { DynamicPostContentSection, DynamicPostItem } from "./types";
......@@ -18,36 +21,60 @@ function getGridClassName(columns: number) {
function StructuredImageSection({ section }: { section: DynamicPostContentSection }) {
const images = section.images.filter((item) => item.image?.url);
const [activeImage, setActiveImage] = useState<{
src: string;
alt: string;
caption?: string;
} | null>(null);
if (!images.length) return null;
return (
<div className={`not-prose my-6 grid gap-4 ${getGridClassName(section.image_columns)}`}>
{images.map((item) => {
const image = item.image;
if (!image?.url) return null;
return (
<figure
key={`${section.id}-${image.id || image.url}-${item.position}`}
className="overflow-hidden rounded-[18px] bg-white"
>
<SafeImage
src={image.url}
alt={image.alt || image.name || "Hình ảnh bài viết"}
width={1200}
height={800}
className="h-auto w-full object-contain"
/>
{item.caption ? (
<figcaption className="mt-2 text-center text-sm text-gray-600">
{item.caption}
</figcaption>
) : null}
</figure>
);
})}
</div>
<>
<div className={`not-prose my-6 grid gap-4 ${getGridClassName(section.image_columns)}`}>
{images.map((item) => {
const image = item.image;
if (!image?.url) return null;
return (
<figure
key={`${section.id}-${image.id || image.url}-${item.position}`}
className="group cursor-zoom-in overflow-hidden rounded-[18px] bg-white"
onClick={() =>
setActiveImage({
src: image.url,
alt: image.alt || image.name || "Hình ảnh bài viết",
caption: item.caption ?? undefined,
})
}
>
<SafeImage
src={image.url}
alt={image.alt || image.name || "Hình ảnh bài viết"}
width={1200}
height={800}
className="h-auto w-full object-contain transition-transform duration-300 group-hover:scale-[1.02]"
/>
{item.caption ? (
<figcaption className="mt-2 text-center text-sm text-gray-600">
{item.caption}
</figcaption>
) : null}
</figure>
);
})}
</div>
<ImageLightbox
src={activeImage?.src ?? ""}
alt={activeImage?.alt}
caption={activeImage?.caption}
open={Boolean(activeImage)}
onOpenChange={(open) => {
if (!open) setActiveImage(null);
}}
/>
</>
);
}
......
import type { Category } from "@/api/vcci-news/models/category";
import { getApiV10Category, useGetApiV10Category } from "@/api/vcci-news/endpoints/category";
import { getApiV10Post, getApiV10PostId, useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import { getApiV10Post, getApiV10PostId } from "@/api/vcci-news/endpoints/post";
import Links from "@/links";
import { getCategoryFallbackResponse } from "@/mockdata/categories";
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import type {
DynamicCategoryMenuItem,
DynamicCategoryRouteItem,
......@@ -14,12 +10,6 @@ import type {
DynamicPostUser,
} from "./types";
type CategoryListResponse = {
responseData?: {
rows?: Category[];
};
};
type RawPostCategory = {
id?: string | null;
name?: string | null;
......@@ -280,36 +270,6 @@ export const buildVisibleNewsFilters = (
"type==news",
]);
export async function fetchDynamicCategories(): Promise<DynamicCategoryRouteItem[]> {
const response = await getApiV10Category({
page: 1,
pageSize: 200,
sortField: "sort_order",
sortOrder: "asc",
}).catch(() => getCategoryFallbackResponse());
const rows = (response.responseData?.rows ?? []) as unknown as Category[];
return sortCategories(
rows
.map((item) => {
const type = normalizeCategoryType(item.type);
if (!item.id || !item.name || !type) return null;
return {
id: item.id,
name: item.name,
slug: item.slug ?? "",
url: normalizePath(item.url),
type,
parent_id: item.parent_id ?? null,
sort_order: item.sort_order ?? null,
} satisfies DynamicCategoryRouteItem;
})
.filter((item): item is DynamicCategoryRouteItem => Boolean(item)),
);
}
export async function fetchDynamicPostList(params: {
filters?: string;
page?: number;
......@@ -377,43 +337,6 @@ export async function fetchDynamicPostBySlug(path: string) {
return result.rows[0] ?? null;
}
export async function fetchDynamicSinglePagePost(categoryId: string) {
const result = await fetchDynamicPostList({
page: 1,
pageSize: 1,
filters: buildPostFilters([
`category.id==${categoryId}`,
"is_hidden==false",
"is_active==true",
"type==page",
]),
});
return result.rows[0] ?? null;
}
export function findDynamicCategoryByPath(
categories: DynamicCategoryRouteItem[],
path: string,
) {
const normalizedPath = normalizePath(path);
return categories.find((item) => normalizePath(item.url) === normalizedPath) ?? null;
}
export function findMenuCategoryForPost(
post: DynamicPostItem | null,
categories: DynamicCategoryRouteItem[],
) {
if (!post) return null;
for (const category of post.categories) {
const matched = categories.find((item) => item.id === category.id);
if (matched) return matched;
}
return null;
}
export function findDisplayCategoryForPost(
post: DynamicPostItem | null,
activeCategory: DynamicCategoryRouteItem | null,
......@@ -476,13 +399,6 @@ export function buildDynamicCategoryMenu(
}));
}
export function findFirstChildCategory(
category: DynamicCategoryRouteItem,
categories: DynamicCategoryRouteItem[],
) {
return sortCategories(categories.filter((item) => item.parent_id === category.id))[0] ?? null;
}
export function resolveDynamicPostImage(thumbnail?: DynamicPostThumbnail) {
const value = thumbnail?.path ?? thumbnail?.original ?? thumbnail?.url ?? "";
......@@ -625,116 +541,3 @@ export function isDynamicPostVisible(post: DynamicPostItem) {
}
export { buildPostFilters, normalizePath };
export type UseDynamicPostListOptions = {
page?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
filters?: string;
enabled?: boolean;
staleTime?: number;
};
export function useDynamicPostList(options: UseDynamicPostListOptions) {
const page = options.page ?? 1;
const pageSize = options.pageSize ?? 5;
return useGetApiV10Post(
{
page,
pageSize,
sortField: options.sortField ?? "release_at",
sortOrder: (options.sortOrder ?? "desc") as "asc" | "desc",
filters: options.filters?.trim() || undefined,
},
{
query: {
enabled: options.enabled !== false,
staleTime: options.staleTime ?? 60 * 1000,
select: (response): DynamicPostListResult => {
const data = response?.responseData;
const count = Number(data?.count ?? 0);
return {
count,
page,
pageSize,
totalPages: pageSize > 0 ? Math.max(1, Math.ceil(count / pageSize)) : 1,
rows: ((data?.rows ?? []) as unknown as RawPostItem[])
.map(mapPost)
.filter((item) => item.id && item.title),
};
},
},
},
);
}
export type UseDynamicCategoriesOptions = {
enabled?: boolean;
staleTime?: number;
};
export function useDynamicCategories(options: UseDynamicCategoriesOptions = {}) {
return useGetApiV10Category(
{
page: 1,
pageSize: 200,
sortField: "sort_order",
sortOrder: "asc",
},
{
query: {
enabled: options.enabled !== false,
staleTime: options.staleTime ?? 5 * 60 * 1000,
select: (response): DynamicCategoryRouteItem[] => {
const rows = (response?.responseData?.rows ?? []) as unknown as Category[];
return sortCategories(
rows
.map((item) => {
const type = normalizeCategoryType(item.type);
if (!item.id || !item.name || !type) return null;
return {
id: item.id,
name: item.name,
slug: item.slug ?? "",
url: normalizePath(item.url),
type,
sort_order: item.sort_order ?? null,
parent_id: item.parent_id ?? null,
} as DynamicCategoryRouteItem;
})
.filter((item): item is DynamicCategoryRouteItem => item !== null),
);
},
},
},
);
}
export function useDynamicPostDetail(postId: string, routePath: string, options: {
enabled?: boolean;
staleTime?: number;
} = {}) {
return useQuery({
queryKey: ["dynamic-post-detail", postId || routePath],
queryFn: () =>
postId
? fetchDynamicPostById(postId)
: fetchDynamicPostBySlug(routePath),
enabled: options.enabled !== false,
staleTime: options.staleTime ?? 60 * 1000,
});
}
export function useDynamicSinglePagePost(categoryId: string | undefined, options: {
enabled?: boolean;
staleTime?: number;
} = {}) {
return useQuery({
queryKey: ["dynamic-single-page-post", categoryId],
queryFn: () => fetchDynamicSinglePagePost(categoryId!),
enabled: (options.enabled !== false) && Boolean(categoryId),
staleTime: options.staleTime ?? 60 * 1000,
});
}
'use client';
import StructuredPostContent from "../StructuredPostContent";
import type { DynamicPostItem } from "../types";
import parse from "html-react-parser";
type DefaultInformationPageProps = {
post: DynamicPostItem;
};
export default function DefaultInformationPage({
post,
}: DefaultInformationPageProps) {
return (
<section className="block">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
{post.title}
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
{post.summary ? (
<p className="mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8">
{parse(post.summary)}
</p>
) : null}
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="page-detail-content prose tiptap max-w-none overflow-hidden">
<StructuredPostContent post={post} />
</div>
</div>
</div>
</section>
);
}
'use client';
import links from "@/links";
import type { DynamicPostItem } from "../types";
type MemberRegistrationPageProps = {
post: DynamicPostItem;
};
const MEMBERSHIP_REQUIREMENTS = [
"Đơ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)",
"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)",
];
const MEMBERSHIP_FEES = [
"Doanh số dưới 10 tỉ đồng đóng 3 triệu đồng/năm",
"Doanh số từ 10 - 50 tỉ đồng đóng 7 triệu đồng/năm",
"Doanh số trên 50 tỉ đồng đóng 15 triệu đồng/năm",
];
const ATTACHED_FORMS = [
{
label: "Đơn đăng ký tham gia nhập hội viên VCCI (Mẫu Doanh nghiệp)",
href: "/Don-dang-ky-tham-gia-nhap-hoi-vien-VCCI_Mau-Doanh-nghiep-1.docx",
download: true,
},
{
label: "Đơn đăng ký tham gia nhập hội viên VCCI (Mẫu Hiệp hội)",
href: "/Don-dang-ky-tham-gia-nhap-hoi-vien-VCCI_Mau-Hiep-hoi.docx",
download: true,
},
{
label: "Hướng dẫn hồ sơ đăng ký Hội viên VCCI",
href: `${links.externalApiOrigin}/dang-ky`,
download: false,
},
] as const;
export default function MemberRegistrationPage({ post }: MemberRegistrationPageProps) {
return (
<section className="">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Đăng ký hội viên
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 space-y-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
{post.content?.trim() ||
"Đ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 text-[18px] leading-9 text-[#1f2a44]">
Để 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="space-y-2 pl-6 text-[18px] leading-9 text-[#1f2a44]">
{MEMBERSHIP_REQUIREMENTS.map((item) => (
<li key={item} className="list-disc">
<strong>{item}</strong>
</li>
))}
</ul>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
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. Theo quyết định của Ban chấp hành VCCI, lệ phí hiện hành được tính như sau:
</p>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
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="space-y-2 pl-6 text-[18px] leading-9 text-[#1f2a44]">
{MEMBERSHIP_FEES.map((item) => (
<li key={item} className="list-disc">
{item}
</li>
))}
</ul>
<p className="text-justify text-[18px] leading-9 text-[#1f2a44]">
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>
<p className="font-semibold text-[#2450b5]">Để biết thêm thông tin chi tiết, vui lòng liên hệ:</p>
<div className="mt-4 space-y-1 text-[18px] leading-9 text-[#1f2a44]">
<p className="font-semibold">Phòng Hội viên Đào tạo và Truyền thông:</p>
<p>C. Thúy – ĐD: 0903 909 756</p>
<p>Email: luuthanhthuy72@yahoo.com; hoivien@vcci-hcm.org.vn</p>
<p>Điện thoại: 028. 3932 0611 – Fax: 028. 3932 5472</p>
<p>Địa chỉ: P. 306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, Phường Xuân Hòa, TP. Hồ Chí Minh</p>
</div>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Biểu mẫu đính kèm:</p>
<ul className="mt-3 space-y-2 pl-6 text-[#2450b5]">
{ATTACHED_FORMS.map((item) => (
<li key={item.label} className="list-disc italic">
{item.download ? (
<a href={item.href} download className="hover:text-[#173f9f]">
{item.label}
</a>
) : (
<a href={item.href} target="_blank" rel="noreferrer" className="hover:text-[#173f9f]">
{item.label}
</a>
)}
</li>
))}
</ul>
</div>
<div className="flex justify-center pt-4">
<a
href={`https://vccihcm.vn/dang-ky`}
target="_blank"
rel="noreferrer"
className="inline-flex min-w-[220px] items-center justify-center rounded-[4px] bg-[#2450b5] px-6 py-4 text-[18px] font-semibold text-white transition-colors hover:bg-[#173f9f]"
>
Đăng ký Hội viên
</a>
</div>
</div>
</div>
</section>
);
}
export const ABOUT_VCCI_HCM_SLUG = "ve-vcci-hcm";
export const SERVICE_PAGE_SLUG = "dich-vu-cung-cap";
export const MEMBER_BENEFITS_PAGE_SLUG = "loi-ich-hoi-vien-vcci";
export const MEMBER_REGISTRATION_PAGE_SLUG = "dang-ky-hoi-vien";
export const MARKET_PROFILE_PAGE_SLUG = "ho-so-thi-truong";
export const LEGAL_TRADE_PAGE_SLUGS = new Set([
"phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai",
"phap-che-va-cttm",
]);
export { default as AboutVcciHcmPage } from "./AboutVcciHcmPage";
export { default as DefaultInformationPage } from "./DefaultInformationPage";
export { default as LegalTradePages } from "./legal-trade-pages";
export { default as MarketProfilePage } from "./MarketProfilePage";
export { default as MemberBenefitsPage } from "./MemberBenefitsPage";
export { default as MemberRegistrationPage } from "./MemberRegistrationPage";
export { default as ServicePage } from "./ServicePage";
export * from "./constants";
'use client';
import { Mail, MapPin, Phone, UserRound } from "lucide-react";
import type { LegalTradePageProps } from "./types";
export default function ContactPage(_: LegalTradePageProps) {
return (
<section className="py-2">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Thông tin liên hệ
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 space-y-5">
<article className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">
Phòng Pháp chế và xác nhận Chứng từ thương mại
</h2>
<div className="mt-5 space-y-4 text-[16px] leading-8 text-[#5f6f86]">
<p>Liên đoàn Thương mại và Công nghiệp Việt Nam – Chi nhánh khu vực Thành phố Hồ Chí Minh (VCCI-HCM)</p>
<div className="flex items-start gap-3">
<MapPin className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>Phòng 103, Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, P. Xuân Hòa, TP. HCM</span>
</div>
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>028-3932 6498</span>
</div>
<div className="flex items-start gap-3">
<Mail className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>co@vcci-hcm.org.vn</span>
</div>
</div>
</article>
<article className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">
Xử lý vướng mắc, phản ánh, góp ý trong quá trình làm thủ tục cấp GCN và xác nhận CTTM
</h2>
<div className="mt-5 space-y-5 text-[16px] leading-8 text-[#5f6f86]">
<div>
<p className="font-semibold text-[#1f2a44]">Điểm cấp số 1</p>
<p>Điện thoại: 028-3932 6498</p>
<p>Email: co@vcci-hcm.org.vn</p>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Điểm cấp số 2</p>
<p>Phó Trưởng phòng: Nguyễn Văn Đức</p>
<p>Mobile: 090 949 7155</p>
<p>Email: nvduc1980@gmail.com</p>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Điểm cấp số 3</p>
<p>Trưởng phòng (Cơ sở 2): Bà Ma Thị Hương</p>
<p>Mobile: 039 512 2922</p>
<p>Email: huongmtvccivt@gmail.com</p>
</div>
</div>
</article>
<article className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">
Hướng dẫn hồ sơ và tiếp nhận phản ánh
</h2>
<div className="mt-5 space-y-5 text-[16px] leading-8 text-[#5f6f86]">
<div>
<p className="font-semibold text-[#1f2a44]">Hướng dẫn khai hồ sơ thương nhân, chữ ký số và IT</p>
<p>Điểm cấp số 1, điện thoại: 028-3932 6498</p>
<p>Điểm cấp số 2, điện thoại: 0274-380 0048</p>
<p>Điểm cấp số 3, điện thoại: 025-4385 2710</p>
</div>
<div>
<p className="font-semibold text-[#1f2a44]">Tiếp thu, giải quyết phản ánh, khiếu nại, góp ý</p>
<p>Trưởng phòng (Trụ sở chính): Ông Vũ Xuân Hưng</p>
<p>Điện thoại: 028-3932 6929 hoặc Mobile: 0909 170 171 (Đường dây nóng)</p>
<p>Email: vuxuanhung@vcci-hcm.org.vn</p>
</div>
</div>
</article>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Đầu mối hỗ trợ</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<Mail className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>co@vcci-hcm.org.vn</span>
</div>
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>Đường dây nóng: 0909 170 171</span>
</div>
<div className="flex items-start gap-3">
<UserRound className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>Ông Vũ Xuân Hưng</span>
</div>
</div>
</div>
</aside>
</div>
</section>
);
}
'use client';
import { CircleDollarSign, FileStack, Layers3 } from "lucide-react";
import type { LegalTradePageProps } from "./types";
const FEE_ITEMS = [
{
title: "Một bộ GCN, CTTM (4 bản)",
value: "100.000đ/bộ",
icon: FileStack,
},
{
title: "Bản làm thêm tính từ bản thứ 5 trở lên",
value: "10.000đ/bản",
icon: Layers3,
},
{
title: "Phôi Giấy chứng nhận",
value: "20.000đ/tờ",
icon: CircleDollarSign,
},
] as const;
export default function FeesPage(_: LegalTradePageProps) {
return (
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Phí cấp GCN và xác nhận CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-3xl border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-[#2450b5]">Biểu phí hiện hành</p>
<p className="mt-3 text-[16px] leading-8 text-[#5f6f86]">
Mức phí áp dụng cho việc cấp Giấy chứng nhận và xác nhận Chứng từ thương mại được tính theo từng loại hồ sơ và số lượng bản phát hành.
</p>
</div>
<div className="mt-8 grid gap-4 md:grid-cols-3">
{FEE_ITEMS.map((item, index) => {
const Icon = item.icon;
return (
<article
key={item.title}
className={[
"rounded-[26px] border px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]",
index === 0 ? "border-[#dbe7ff] bg-[#f8fbff]" : "border-[#edf1f6] bg-white",
].join(" ")}
>
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<Icon className="h-5 w-5" />
</div>
<h2 className="mt-4 text-[20px] font-bold leading-tight text-[#1f2a44]">{item.title}</h2>
<p className="mt-4 text-[30px] font-bold leading-none text-[#2450b5]">{item.value}</p>
</article>
);
})}
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Tóm tắt chi phí</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>01 bộ tiêu chuẩn gồm 4 bản</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Có phụ phí cho bản làm thêm từ bản thứ 5</span>
</div>
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Phôi Giấy chứng nhận được tính riêng theo từng tờ</span>
</div>
</div>
</div>
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Lưu ý khi chuẩn bị</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<p>Kiểm tra trước số lượng bản cần cấp để chuẩn bị đúng chi phí thực hiện.</p>
<p>Chuẩn bị lệ phí đầy đủ sẽ giúp quá trình tiếp nhận và xử lý hồ sơ diễn ra nhanh hơn.</p>
</div>
</div>
</aside>
</div>
</section>
);
}
'use client';
import { Download, FileBadge2, FileText } from "lucide-react";
import type { LegalTradePageProps } from "./types";
export default function FormsPage(_: LegalTradePageProps) {
return (
<section className="py-2">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Biểu mẫu GCN và nội dung khai báo GCN, CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-[26px] border border-[#edf1f6] bg-white px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]">
<div className="flex items-start gap-4">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<FileText className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<h2 className="text-[22px] font-bold leading-tight text-[#1f2a44]">
bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm.docx
</h2>
<p className="mt-3 text-[16px] leading-8 text-[#5f6f86]">
Nhấn tải xuống để xem toàn bộ biểu mẫu và nội dung khai báo.
</p>
<a
href="/bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm.docx"
download
className="mt-5 inline-flex items-center gap-2 rounded-[4px] bg-[#2450b5] px-5 py-3 text-[15px] font-semibold text-white transition-colors hover:bg-[#173f9f]"
>
<Download className="h-4 w-4" />
Tải biểu mẫu
</a>
</div>
</div>
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Lưu ý</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
<div className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>Người dùng có thể tải về để xem biểu mẫu đầy đủ trên máy của mình.</span>
</div>
</div>
</div>
</aside>
</div>
</section>
);
}
'use client';
import {
Building2,
CircleDollarSign,
ClipboardList,
FileBadge2,
FileCheck2,
Mail,
MapPin,
Phone,
Scale,
ScrollText,
} from "lucide-react";
import type { LegalTradeTemplate } from "./types";
type LegalTradeLayoutProps = {
template: LegalTradeTemplate;
};
const ICONS = [Scale, FileCheck2, ScrollText, CircleDollarSign, Building2, ClipboardList];
export default function LegalTradeLayout({ template }: LegalTradeLayoutProps) {
return (
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_340px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
{template.pageTitle}
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
{template.intro ? (
<p className="mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8">
{template.intro}
</p>
) : null}
<div className="mt-7 space-y-5">
{template.sections.map((section, index) => {
const Icon = ICONS[index % ICONS.length];
return (
<article
key={section.title}
className="rounded-3xl border border-[#edf1f6] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8"
>
<div className="flex items-start gap-4">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-[#2450b5]">
<Icon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<h2 className="text-[24px] font-bold leading-tight text-[#1f2a44]">{section.title}</h2>
{section.description ? (
<p className="mt-4 text-[16px] leading-8 text-[#5f6f86]">{section.description}</p>
) : null}
{section.bullets?.length ? (
<ul className="mt-4 space-y-3 text-[16px] leading-8 text-[#5f6f86]">
{section.bullets.map((item) => (
<li key={item} className="flex items-start gap-3">
<span className="mt-3 h-2 w-2 shrink-0 rounded-full bg-[#f5a400]" />
<span>{item}</span>
</li>
))}
</ul>
) : null}
{section.numbered?.length ? (
<ol className="mt-4 space-y-4 text-[16px] leading-8 text-[#5f6f86]">
{section.numbered.map((item, itemIndex) => (
<li key={item} className="flex items-start gap-4">
<span className="mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#2450b5] text-sm font-bold text-white">
{itemIndex + 1}
</span>
<span>{item}</span>
</li>
))}
</ol>
) : null}
</div>
</div>
</article>
);
})}
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
{template.sideCard ? (
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">{template.sideCard.title}</h2>
<div className="mt-5 space-y-4">
{template.sideCard.items.map((item) => (
<div key={item} className="flex items-start gap-3 text-[16px] leading-7 text-[#5f6f86]">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>{item}</span>
</div>
))}
</div>
</div>
) : null}
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Liên hệ hỗ trợ</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#f5c21b]" />
<span>028-3932 6498</span>
</div>
<div className="flex items-start gap-3">
<Mail className="mt-1 h-4 w-4 shrink-0 text-[#f5c21b]" />
<span>co@vcci-hcm.org.vn</span>
</div>
<div className="flex items-start gap-3">
<MapPin className="mt-1 h-4 w-4 shrink-0 text-[#f5c21b]" />
<span>Phòng 103, Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, P. Xuân Hòa, TP. HCM</span>
</div>
<div className="flex items-start gap-3">
<FileBadge2 className="mt-1 h-4 w-4 shrink-0 text-[#f5c21b]" />
<span>Hệ thống trực tuyến: covcci.com.vn</span>
</div>
</div>
</div>
</aside>
</div>
</section>
);
}
'use client';
import { Building2, Clock3, MapPin, Phone } from "lucide-react";
import type { LegalTradePageProps } from "./types";
const LOCATIONS = [
{
title: "Điểm cấp số 1",
address:
"Phòng 103, Lầu 1, Tòa nhà VCCI HCM, 171 Võ Thị Sáu, Phường Xuân Hòa, Thành phố Hồ Chí Minh",
phone: "028-3932 6498",
},
{
title: "Điểm cấp số 2",
address:
"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, Phường Bình Hòa, Thành phố Hồ Chí Minh",
phone: "0274-380 0048",
},
{
title: "Điểm cấp số 3",
address: "155 Nguyễn Thái Học, Phường Tam Thắng, Thành phố Hồ Chí Minh",
phone: "025-4385 2710",
},
] as const;
export default function LocationsPage(_: LegalTradePageProps) {
return (
<section className="">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_320px] lg:items-start">
<div className="min-w-0">
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
Điểm cấp và cấp GCN và xác nhận CTTM
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<div className="mt-7 rounded-3xl bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<div className="rounded-3xl border border-[#e5edf8] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Building2 className="h-5 w-5" />
<p className="text-sm font-semibold uppercase tracking-[0.18em]">
1. Các điểm cấp GCN và xác nhận CTTM thuộc VCCI-HCM
</p>
</div>
</div>
<div className="mt-8 grid gap-4">
{LOCATIONS.map((item, index) => (
<article
key={item.title}
className={[
"rounded-[26px] border px-5 py-5 shadow-[0_14px_34px_rgba(17,24,39,0.06)]",
index === 0 ? "border-[#dbe7ff] bg-[#f8fbff]" : "border-[#edf1f6] bg-white",
].join(" ")}
>
<div className="flex items-start gap-4">
<span className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#eff4ff] text-lg font-bold text-[#2450b5]">
{index + 1}
</span>
<div className="min-w-0 flex-1">
<h2 className="text-[22px] font-bold leading-tight text-[#1f2a44]">{item.title}</h2>
<div className="mt-4 space-y-3 text-[16px] leading-8 text-[#5f6f86]">
<div className="flex items-start gap-3">
<MapPin className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>{item.address}</span>
</div>
<div className="flex items-start gap-3">
<Phone className="mt-1 h-4 w-4 shrink-0 text-[#2450b5]" />
<span>{item.phone}</span>
</div>
</div>
</div>
</div>
</article>
))}
</div>
<div className="mt-8 rounded-3xl border border-[#dbe7ff] bg-[#f8fbff] px-5 py-5">
<div className="flex items-center gap-3 text-[#2450b5]">
<Clock3 className="h-5 w-5" />
<p className="text-sm font-semibold uppercase tracking-[0.18em]">2. Giờ tiếp nhận hồ sơ</p>
</div>
<div className="mt-4 space-y-2 text-[16px] leading-8 text-[#5f6f86]">
<p>– Từ thứ Hai đến thứ Sáu</p>
<p>Buổi sáng: 7h30 – 11h30</p>
<p>Buổi chiều: 13h30 – 16h30</p>
<p>Thời gian cấp: không quá 08 giờ làm việc</p>
</div>
</div>
</div>
</div>
<aside className="space-y-5 lg:sticky lg:top-24">
<div className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)]">
<h2 className="text-[28px] font-bold leading-tight text-[#1f2a44]">Liên hệ nhanh</h2>
<div className="mt-5 space-y-4 text-[16px] leading-7 text-[#5f6f86]">
{LOCATIONS.map((item) => (
<div key={item.title} className="flex items-start gap-3">
<span className="mt-2 h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>
{item.title}: {item.phone}
</span>
</div>
))}
</div>
</div>
<div className="rounded-[28px] bg-linear-to-br from-[#1d56b7] to-[#21467f] px-6 py-6 text-white shadow-[0_22px_46px_rgba(28,52,120,0.18)]">
<h2 className="text-[26px] font-bold leading-tight">Khung giờ làm việc</h2>
<div className="mt-5 space-y-4 text-[15px] leading-7 text-white/88">
<p>Từ thứ Hai đến thứ Sáu</p>
<p>Buổi sáng: 7h30 – 11h30</p>
<p>Buổi chiều: 13h30 – 16h30</p>
<p>Thời gian cấp: không quá 08 giờ làm việc</p>
</div>
</div>
</aside>
</div>
</section>
);
}
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../../types";
export type LegalTradePageProps = {
post: DynamicPostItem;
category: DynamicCategoryRouteItem;
};
export type LegalTradeSection = {
title: string;
description?: string;
bullets?: string[];
numbered?: string[];
};
export type LegalTradeTemplate = {
pageTitle: string;
intro?: string;
sections: LegalTradeSection[];
sideCard?: {
title: string;
items: string[];
};
};
export default function Page() {
return (
<div className="container flex justify-center items-center h-full py-20">
Danh bạ hội viên đang được xây dựng
</div>
);
}
\ No newline at end of file
......@@ -3,11 +3,7 @@ import Footer from "@/components/layout/main/footer";
import React from "react";
import ScrollToTopButton from "../../components/layout/main/ScrollToTopButton";
export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
export default function Layout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<main className="flex flex-col min-h-screen bg-white">
<Header />
......
This diff is collapsed.
......@@ -3,12 +3,11 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Facebook, Linkedin, Menu, Twitter, X, Youtube } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { SafeImage } from "@/components/shared/safe-image";
import Link from "next/link";
const fallbackLogo = "/logo.png";
import { useGetApiV10Logo } from "@/api/vcci-news/endpoints/logo";
import { getApiV10SiteInformation } from "@/api/vcci-news/endpoints/site-information";
import { useGetApiV10SiteInformation } from "@/api/vcci-news/endpoints/site-information";
import links from "@/links";
import type { Logo } from "@/api/vcci-news/models/logo";
import type {
......@@ -209,12 +208,14 @@ function Header() {
}
);
const { data: siteInformationResponse } =
useQuery<ApiEnvelope<SiteInformationData> | null>({
queryKey: ["site-information"],
queryFn: () => getApiV10SiteInformation().catch(() => null),
staleTime: 5 * 60 * 1000,
});
const { data: siteInformationResponse } = useGetApiV10SiteInformation(
undefined,
{
query: {
staleTime: 5 * 60 * 1000,
},
},
);
const menuItems = useMemo(
() => buildHeaderMenuTree(categoriesResponse?.responseData?.rows),
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment