Commit 41547f6c authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

[tag]0.1-vcci

parents 13c64ec8 51ad7fac
Pipeline #53068 passed with stages
in 4 minutes and 27 seconds
......@@ -3,18 +3,48 @@
import { useEffect, useMemo } from "react";
import { notFound, useParams, useRouter, useSearchParams } from "next/navigation";
import { Spinner } from "@/components/ui";
import ArticlePage from "./templates/ArticlePage";
import ArticleDetailPage from "./templates/ArticleDetailPage";
import CatalogPage from "./templates/CatalogPage";
import NewsPage from "./templates/NewsPage";
import NewsDetailPage from "./templates/NewsDetailPage";
import InformationPage from "./templates/InformationPage";
import {
useDynamicCategories,
useDynamicPostDetail,
useDynamicSinglePagePost,
findDynamicCategoryByPath,
findFirstChildCategory,
findMenuCategoryForPost,
} from "./templates/data";
import AnPham from "./static-pages/AnPham";
import ThuVienTaiLieu from "./static-pages/ThuVienTaiLieu";
import AboutVcciHcm from "./static-pages/AboutVcciHcm";
import Service from "./static-pages/Service";
import MemberRegistration from "./static-pages/MemberRegistration";
import MarketProfile from "./static-pages/MarketProfile";
import MemberBenefits from "./static-pages/MemberBenefits";
import PhapChe from "./static-pages/PhapChe";
import CertificateTradeDocument from "./static-pages/CertificateTradeDocument";
import Procedure from "./static-pages/Procedure";
import Forms from "./static-pages/Forms";
import Fees from "./static-pages/Fees";
import Locations from "./static-pages/Locations";
import Contact from "./static-pages/Contact";
import MemberDirectory from "./static-pages/MemberDirectory";
import Search from "./static-pages/Search";
import SiteMap from "./static-pages/SiteMap";
import Video from "./static-pages/Video";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import { useGetApiV10Category } from "@/api/vcci-news/endpoints/category";
import type { DynamicPostItem, DynamicCategoryRouteItem } from "./templates/types";
const normalizePath = (value?: string | null) => {
const trimmed = value?.trim() ?? "";
if (!trimmed || trimmed === "/") return "/";
return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
};
const getSlugFromPath = (value?: string | null) => {
const normalizedPath = normalizePath(value);
const segments = normalizedPath.split("/").filter(Boolean);
const lastSegment = segments.at(-1);
if (!lastSegment) return "";
try {
return decodeURIComponent(lastSegment).trim();
} catch {
return lastSegment.trim();
}
};
export default function DynamicPageClient() {
const params = useParams();
......@@ -26,49 +56,119 @@ export default function DynamicPageClient() {
const postId = searchParams.get("id")?.trim() ?? "";
const preferredCategoryId = searchParams.get("categoryId")?.trim() ?? "";
const categoryQuery = useDynamicCategories({ staleTime: 5 * 60 * 1000 });
// Detect ending slug for static pages (multi-depth support: /abc, /x/abc, /x/y/abc)
const endingSlug = slug.length > 0 ? String(slug[slug.length - 1] ?? "") : "";
const isStaticPage = [
"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",
].includes(endingSlug);
const { data: categoryData, isLoading: categoryLoading } = useGetApiV10Category({
page: 1,
pageSize: 200,
sortField: "sort_order",
sortOrder: "asc",
});
const allCategories = useMemo(
() =>
((categoryData?.responseData?.rows ?? []) as unknown as DynamicCategoryRouteItem[])
.filter((item) => item.id && item.name && item.type)
.sort((a, b) => (a.sort_order ?? Number.MAX_SAFE_INTEGER) - (b.sort_order ?? Number.MAX_SAFE_INTEGER)),
[categoryData],
);
const matchedCategory = useMemo(
() => findDynamicCategoryByPath(categoryQuery.data ?? [], routePath),
[categoryQuery.data, routePath],
() => allCategories.find((item) => normalizePath(item.url) === normalizePath(routePath)) ?? null,
[allCategories, routePath],
);
const detailQuery = useDynamicPostDetail(postId, routePath, {
enabled:
(Boolean(postId) || Boolean(routePath)) &&
!categoryQuery.isLoading &&
(Boolean(postId) || !matchedCategory),
staleTime: 60 * 1000,
});
const needsCategory = ["an-pham", "thu-vien-tai-lieu", "ve-vcci-hcm", "dich-vu-cung-cap", "dang-ky-hoi-vien", "ho-so-thi-truong"].includes(endingSlug);
// Fetch post detail by id or slug (for news articles)
const postSlug = getSlugFromPath(routePath);
const detailFilters = postId
? `id==${postId},is_hidden==false,is_active==true,type==news`
: postSlug
? `slug==${postSlug},is_hidden==false,is_active==true,type==news`
: undefined;
const { data: detailData, isLoading: detailLoading } = useGetApiV10Post(
{
page: 1,
pageSize: 1,
sortField: "release_at",
sortOrder: "desc",
filters: detailFilters,
},
{
query: {
enabled: !isStaticPage && !needsCategory && Boolean(detailFilters) && !categoryLoading && (Boolean(postId) || !matchedCategory),
},
},
);
const detailPost = (detailData?.responseData?.rows?.[0] ?? null) as unknown as DynamicPostItem | null;
const resolvedCategory = useMemo(
() =>
(preferredCategoryId
? categoryQuery.data?.find((item) => item.id === preferredCategoryId)
? allCategories.find((item) => item.id === preferredCategoryId)
: undefined) ??
matchedCategory ??
findMenuCategoryForPost(detailQuery.data ?? null, categoryQuery.data ?? []),
[preferredCategoryId, matchedCategory, detailQuery.data, categoryQuery.data],
(detailPost
? allCategories.find((cat) => detailPost.categories?.some((c) => c.id === cat.id))
: null),
[preferredCategoryId, matchedCategory, detailPost, allCategories],
);
const singlePageQuery = useDynamicSinglePagePost(resolvedCategory?.id, {
enabled: resolvedCategory?.type === "page",
staleTime: 60 * 1000,
});
// Fetch single page post (for type === "page")
const singlePageFilters = matchedCategory?.id
? `category.id==${matchedCategory.id},is_hidden==false,is_active==true,type==page`
: undefined;
const { data: singlePageData } = useGetApiV10Post(
{
page: 1,
pageSize: 1,
sortField: "release_at",
sortOrder: "desc",
filters: singlePageFilters,
},
{
query: {
enabled: Boolean(matchedCategory?.id) && matchedCategory?.type === "page",
},
},
);
const singlePagePost = (singlePageData?.responseData?.rows?.[0] ?? null) as unknown as DynamicPostItem | null;
// Redirect: if URL is /hoi-vien (1 segment) and type === "category" → redirect to first child
useEffect(() => {
if (!matchedCategory || matchedCategory.type !== "category") return;
const firstChild = findFirstChildCategory(matchedCategory, categoryQuery.data ?? []);
const firstChild = allCategories
.filter((item) => item.parent_id === matchedCategory.id)
.sort((a, b) => (a.sort_order ?? Number.MAX_SAFE_INTEGER) - (b.sort_order ?? Number.MAX_SAFE_INTEGER))[0];
if (slug.length === 1 && firstChild?.url) {
router.replace(firstChild.url);
}
}, [matchedCategory, categoryQuery.data, router, slug.length]);
}, [matchedCategory, allCategories, router, slug.length]);
// all page components
const isLoading =
categoryQuery.isLoading ||
detailQuery.isLoading ||
(resolvedCategory?.type === "page" && singlePageQuery.isLoading);
categoryLoading ||
detailLoading ||
(resolvedCategory?.type === "page" && !singlePagePost) ||
resolvedCategory?.type === "category";
if (isLoading) {
return (
......@@ -78,54 +178,167 @@ export default function DynamicPageClient() {
);
}
if (detailQuery.data) {
switch (endingSlug) {
case "an-pham":
return (
<ArticleDetailPage
post={detailQuery.data}
category={resolvedCategory}
allCategories={categoryQuery.data ?? []}
<AnPham
category={matchedCategory}
allCategories={allCategories}
/>
);
}
if (resolvedCategory?.type === "page") {
if (!singlePageQuery.data) return notFound();
case "thu-vien-tai-lieu":
return (
<InformationPage
post={singlePageQuery.data}
category={resolvedCategory}
allCategories={categoryQuery.data ?? []}
<ThuVienTaiLieu
category={matchedCategory}
allCategories={allCategories}
/>
);
case "ve-vcci-hcm":
return (
<AboutVcciHcm
post={singlePagePost}
category={matchedCategory}
allCategories={allCategories}
/>
);
case "dich-vu-cung-cap":
return (
<Service
post={singlePagePost}
category={matchedCategory}
allCategories={allCategories}
/>
);
case "dang-ky-hoi-vien":
return (
<MemberRegistration
post={singlePagePost}
category={matchedCategory}
allCategories={allCategories}
/>
);
case "ho-so-thi-truong":
return (
<MarketProfile
post={singlePagePost}
category={matchedCategory}
allCategories={allCategories}
/>
);
case "loi-ich-hoi-vien-vcci":
return (
<MemberBenefits
category={matchedCategory}
allCategories={allCategories}
/>
);
case "phap-che":
return (
<PhapChe
category={matchedCategory}
allCategories={allCategories}
/>
);
case "giay-chung-nhan-gcn-va-chung-tu-thuong-mai-cttm":
return (
<CertificateTradeDocument
category={matchedCategory}
allCategories={allCategories}
/>
);
case "quy-trinh-tiep-nhan-ho-so-cap-gcn-va-xac-nhan-cttm":
return (
<Procedure
category={matchedCategory}
allCategories={allCategories}
/>
);
case "bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm":
return (
<Forms
category={matchedCategory}
allCategories={allCategories}
/>
);
case "phi-cap-gcn-va-xac-nhan-cttm":
return (
<Fees
category={matchedCategory}
allCategories={allCategories}
/>
);
case "diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm":
return (
<Locations
category={matchedCategory}
allCategories={allCategories}
/>
);
case "thong-tin-lien-he":
return (
<Contact
category={matchedCategory}
allCategories={allCategories}
/>
);
case "danh-ba-hoi-vien":
return (
<MemberDirectory
category={matchedCategory}
allCategories={allCategories}
/>
);
case "search":
return (
<Search
category={matchedCategory}
allCategories={allCategories}
/>
);
case "site-map":
return (
<SiteMap
category={matchedCategory}
allCategories={allCategories}
/>
);
case "video":
return (
<Video
category={matchedCategory}
allCategories={allCategories}
/>
);
}
if (resolvedCategory?.type === "news") {
if (
resolvedCategory.slug === "an-pham" ||
resolvedCategory.slug === "thu-vien-tai-lieu"
) {
if (detailPost) {
return (
<CatalogPage
category={resolvedCategory}
allCategories={categoryQuery.data ?? []}
<NewsDetailPage
post={detailPost}
category={resolvedCategory ?? null}
allCategories={allCategories}
/>
);
}
if (resolvedCategory?.type === "page") {
if (!singlePagePost) return notFound();
return (
<ArticlePage
<InformationPage
post={singlePagePost}
category={resolvedCategory}
allCategories={categoryQuery.data ?? []}
allCategories={allCategories}
/>
);
}
if (resolvedCategory?.type === "category") {
if (resolvedCategory?.type === "news") {
return (
<div className="flex min-h-[50vh] items-center justify-center">
<Spinner />
</div>
<NewsPage
category={resolvedCategory}
allCategories={allCategories}
/>
);
}
......
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] ?? "") : "";
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}` },
};
}
let post = null;
// Try to find a matching category for non-post routes
let categoryTitle = "";
if (!postId) {
try {
post = postId
? await fetchDynamicPostById(postId)
: await fetchDynamicPostBySlug(path);
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 {
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}` },
};
}
......
"use client";
import dayjs from "dayjs";
import { ShieldCheck, Target, Zap } from "lucide-react";
import parse from "html-react-parser";
import Link from "next/link";
import ListCategory from "@/components/base/list-category";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import { SafeImage } from "@/components/shared/safe-image";
import links from "@/links";
import {
buildDynamicPostHref,
buildDynamicCategoryMenu,
stripHtml,
} from "../templates/data";
import StructuredPostContent from "../templates/StructuredPostContent";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../templates/types";
const ABOUT_HIGHLIGHTS = [
{
key: "vision",
title: "Tầm nhìn",
description:
"Trở thành tổ chức hàng đầu đại diện cho cộng đồng doanh nghiệp tại phía Nam, kiến tạo môi trường kinh doanh thuận lợi và bền vững.",
icon: Target,
featured: false,
},
{
key: "mission",
title: "Sứ mệnh",
description:
"Nâng cao năng lực cạnh tranh của cộng đồng doanh nghiệp thông qua các hoạt động đối thoại, xúc tiến và xây dựng năng lực, tạo cầu nối vững chắc.",
icon: Zap,
featured: true,
},
{
key: "values",
title: "Giá trị cốt lõi",
bullets: ["Uy tín - Minh bạch", "Chuyên nghiệp", "Đổi mới sáng tạo", "Tinh thần cộng đồng"],
icon: ShieldCheck,
featured: false,
},
] as const;
const ACTIVITY_AREAS = [
{
name: "TP. Hồ Chí Minh",
description:
"Trung tâm điều phối, kết nối doanh nghiệp và lan tỏa các chương trình hỗ trợ hội viên trên toàn khu vực.",
toneClass: "from-[#f59e0b] to-[#ef4444]",
},
{
name: "Đồng Nai",
description:
"Địa bàn công nghiệp trọng điểm, gắn với nhu cầu xúc tiến thương mại và hỗ trợ sản xuất - xuất khẩu.",
toneClass: "from-[#2563eb] to-[#60a5fa]",
},
{
name: "Lâm Đồng",
description:
"Khu vực phát triển nông nghiệp công nghệ cao, du lịch và các mô hình kinh tế xanh, bền vững.",
toneClass: "from-[#16a34a] to-[#86efac]",
},
{
name: "Tây Ninh",
description:
"Cửa ngõ giao thương quan trọng, thuận lợi cho kết nối chuỗi cung ứng, logistics và thương mại biên giới.",
toneClass: "from-[#7c3aed] to-[#c4b5fd]",
},
] as const;
const TIN_VCCI_CATEGORY_ID = "b89b2ba6-a699-47cb-87e4-0643aea549a9";
function renderSummary(summary?: string) {
const value = summary?.trim() ?? "";
if (!value || !stripHtml(value)) {
return null;
}
return parse(value);
}
type TinVcciApiRow = {
id?: string | null;
title?: string | null;
slug?: string | null;
published_at?: string | null;
release_at?: string | null;
created_at?: string | null;
thumbnail?: {
path?: string | null;
original?: string | null;
url?: string | null;
} | null;
};
type AboutVcciHcmProps = {
post: DynamicPostItem | null;
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function AboutVcciHcm({ post, category, allCategories }: AboutVcciHcmProps) {
const tinVcciFilters = [
`category.id==${TIN_VCCI_CATEGORY_ID}`,
"is_hidden==false",
"is_active==true",
"type==news",
]
.map((item) => item?.trim())
.filter(Boolean)
.join(",");
const { data: tinVcciData, isLoading: tinVcciLoading } = useGetApiV10Post({
page: 1,
pageSize: 3,
sortField: "release_at",
sortOrder: "desc",
filters: tinVcciFilters || undefined,
});
const tinVcciItems = ((tinVcciData?.responseData?.rows ?? []) as unknown as TinVcciApiRow[]).map((item) => ({
id: String(item.id ?? ""),
title: String(item.title ?? "").trim(),
externalLink: buildDynamicPostHref(item.slug?.trim() || "#", item.id ? String(item.id) : ""),
publishedAt: String(item.published_at ?? item.release_at ?? item.created_at ?? ""),
thumbnailUrl:
links.resolveImageUrl(
item.thumbnail?.url?.trim() ||
item.thumbnail?.path?.trim() ||
item.thumbnail?.original?.trim() ||
"",
) || "/thumbnail.png",
thumbnailAlt: String(item.title ?? "").trim() || "Tin VCCI",
}));
const summaryContent = renderSummary(post?.summary);
if (!post) {
return (
<div className="flex min-h-[50vh] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-[#2450b5] border-t-transparent" />
</div>
);
}
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="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px] 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]">
Giới thiệu <span className="text-[#2f57ff]">chung</span>
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
{summaryContent ? (
<div className="mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8">
{summaryContent}
</div>
) : 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="about-vcci-page-content page-detail-content prose tiptap max-w-none overflow-hidden">
<StructuredPostContent post={post} />
</div>
</div>
</div>
<aside className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)] lg:sticky lg:top-24">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44]">
Khu vực hoạt động
</h2>
<div className="mt-6 space-y-4">
{ACTIVITY_AREAS.map((item) => (
<div key={item.name} className="flex items-center gap-3 text-[18px] text-[#58667d]">
<span className="h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>{item.name}</span>
</div>
))}
</div>
</aside>
</section>
<style jsx global>{`
.about-vcci-page-content figure {
width: 100% !important;
max-width: 100% !important;
margin: 28px 0 !important;
text-align: center;
}
.about-vcci-page-content img {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
object-fit: contain;
}
`}</style>
<section className="mt-10 space-y-10 md:mt-12 md:space-y-12">
<div>
<div className="text-center">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44] md:text-[38px]">
Tầm nhìn, <span className="text-[#2f57ff]">Sứ mệnh</span> &{" "}
<span className="text-[#f0a400]">Giá trị</span>
</h2>
<div className="mx-auto mt-3 h-1 w-16 rounded-full bg-[#f5a400]" />
</div>
<div className="mt-8 grid gap-4 lg:grid-cols-3">
{ABOUT_HIGHLIGHTS.map((item) => {
const Icon = item.icon;
return (
<article
key={item.key}
className={[
"rounded-3xl border px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)]",
item.featured
? "border-[#1f56b8] bg-linear-to-br from-[#1d56b7] to-[#21467f] text-white"
: "border-[#edf1f6] bg-white text-[#24415f]",
].join(" ")}
>
<div
className={[
"flex h-11 w-11 items-center justify-center rounded-2xl",
item.featured ? "bg-white/10 text-[#ffbf2b]" : "bg-[#eff4ff] text-[#7ea1eb]",
].join(" ")}
>
<Icon className="h-5 w-5" />
</div>
<h3
className={[
"mt-5 text-[24px] font-bold",
item.featured ? "text-white" : "text-[#1d2e4f]",
].join(" ")}
>
{item.title}
</h3>
{"description" in item ? (
<p
className={[
"mt-3 text-[15px] leading-7",
item.featured ? "text-white/82" : "text-[#5f6f86]",
].join(" ")}
>
{item.description}
</p>
) : (
<ul className="mt-3 space-y-2.5 text-[15px] text-[#5f6f86]">
{item.bullets.map((bullet) => (
<li key={bullet} className="flex items-start gap-2.5">
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-[#f5a400]" />
<span>{bullet}</span>
</li>
))}
</ul>
)}
</article>
);
})}
</div>
</div>
<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="flex items-start justify-between gap-4">
<div className="min-w-0">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44]">
Khu vực hoạt động
</h2>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<p className="mt-4 max-w-3xl text-[16px] leading-8 text-[#5f6f86]">
VCCI-HCM hoạt động tại 4 khu vực trọng điểm, bảo đảm hỗ trợ doanh nghiệp theo từng địa bàn cụ thể.
</p>
</div>
<div className="hidden rounded-[18px] border border-[#edf1f6] bg-[#f8fbff] px-4 py-3 text-sm font-medium text-[#2450b5] md:block">
4 điểm hoạt động chính
</div>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{ACTIVITY_AREAS.map((item, index) => (
<article
key={item.name}
className="rounded-[22px] border border-[#edf1f6] bg-[#fbfcff] px-5 py-5 shadow-[0_10px_26px_rgba(17,24,39,0.04)]"
>
<div className="flex items-center gap-3">
<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">
<h3 className="text-[20px] font-bold leading-tight text-[#1f2a44]">
{item.name}
</h3>
</div>
</div>
<p className="mt-4 text-[15px] leading-7 text-[#5f6f86]">
{item.description}
</p>
</article>
))}
</div>
</div>
<div>
<div className="mb-6 flex items-center justify-between gap-4">
<div>
<h2 className="text-[28px] font-bold leading-tight text-[#2450b5] md:text-[32px]">
TIN VCCI
</h2>
<div className="mt-3 h-1 w-16 rounded-full bg-[#f5a400]" />
</div>
<Link
href="/thong-tin-truyen-thong/tin-vcci"
className="text-sm font-semibold text-[#2450b5] transition-colors hover:text-[#173f9f]"
>
Xem tất cả
</Link>
</div>
<div className="grid gap-5 pb-6 md:grid-cols-2 xl:grid-cols-3">
{tinVcciItems.map((item) => (
<Link
key={item.id}
href={item.externalLink}
className="group overflow-hidden rounded-[22px] bg-white shadow-[0_18px_38px_rgba(28,52,120,0.16)] transition-transform hover:-translate-y-1"
>
<div className="relative aspect-[1.28] overflow-hidden">
<SafeImage
src={item.thumbnailUrl}
alt={item.thumbnailAlt}
width={720}
height={520}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.04]"
/>
<div className="absolute inset-0 bg-linear-to-t from-[#1d2f56]/90 via-[#1d2f56]/28 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-4">
<span className="inline-flex rounded-[10px] bg-[#f5c21b] px-2.5 py-1 text-xs font-bold text-[#1d3f90]">
Tin VCCI
</span>
<h3 className="mt-3 line-clamp-2 text-[17px] font-bold leading-6 text-white">
{item.title}
</h3>
<p className="mt-2 text-sm text-white/78">
{dayjs(item.publishedAt).format("DD/MM/YYYY")}
</p>
</div>
</div>
</Link>
))}
</div>
</div>
</section>
</>
</div>
</div>
);
}
'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';
"use client";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem } from "../templates/types";
import {
BadgeCheck,
Building2,
......@@ -8,7 +11,6 @@ import {
Globe2,
ShieldCheck,
} from "lucide-react";
import type { LegalTradePageProps } from "./types";
const DEFINITIONS = [
{
......@@ -49,8 +51,18 @@ const DEFINITIONS = [
},
];
export default function CertificateTradeDocumentPage(_: LegalTradePageProps) {
type CertificateTradeDocumentProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function CertificateTradeDocument({ category, allCategories }: CertificateTradeDocumentProps) {
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">
......@@ -128,5 +140,7 @@ export default function CertificateTradeDocumentPage(_: LegalTradePageProps) {
</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 { 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 { useMemo, useState } from "react";
import { FileText, Globe2, Newspaper, TrendingUp } from "lucide-react";
import { SafeImage } from "@/components/shared/safe-image";
import type { DynamicPostItem } from "../types";
type MarketProfilePageProps = {
post: DynamicPostItem;
};
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "../templates/data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../templates/types";
type RegionMarketItem = {
name: string;
......@@ -210,7 +208,13 @@ const OVERVIEW_ITEMS = [
},
] as const;
export default function MarketProfilePage({ post }: MarketProfilePageProps) {
type MarketProfileProps = {
post: DynamicPostItem | null;
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function MarketProfile({ post, category, allCategories }: MarketProfileProps) {
const [activeRegionKey, setActiveRegionKey] = useState("dong-nam-a");
const activeRegion = useMemo(
......@@ -219,7 +223,12 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
[activeRegionKey],
);
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="space-y-8">
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_300px] xl:items-start">
<div className="min-w-0">
......@@ -230,7 +239,7 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<p className="mt-5 max-w-4xl text-base leading-8 text-[#5b6880] md:text-[17px]">
{post.summary?.trim() || activeRegion.description}
{post?.summary?.trim() || activeRegion.description}
</p>
<div className="mt-7 overflow-hidden rounded-[30px] border border-[#dce7f7] bg-white shadow-[0_18px_42px_rgba(17,24,39,0.06)]">
......@@ -312,5 +321,7 @@ export default function MarketProfilePage({ post }: MarketProfilePageProps) {
})}
</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,8 +85,18 @@ const MEMBER_BENEFITS = [
},
] as const;
export default function MemberBenefitsPage() {
type MemberBenefitsProps = {
category: DynamicCategoryRouteItem | null;
allCategories: DynamicCategoryRouteItem[];
};
export default function MemberBenefits({ category, allCategories }: MemberBenefitsProps) {
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="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]">
......@@ -166,5 +179,7 @@ export default function MemberBenefitsPage() {
</div>
</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,8 +248,17 @@ 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 (
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
......@@ -243,5 +268,6 @@ export default function Page() {
>
<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,16 +71,23 @@ 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ế.";
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="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]">
......@@ -130,5 +141,7 @@ export default function ServicePage({ post }: ServicePageProps) {
</div>
</div>
</section>
</div>
</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(
{
const { data: videosData, isLoading: videosLoading, isError: videosError } = 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) => ({
});
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 ?? ""),
})),
count,
page: pageData.page ?? page,
pageSize,
totalPages: Math.max(1, Math.ceil(count / pageSize)),
};
},
},
},
);
const videos = videosQuery.data?.rows ?? [];
const totalPages = videosQuery.data?.totalPages ?? 1;
const currentPage = videosQuery.data?.page ?? page;
}));
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,8 +133,17 @@ 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 (
<>
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
......@@ -163,5 +153,6 @@ export default function Page() {
>
<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,10 +21,16 @@ 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;
......@@ -30,14 +39,21 @@ function StructuredImageSection({ section }: { section: DynamicPostContentSectio
return (
<figure
key={`${section.id}-${image.id || image.url}-${item.position}`}
className="overflow-hidden rounded-[18px] bg-white"
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"
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">
......@@ -48,6 +64,17 @@ function StructuredImageSection({ section }: { section: DynamicPostContentSectio
);
})}
</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 dayjs from "dayjs";
import { useQuery } from "@tanstack/react-query";
import { ShieldCheck, Target, Zap } from "lucide-react";
import parse from "html-react-parser";
import Link from "next/link";
import { useGetApiV10Post } from "@/api/vcci-news/endpoints/post";
import { SafeImage } from "@/components/shared/safe-image";
import { buildDynamicPostHref, buildVisibleNewsFilters, stripHtml } from "../data";
import links from "@/links";
import StructuredPostContent from "../StructuredPostContent";
import type { DynamicPostItem } from "../types";
const ABOUT_HIGHLIGHTS = [
{
key: "vision",
title: "Tầm nhìn",
description:
"Trở thành tổ chức hàng đầu đại diện cho cộng đồng doanh nghiệp tại phía Nam, kiến tạo môi trường kinh doanh thuận lợi và bền vững.",
icon: Target,
featured: false,
},
{
key: "mission",
title: "Sứ mệnh",
description:
"Nâng cao năng lực cạnh tranh của cộng đồng doanh nghiệp thông qua các hoạt động đối thoại, xúc tiến và xây dựng năng lực, tạo cầu nối vững chắc.",
icon: Zap,
featured: true,
},
{
key: "values",
title: "Giá trị cốt lõi",
bullets: ["Uy tín - Minh bạch", "Chuyên nghiệp", "Đổi mới sáng tạo", "Tinh thần cộng đồng"],
icon: ShieldCheck,
featured: false,
},
] as const;
const ACTIVITY_AREAS = [
{
name: "TP. Hồ Chí Minh",
description:
"Trung tâm điều phối, kết nối doanh nghiệp và lan tỏa các chương trình hỗ trợ hội viên trên toàn khu vực.",
toneClass: "from-[#f59e0b] to-[#ef4444]",
},
{
name: "Đồng Nai",
description:
"Địa bàn công nghiệp trọng điểm, gắn với nhu cầu xúc tiến thương mại và hỗ trợ sản xuất - xuất khẩu.",
toneClass: "from-[#2563eb] to-[#60a5fa]",
},
{
name: "Lâm Đồng",
description:
"Khu vực phát triển nông nghiệp công nghệ cao, du lịch và các mô hình kinh tế xanh, bền vững.",
toneClass: "from-[#16a34a] to-[#86efac]",
},
{
name: "Tây Ninh",
description:
"Cửa ngõ giao thương quan trọng, thuận lợi cho kết nối chuỗi cung ứng, logistics và thương mại biên giới.",
toneClass: "from-[#7c3aed] to-[#c4b5fd]",
},
] as const;
const TIN_VCCI_CATEGORY_ID = "b89b2ba6-a699-47cb-87e4-0643aea549a9";
function renderSummary(summary?: string) {
const value = summary?.trim() ?? "";
if (!value || !stripHtml(value)) {
return null;
}
return parse(value);
}
type TinVcciApiRow = {
id?: string | null;
title?: string | null;
slug?: string | null;
published_at?: string | null;
release_at?: string | null;
created_at?: string | null;
thumbnail?: {
path?: string | null;
original?: string | null;
url?: string | null;
} | null;
};
type TinVcciApiEnvelope = {
responseData?: {
rows?: TinVcciApiRow[];
};
};
type AboutVcciHcmPageProps = {
post: DynamicPostItem;
};
export default function AboutVcciHcmPage({
post,
}: AboutVcciHcmPageProps) {
const tinVcciQuery = useGetApiV10Post(
{
page: 1,
pageSize: 3,
sortField: "release_at",
sortOrder: "desc",
filters: buildVisibleNewsFilters([`category.id==${TIN_VCCI_CATEGORY_ID}`]),
},
{
query: {
staleTime: 60 * 1000,
select: (response) =>
((response?.responseData?.rows ?? []) as unknown as TinVcciApiRow[]).map((item) => ({
id: String(item.id ?? ""),
title: String(item.title ?? "").trim(),
externalLink: buildDynamicPostHref(item.slug?.trim() || "#", item.id ? String(item.id) : ""),
publishedAt: String(item.published_at ?? item.release_at ?? item.created_at ?? ""),
thumbnailUrl:
links.resolveImageUrl(
item.thumbnail?.url?.trim() ||
item.thumbnail?.path?.trim() ||
item.thumbnail?.original?.trim() ||
"",
) || "/thumbnail.png",
thumbnailAlt: String(item.title ?? "").trim() || "Tin VCCI",
})),
},
},
);
const tinVcciItems = tinVcciQuery.data ?? [];
const summaryContent = renderSummary(post.summary);
return (
<>
<section className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px] 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]">
Giới thiệu <span className="text-[#2f57ff]">chung</span>
</h1>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
{summaryContent ? (
<div className="mt-5 max-w-6xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8">
{summaryContent}
</div>
) : 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="about-vcci-page-content page-detail-content prose tiptap max-w-none overflow-hidden">
<StructuredPostContent post={post} />
</div>
</div>
</div>
<aside className="rounded-[28px] border border-[#edf1f6] bg-[#fbfcff] px-6 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.05)] lg:sticky lg:top-24">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44]">
Khu vực hoạt động
</h2>
<div className="mt-6 space-y-4">
{ACTIVITY_AREAS.map((item) => (
<div key={item.name} className="flex items-center gap-3 text-[18px] text-[#58667d]">
<span className="h-2.5 w-2.5 shrink-0 rounded-full bg-[#2f6ce5]" />
<span>{item.name}</span>
</div>
))}
</div>
</aside>
</section>
<style jsx global>{`
.about-vcci-page-content figure {
width: 100% !important;
max-width: 100% !important;
margin: 28px 0 !important;
text-align: center;
}
.about-vcci-page-content img {
width: 100% !important;
max-width: 100% !important;
height: auto !important;
margin-left: auto !important;
margin-right: auto !important;
object-fit: contain;
}
`}</style>
<section className="mt-10 space-y-10 md:mt-12 md:space-y-12">
<div>
<div className="text-center">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44] md:text-[38px]">
Tầm nhìn, <span className="text-[#2f57ff]">Sứ mệnh</span> &{" "}
<span className="text-[#f0a400]">Giá trị</span>
</h2>
<div className="mx-auto mt-3 h-1 w-16 rounded-full bg-[#f5a400]" />
</div>
<div className="mt-8 grid gap-4 lg:grid-cols-3">
{ABOUT_HIGHLIGHTS.map((item) => {
const Icon = item.icon;
return (
<article
key={item.key}
className={[
"rounded-3xl border px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)]",
item.featured
? "border-[#1f56b8] bg-linear-to-br from-[#1d56b7] to-[#21467f] text-white"
: "border-[#edf1f6] bg-white text-[#24415f]",
].join(" ")}
>
<div
className={[
"flex h-11 w-11 items-center justify-center rounded-2xl",
item.featured ? "bg-white/10 text-[#ffbf2b]" : "bg-[#eff4ff] text-[#7ea1eb]",
].join(" ")}
>
<Icon className="h-5 w-5" />
</div>
<h3
className={[
"mt-5 text-[24px] font-bold",
item.featured ? "text-white" : "text-[#1d2e4f]",
].join(" ")}
>
{item.title}
</h3>
{"description" in item ? (
<p
className={[
"mt-3 text-[15px] leading-7",
item.featured ? "text-white/82" : "text-[#5f6f86]",
].join(" ")}
>
{item.description}
</p>
) : (
<ul className="mt-3 space-y-2.5 text-[15px] text-[#5f6f86]">
{item.bullets.map((bullet) => (
<li key={bullet} className="flex items-start gap-2.5">
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-[#f5a400]" />
<span>{bullet}</span>
</li>
))}
</ul>
)}
</article>
);
})}
</div>
</div>
<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="flex items-start justify-between gap-4">
<div className="min-w-0">
<h2 className="text-[30px] font-bold leading-tight text-[#1f2a44]">
Khu vực hoạt động
</h2>
<div className="mt-3 h-[3px] w-16 rounded-full bg-[#f5a400]" />
<p className="mt-4 max-w-3xl text-[16px] leading-8 text-[#5f6f86]">
VCCI-HCM hoạt động tại 4 khu vực trọng điểm, bảo đảm hỗ trợ doanh nghiệp theo từng địa bàn cụ thể.
</p>
</div>
<div className="hidden rounded-[18px] border border-[#edf1f6] bg-[#f8fbff] px-4 py-3 text-sm font-medium text-[#2450b5] md:block">
4 điểm hoạt động chính
</div>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{ACTIVITY_AREAS.map((item, index) => (
<article
key={item.name}
className="rounded-[22px] border border-[#edf1f6] bg-[#fbfcff] px-5 py-5 shadow-[0_10px_26px_rgba(17,24,39,0.04)]"
>
<div className="flex items-center gap-3">
<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">
<h3 className="text-[20px] font-bold leading-tight text-[#1f2a44]">
{item.name}
</h3>
</div>
</div>
<p className="mt-4 text-[15px] leading-7 text-[#5f6f86]">
{item.description}
</p>
</article>
))}
</div>
</div>
<div>
<div className="mb-6 flex items-center justify-between gap-4">
<div>
<h2 className="text-[28px] font-bold leading-tight text-[#2450b5] md:text-[32px]">
TIN VCCI
</h2>
<div className="mt-3 h-1 w-16 rounded-full bg-[#f5a400]" />
</div>
<Link
href="/thong-tin-truyen-thong/tin-vcci"
className="text-sm font-semibold text-[#2450b5] transition-colors hover:text-[#173f9f]"
>
Xem tất cả
</Link>
</div>
<div className="grid gap-5 pb-6 md:grid-cols-2 xl:grid-cols-3">
{tinVcciItems.map((item) => (
<Link
key={item.id}
href={item.externalLink}
className="group overflow-hidden rounded-[22px] bg-white shadow-[0_18px_38px_rgba(28,52,120,0.16)] transition-transform hover:-translate-y-1"
>
<div className="relative aspect-[1.28] overflow-hidden">
<SafeImage
src={item.thumbnailUrl}
alt={item.thumbnailAlt}
width={720}
height={520}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.04]"
/>
<div className="absolute inset-0 bg-linear-to-t from-[#1d2f56]/90 via-[#1d2f56]/28 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-4">
<span className="inline-flex rounded-[10px] bg-[#f5c21b] px-2.5 py-1 text-xs font-bold text-[#1d3f90]">
Tin VCCI
</span>
<h3 className="mt-3 line-clamp-2 text-[17px] font-bold leading-6 text-white">
{item.title}
</h3>
<p className="mt-2 text-sm text-white/78">
{dayjs(item.publishedAt).format("DD/MM/YYYY")}
</p>
</div>
</div>
</Link>
))}
</div>
</div>
</section>
</>
);
}
'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>
);
}
'use client';
import { BriefcaseBusiness, FileText, GraduationCap, Mail, Phone, Scale } from "lucide-react";
import type { LegalTradePageProps } from "./types";
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,
},
];
export default function PhapChePage(_: 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á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>
);
}
'use client';
import { Clock3, FileCheck2, FileUp, Landmark, ReceiptText, RotateCcw } from "lucide-react";
import type { LegalTradePageProps } from "./types";
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,
},
];
export default function ProcedurePage(_: 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]">
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>
);
}
'use client';
import { useParams } from "next/navigation";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "../../types";
import ContactPage from "./ContactPage";
import CertificateTradeDocumentPage from "./CertificateTradeDocumentPage";
import FeesPage from "./FeesPage";
import FormsPage from "./FormsPage";
import LocationsPage from "./LocationsPage";
import PhapChePage from "./PhapChePage";
import ProcedurePage from "./ProcedurePage";
type LegalTradeRouterProps = {
post: DynamicPostItem;
category: DynamicCategoryRouteItem;
};
function resolveVariant(
post: DynamicPostItem,
category: DynamicCategoryRouteItem,
fullPath: string,
) {
const url = category.url || fullPath;
const slug = category.slug || post.slug;
if (
slug === "phap-che" ||
url === "/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/phap-che"
) {
return "phap-che" as const;
}
if (
slug === "giay-chung-nhan-gcn-va-chung-tu-thuong-mai-cttm" ||
url ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/giay-chung-nhan-gcn-va-chung-tu-thuong-mai-cttm"
) {
return "certificate-trade-document" as const;
}
if (
slug === "quy-trinh-tiep-nhan-ho-so-cap-gcn-va-xac-nhan-cttm" ||
slug === "thu-tuc-cap-co" ||
url ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/quy-trinh-tiep-nhan-ho-so-cap-gcn-va-xac-nhan-cttm" ||
url === "/xuat-xu-hang-hoa/thu-tuc-cap-co"
) {
return "procedure" as const;
}
if (slug === "bieu-mau-co-va-cach-khai" || url === "/xuat-xu-hang-hoa/bieu-mau-co-va-cach-khai") {
return "forms" as const;
}
if (
slug === "bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm" ||
fullPath ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm" ||
url ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/bieu-mau-gcn-va-noi-dung-khai-bao-gcn-cttm"
) {
return "forms" as const;
}
if (
slug === "phi-cap-gcn-va-xac-nhan-cttm" ||
slug === "phi-va-le-phi-cap-co" ||
fullPath ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/phi-cap-gcn-va-xac-nhan-cttm" ||
url ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/phi-cap-gcn-va-xac-nhan-cttm" ||
url === "/xuat-xu-hang-hoa/phi-va-le-phi-cap-co"
) {
return "fees" as const;
}
if (
slug === "diem-cap-va-cap-gcn-va-xac-nhan-cttm" ||
slug === "diem-cap-va-thoi-gian-cap-co" ||
slug === "diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm" ||
fullPath ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/diem-cap-va-cap-gcn-va-xac-nhan-cttm" ||
fullPath ===
"/phap-che-va-xac-nhan-chung-tu-tm/diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm" ||
url ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/diem-cap-va-cap-gcn-va-xac-nhan-cttm" ||
url ===
"/phap-che-va-xac-nhan-chung-tu-tm/diem-cap-va-thoi-gian-cap-gcn-va-xac-nhan-cttm" ||
url === "/xuat-xu-hang-hoa/diem-cap-va-thoi-gian-cap-co"
) {
return "locations" as const;
}
if (
slug === "thong-tin-lien-he" ||
slug === "thong-tin-lien-he-co" ||
fullPath ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/thong-tin-lien-he" ||
url ===
"/phap-che-cap-giay-chung-nhan-va-xac-nhan-chung-tu-thuong-mai/thong-tin-lien-he" ||
url === "/xuat-xu-hang-hoa/thong-tin-lien-he-co"
) {
return "contact" as const;
}
return "overview" as const;
}
export default function LegalTradePages({ post, category }: LegalTradeRouterProps) {
const params = useParams();
const pathSegments = Array.isArray(params.slug) ? params.slug : [params.slug];
const currentSlug = pathSegments.at(-1) ?? "";
const fullPath = `/${pathSegments.filter(Boolean).join("/")}`;
const variant = resolveVariant(
{
...post,
slug: currentSlug || post.slug,
},
{
...category,
slug: currentSlug || category.slug,
url: fullPath || category.url,
},
fullPath,
);
if (variant === "phap-che") {
return <PhapChePage post={post} category={category} />;
}
if (variant === "certificate-trade-document") {
return <CertificateTradeDocumentPage post={post} category={category} />;
}
if (variant === "procedure") {
return <ProcedurePage post={post} category={category} />;
}
if (variant === "forms") {
return <FormsPage post={post} category={category} />;
}
if (variant === "fees") {
return <FeesPage post={post} category={category} />;
}
if (variant === "locations") {
return <LocationsPage post={post} category={category} />;
}
if (variant === "contact") {
return <ContactPage post={post} category={category} />;
}
return <PhapChePage post={post} category={category} />;
}
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 />
......
"use client";
import React from "react";
import Link from "next/link";
import { useGetApiV10PageConfig } from "@/api/vcci-news/endpoints/page-config";
import { GetNewsPageConfigResponseType } from "@/api/vcci-news/types/news-page-config";
function SiteMapPage() {
const { data: categoriesData, isLoading, isError } = useGetApiV10PageConfig<GetNewsPageConfigResponseType>();
if (isLoading) {
return (
<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 (
<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 (
<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>
);
}
export default SiteMapPage;
......@@ -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),
const { data: siteInformationResponse } = useGetApiV10SiteInformation(
undefined,
{
query: {
staleTime: 5 * 60 * 1000,
});
},
},
);
const menuItems = useMemo(
() => buildHeaderMenuTree(categoriesResponse?.responseData?.rows),
......
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
type ImageLightboxProps = {
src: string;
alt?: string;
caption?: string;
open: boolean;
onOpenChange: (open: boolean) => void;
className?: string;
};
export function ImageLightbox({
src,
alt,
caption,
open,
onOpenChange,
className,
}: ImageLightboxProps) {
React.useEffect(() => {
if (!open) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onOpenChange(false);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, onOpenChange]);
return (
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 z-50 bg-black/90 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogPrimitive.Content
className={cn(
"fixed inset-0 z-50 flex flex-col items-center justify-center p-4 outline-none",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
onClick={() => onOpenChange(false)}
>
<DialogPrimitive.Close
className="absolute right-4 top-4 z-10 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white/90 transition hover:bg-white/20 hover:text-white focus:outline-none focus:ring-2 focus:ring-white/40"
aria-label="Đóng"
>
<X className="h-5 w-5" />
</DialogPrimitive.Close>
<div
className="flex max-h-full max-w-full flex-col items-center justify-center"
onClick={(event) => event.stopPropagation()}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={alt || ""}
className="max-h-[85vh] max-w-full rounded-lg object-contain shadow-2xl"
/>
{caption ? (
<DialogPrimitive.Description className="mt-4 max-w-3xl text-center text-sm text-white/80 sm:text-base">
{caption}
</DialogPrimitive.Description>
) : (
<DialogPrimitive.Description className="sr-only">
{alt || "Hình ảnh phóng to"}
</DialogPrimitive.Description>
)}
</div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
}
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