Commit 8e5cfd6a authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

Merge branch 'feat/video-newsletter' into 'develop-news'

fix

See merge request !63
parents f818e2b5 2902be94
......@@ -12,14 +12,16 @@ interface RetriableAxiosRequestConfig extends InternalAxiosRequestConfig {
const createAxiosInstance = () => {
const instance = Axios.create({
baseURL: links.apiEndpoint,
withCredentials: true,
withCredentials: false,
});
instance.interceptors.request.use(async (config) => {
if (shouldSkipAuthHandling(config.url)) {
if (shouldSkipAuthHandling(config.url) || !shouldHandleAdminAuth()) {
config.withCredentials = false;
return config;
}
config.withCredentials = true;
const token = await ensureValidAdminAccessToken().catch(() => null);
if (token) {
......@@ -40,7 +42,8 @@ const createAxiosInstance = () => {
error.response?.status !== 401 ||
!originalRequest ||
originalRequest._retry ||
shouldSkipAuthHandling(originalRequest.url)
shouldSkipAuthHandling(originalRequest.url) ||
!shouldHandleAdminAuth()
) {
return Promise.reject(error);
}
......@@ -89,6 +92,11 @@ const shouldSkipAuthHandling = (url?: string | null) => {
return /\/auth\/(login|refresh|logout)(\?|$)/.test(url);
};
const shouldHandleAdminAuth = () => {
if (typeof window === "undefined") return false;
return window.location.pathname.startsWith("/admin");
};
const convertHeaders = (headers?: HeadersInit): Record<string, string> | undefined => {
if (!headers) return undefined;
......
......@@ -135,6 +135,9 @@ const HOME_CATEGORY_IDS = {
tinKinhTe: "755106b6-1aca-47dc-9a9c-d434736c33a1",
chuyenDe: "8e7090e5-bfc3-4128-81a5-37ec78c33bad",
suKien: "b85f6710-bcbc-4c0b-8b3a-09fff0e5e51a",
daoTao: "36df7021-9a74-43d6-9084-0d5ed347b7f4",
coHoiKinhDoanh: "0a460499-89c1-4f52-8592-1fb7bb69c4a2",
ketNoiHoiVien: "a37b8a02-e8b3-42ce-9225-6dae460fed99",
chinhSachPhapLuat: "cc448be9-b9ea-46a8-aa7b-0584803330e8",
lienKetNhanh: "d7f05384-b1b4-428e-b9b3-37e0e1b0cecd",
} as const;
......@@ -232,33 +235,6 @@ async function fetchHomePostRows(path: string) {
return response.responseData?.rows ?? [];
}
async function fetchHomeCategoryRows() {
const response = await useCustomClient<HomeEnvelope<HomePagedResult<RawHomeCategory>>>(
"/category?page=1&pageSize=200&sortField=sort_order&sortOrder=ASC",
);
return response.responseData?.rows ?? [];
}
function findCategoryIdByAliases(
categories: RawHomeCategory[],
aliases: readonly string[],
) {
const aliasKeys = new Set(aliases.map(normalizeSearchText));
const aliasSlugs = new Set(aliases.map(normalizeSlug));
return categories.find((category) => {
const categoryNameKey = normalizeSearchText(category.name);
const categorySlugKey = normalizeSlug(category.slug || category.name);
const categoryUrlKey = normalizeSlug(category.url);
return (
aliasKeys.has(categoryNameKey) ||
aliasSlugs.has(categorySlugKey) ||
Array.from(aliasSlugs).some((slug) => categoryUrlKey.endsWith(slug))
);
})?.id ?? null;
}
function createCategoryPostsQuery(categoryId: string, pageSize: string) {
return new URLSearchParams({
page: "1",
......@@ -276,19 +252,6 @@ function createCategoryPostsQuery(categoryId: string, pageSize: string) {
}
async function fetchHomePosts() {
const categoryRows = await fetchHomeCategoryRows().catch(() => []);
const trainingCategoryId = findCategoryIdByAliases(
categoryRows,
HOME_CATEGORY_ALIASES.daoTao,
);
const businessCategoryId = findCategoryIdByAliases(
categoryRows,
HOME_CATEGORY_ALIASES.coHoiKinhDoanh,
);
const memberConnectionCategoryId = findCategoryIdByAliases(
categoryRows,
HOME_CATEGORY_ALIASES.ketNoiHoiVien,
);
const featuredQuery = new URLSearchParams({
page: "1",
pageSize: "10",
......@@ -308,15 +271,9 @@ async function fetchHomePosts() {
const eventQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.suKien, "5");
const policyQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.chinhSachPhapLuat, "6");
const quickLinksQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.lienKetNhanh, "6");
const trainingQuery = trainingCategoryId
? createCategoryPostsQuery(String(trainingCategoryId), "10")
: null;
const businessQuery = businessCategoryId
? createCategoryPostsQuery(String(businessCategoryId), "10")
: null;
const memberConnectionQuery = memberConnectionCategoryId
? createCategoryPostsQuery(String(memberConnectionCategoryId), "10")
: null;
const trainingQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.daoTao, "10");
const businessQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.coHoiKinhDoanh, "10");
const memberConnectionQuery = createCategoryPostsQuery(HOME_CATEGORY_IDS.ketNoiHoiVien, "10");
const [
featuredRows,
......@@ -337,9 +294,9 @@ async function fetchHomePosts() {
fetchHomePostRows(`/post?${policyQuery.toString()}`),
fetchHomePostRows(`/post?${eventQuery.toString()}`),
fetchHomePostRows(`/post?${quickLinksQuery.toString()}`),
trainingQuery ? fetchHomePostRows(`/post?${trainingQuery.toString()}`) : [],
businessQuery ? fetchHomePostRows(`/post?${businessQuery.toString()}`) : [],
memberConnectionQuery ? fetchHomePostRows(`/post?${memberConnectionQuery.toString()}`) : [],
fetchHomePostRows(`/post?${trainingQuery.toString()}`),
fetchHomePostRows(`/post?${businessQuery.toString()}`),
fetchHomePostRows(`/post?${memberConnectionQuery.toString()}`),
]);
const rows = [
......
'use client';
'use client';
import dayjs from "dayjs";
import parse from "html-react-parser";
import ImageNext from "@/components/shared/image-next";
import ListCategory from "@/components/base/list-category";
import EventsCalendar from "@/app/(main)/(home)/components/events-calendar";
import { getDynamicPostBodyHtml } from "./data";
import { buildDynamicCategoryMenu } from "./data";
import StructuredPostContent from "./StructuredPostContent";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
type ArticleDetailPageProps = {
......@@ -16,15 +17,18 @@ type ArticleDetailPageProps = {
export default function ArticleDetailPage({
post,
category,
allCategories,
}: ArticleDetailPageProps) {
const publishedDate = dayjs(
post.release_at ?? post.published_at ?? post.created_at,
).format("DD/MM/YYYY");
const primaryCategory = post.categories[0]?.name || category?.name || "Tin tức";
const primaryCategory = post.categories[0]?.name || category?.name || "Tin tức";
const categoryMenu = category ? buildDynamicCategoryMenu(category, allCategories) : [];
return (
<div className="min-h-screen bg-[#fbfbfa]">
<div className="container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10">
<div className="min-h-screen bg-white">
{/* {categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null} */}
<div className="container mx-auto px-4 py-4 lg:pb-6 sm:px-6 lg:px-10">
<div className="grid grid-cols-1 gap-8 xl:grid-cols-[minmax(0,1fr)_340px] xl:gap-12">
<main className="min-w-0">
<div className="mb-5 flex flex-wrap items-center gap-3 text-xs">
......@@ -45,9 +49,9 @@ export default function ArticleDetailPage({
</p>
) : null}
<div className="mt-7 rounded-[24px] bg-white px-4 py-5 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 sm:py-6 lg:px-10">
<div className="mt-7 rounded-3xl bg-white px-4 py-5 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 sm:py-6 lg:px-10">
<div className="article-detail-content prose tiptap max-w-none overflow-hidden">
{parse(getDynamicPostBodyHtml(post))}
<StructuredPostContent post={post} />
</div>
</div>
......@@ -116,7 +120,7 @@ export default function ArticleDetailPage({
</div>
</main>
<aside className="space-y-5">
<aside className="space-y-5 xl:pt-0">
<EventsCalendar compact className="xl:w-full xl:min-w-0" />
<div className="overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]">
<div className="relative min-h-[390px] bg-[#1f334f]">
......@@ -127,13 +131,13 @@ export default function ArticleDetailPage({
height={760}
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent" />
<div className="absolute inset-0 bg-liner-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent" />
<div className="absolute bottom-8 left-7 right-7 text-white">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-white/70">
Đối tác quảng bá
</div>
<div className="mt-3 text-2xl font-bold leading-tight">
Business Combo cho doanh nghiệp hội viên
Business Combo cho hội viên doanh nghiệp
</div>
</div>
</div>
......@@ -144,3 +148,4 @@ export default function ArticleDetailPage({
</div>
);
}
......@@ -9,7 +9,9 @@ import { Pagination } from "@/components/base/pagination";
import ImageNext from "@/components/shared/image-next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category";
import {
buildDynamicCategoryMenu,
buildPostFilters,
fetchDynamicPostList,
resolveDynamicPostImage,
......@@ -102,15 +104,20 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
return new Map(entries);
}, [allCategories]);
const categoryMenu = useMemo(
() => buildDynamicCategoryMenu(category, allCategories),
[category, allCategories],
);
return (
<div className="min-h-screen bg-[#fbfbfa]">
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
{postsQuery.isLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<div className="container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10">
<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}
......@@ -197,9 +204,9 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
</div>
</main>
<aside className="order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0">
<aside className="contents xl:order-2 xl:block xl:w-[320px] xl:space-y-5 xl:pt-0">
<form
className="rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
className="order-1 rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)] xl:order-none"
onSubmit={(event) => {
event.preventDefault();
setPage(1);
......@@ -235,7 +242,7 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
</div>
</form>
<div className="overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]">
<div className="order-3 overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)] xl:order-none">
<div className="relative min-h-[390px] bg-[#1f334f]">
<ImageNext
src="/banner.webp"
......@@ -244,13 +251,13 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
height={760}
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent" />
<div className="absolute inset-0 bg-liner-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent" />
<div className="absolute bottom-8 left-7 right-7 text-white">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-white/70">
Đối tác quảng bá
</div>
<div className="mt-3 text-2xl font-bold leading-tight">
Business Combo cho doanh nghiệp hội viên
Business Combo cho hội viên doanh nghiệp
</div>
</div>
</div>
......
......@@ -9,7 +9,9 @@ import { Pagination } from "@/components/base/pagination";
import ImageNext from "@/components/shared/image-next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import ListCategory from "@/components/base/list-category";
import {
buildDynamicCategoryMenu,
buildPostFilters,
fetchDynamicPostList,
resolveDynamicPostImage,
......@@ -81,15 +83,17 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
const totalPages = postsQuery.data?.totalPages ?? 1;
const currentPage = Math.min(page, totalPages);
const paginatedPosts = postsQuery.data?.rows ?? [];
const categoryMenu = buildDynamicCategoryMenu(category, allCategories);
return (
<div className="min-h-screen bg-[#fbfbfa]">
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
{postsQuery.isLoading ? (
<div className="flex h-64 w-full items-center justify-center">
<Spinner />
</div>
) : (
<div className="container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10">
<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}
......@@ -109,7 +113,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
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">
<div className="relative aspect-3/4 overflow-hidden bg-white">
<ImageNext
src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title}
......@@ -131,7 +135,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
</div>
) : (
<div className="rounded-2xl border border-[#edf1f5] bg-white px-6 py-12 text-center text-gray-600">
{"Ch\u01b0a c\u00f3 b\u00e0i vi\u1ebft ph\u00f9 h\u1ee3p trong danh m\u1ee5c n\u00e0y."}
Chưa có tài liệu trong danh mục.
</div>
)}
......@@ -146,9 +150,9 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
</div>
</main>
<aside className="order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0">
<aside className="contents xl:order-2 xl:block xl:w-[320px] xl:space-y-5 xl:pt-0">
<form
className="rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
className="order-1 rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)] xl:order-none"
onSubmit={(event) => {
event.preventDefault();
setPage(1);
......@@ -184,7 +188,7 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
</div>
</form>
<div className="overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]">
<div className="order-3 overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)] xl:order-0">
<div className="relative min-h-[390px] bg-[#1f334f]">
<ImageNext
src="/banner.webp"
......@@ -193,13 +197,13 @@ export default function CatalogPage({ category, allCategories }: CatalogPageProp
height={760}
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent" />
<div className="absolute inset-0 bg-liner-to-t from-[#14213d]/92 via-[#14213d]/28 to-transparent" />
<div className="absolute bottom-8 left-7 right-7 text-white">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-white/70">
Đối tác quảng bá
</div>
<div className="mt-3 text-2xl font-bold leading-tight">
Business Combo cho doanh nghiệp hội viên
Business Combo cho hội viên doanh nghiệp
</div>
</div>
</div>
......
'use client';
import dayjs from "dayjs";
import parse from "html-react-parser";
import { getDynamicPostBodyHtml } from "./data";
import ListCategory from "@/components/base/list-category";
import { buildDynamicCategoryMenu } from "./data";
import StructuredPostContent from "./StructuredPostContent";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
type InformationPageProps = {
......@@ -14,21 +15,24 @@ type InformationPageProps = {
export default function InformationPage({
post,
category,
allCategories,
}: InformationPageProps) {
const publishedDate = dayjs(
post.release_at ?? post.published_at ?? post.created_at,
).format("DD/MM/YYYY");
const categoryMenu = buildDynamicCategoryMenu(category, allCategories);
return (
<div className="min-h-screen bg-[#fbfbfa]">
<div className="container mx-auto px-4 py-8 sm:px-6 lg:px-10 lg:py-10">
<div className="min-h-screen bg-white">
{categoryMenu.length ? <ListCategory categories={categoryMenu} /> : null}
<div className="container mx-auto px-4 py-4 lg:pb-6 sm:px-6 lg:px-10">
<main className="w-full">
<div className="mb-5 flex flex-wrap items-center gap-3 text-xs">
{/* <div className="mb-5 flex flex-wrap items-center gap-3 text-xs">
<span className="rounded-full bg-[#eaf0ff] px-2.5 py-1 font-semibold text-[#1f4fa3]">
{category.name}
</span>
<span className="text-[#9aa3ad]">{publishedDate}</span>
</div>
</div> */}
<h1 className="max-w-6xl text-3xl font-bold leading-tight text-[#111827] md:text-[38px] md:leading-[1.15]">
{post.title}
......@@ -41,9 +45,9 @@ export default function InformationPage({
</p>
) : null}
<div className="mt-7 rounded-[24px] bg-white px-5 py-6 shadow-[0_18px_42px_rgba(17,24,39,0.06)] sm:px-8 lg:px-10">
<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">
{parse(getDynamicPostBodyHtml(post))}
<StructuredPostContent post={post} />
</div>
</div>
......
"use client";
import parse from "html-react-parser";
import ImageNext from "@/components/shared/image-next";
import { getDynamicPostBodyHtml } from "./data";
import type { DynamicPostContentSection, DynamicPostItem } from "./types";
type StructuredPostContentProps = {
post: DynamicPostItem;
};
function getGridClassName(columns: number) {
if (columns >= 4) return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4";
if (columns === 3) return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3";
if (columns === 2) return "grid-cols-1 sm:grid-cols-2";
return "grid-cols-1";
}
function StructuredImageSection({ section }: { section: DynamicPostContentSection }) {
const images = section.images.filter((item) => item.image?.url);
if (!images.length) return null;
return (
<div className={`not-prose my-6 grid gap-4 ${getGridClassName(section.image_columns)}`}>
{images.map((item) => {
const image = item.image;
if (!image?.url) return null;
return (
<figure
key={`${section.id}-${image.id || image.url}-${item.position}`}
className="overflow-hidden rounded-[18px] bg-white"
>
<ImageNext
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"
/>
</figure>
);
})}
</div>
);
}
export default function StructuredPostContent({ post }: StructuredPostContentProps) {
const sections = (post.content_structure?.post_content ?? [])
.slice()
.sort((left, right) => left.position - right.position);
if (!sections.length) {
return <>{parse(getDynamicPostBodyHtml(post))}</>;
}
const hasRenderableSection = sections.some(
(section) => section.content.trim() || section.images.length,
);
if (!hasRenderableSection) {
return <>{parse(getDynamicPostBodyHtml(post))}</>;
}
return (
<>
{sections.map((section) => {
if (section.type === "image") {
return <StructuredImageSection key={section.id} section={section} />;
}
const content = section.content.trim();
if (!content) return null;
return <div key={section.id}>{parse(content)}</div>;
})}
</>
);
}
......@@ -30,6 +30,18 @@ type RawPostThumbnail = {
url?: string | null;
};
type RawPostSectionImage = {
position?: number | null;
image?: {
id?: string | null;
name?: string | null;
alt?: string | null;
url?: string | null;
path?: string | null;
original?: string | null;
} | null;
};
type RawPostItem = {
id?: string | null;
title?: string | null;
......@@ -57,6 +69,9 @@ type RawPostItem = {
type?: string | null;
content?: string | null;
position?: number | null;
image_rows?: number | null;
image_columns?: number | null;
images?: RawPostSectionImage[] | null;
}> | null;
} | null;
};
......@@ -111,6 +126,33 @@ const mapPostContentSections = (item: RawPostItem): DynamicPostContentSection[]
typeof section?.position === "number"
? section.position
: index + 1,
image_rows:
typeof section?.image_rows === "number" && section.image_rows > 0
? section.image_rows
: 1,
image_columns:
typeof section?.image_columns === "number" && section.image_columns > 0
? section.image_columns
: 1,
images: (section?.images ?? [])
.map((item, imageIndex) => ({
position:
typeof item?.position === "number"
? item.position
: imageIndex + 1,
image: item?.image
? {
id: String(item.image.id ?? ""),
name: String(item.image.name ?? item.image.original ?? ""),
alt: String(item.image.alt ?? item.image.name ?? ""),
url: resolveUploadUrl(item.image.url ?? item.image.path ?? item.image.original ?? ""),
path: item.image.path ?? null,
original: item.image.original ?? null,
}
: null,
}))
.filter((item) => Boolean(item.image?.url))
.sort((left, right) => left.position - right.position),
}));
};
......
......@@ -34,6 +34,19 @@ export type DynamicPostContentSection = {
type: string;
content: string;
position: number;
image_rows: number;
image_columns: number;
images: Array<{
position: number;
image: {
id: string;
name: string;
alt: string;
url: string;
path?: string | null;
original?: string | null;
} | null;
}>;
};
export type DynamicPostItem = {
......
......@@ -126,6 +126,14 @@ function Header() {
};
}, []);
useEffect(() => {
document.body.style.overflow = toggleMenu ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [toggleMenu]);
return (
<header className="sticky top-0 z-50 shadow-[0_1px_0_rgba(15,23,42,0.05)]">
<div
......@@ -252,13 +260,32 @@ function Header() {
</div>
<div
className={`fixed left-0 right-0 top-[80px] z-40 border-t border-slate-200 bg-white transition-all duration-300 lg:hidden ${
className={`fixed inset-0 z-[60] bg-white transition-all duration-300 lg:hidden ${
toggleMenu
? "pointer-events-auto h-[calc(100dvh-80px)] translate-y-0 opacity-100"
: "pointer-events-none h-[calc(100dvh-80px)] -translate-y-2 opacity-0"
? "pointer-events-auto translate-y-0 opacity-100"
: "pointer-events-none -translate-y-2 opacity-0"
}`}
>
<div className="flex h-full flex-col overflow-y-auto overscroll-contain px-4 py-3">
<div className="flex h-full flex-col overflow-hidden">
<div className="sticky top-0 z-10 flex h-[78px] shrink-0 items-center justify-between border-b border-slate-100 bg-white px-6 shadow-[0_1px_0_rgba(15,23,42,0.04)]">
<Link href="/" className="flex w-[136px] shrink-0 items-center" onClick={() => setToggleMenu(false)}>
<Image
className="h-auto w-[108px] object-contain"
src={logo}
alt="VCCI-HCM"
priority
/>
</Link>
<button
onClick={() => setToggleMenu(false)}
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-300 bg-white text-[#163b73] transition hover:bg-slate-50"
aria-label={\u00f3ng menu"}
>
<X size={18} />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3">
<input
className="h-11 w-full shrink-0 rounded-md border border-slate-200 px-4 text-sm outline-none placeholder:text-slate-400 focus:border-[#2f57ff]"
type="text"
......@@ -302,6 +329,7 @@ function Header() {
</div>
</div>
</div>
</div>
</header>
);
}
......
......@@ -140,7 +140,7 @@ function SearchContent() {
const currentPage = Number(postsQuery.data?.page ?? page);
return (
<div className="min-h-screen bg-[#fbfbfa]">
<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">
<div className="mb-8">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-4xl">
......@@ -197,9 +197,9 @@ function SearchContent() {
)}
</main>
<aside className="order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0">
<aside className="contents xl:order-2 xl:block xl:w-[320px] xl:space-y-5 xl:pt-0">
<form
className="rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
className="order-1 rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)] xl:order-none"
onSubmit={(event) => {
event.preventDefault();
setPage(1);
......@@ -235,7 +235,7 @@ function SearchContent() {
</div>
</form>
<div className="overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]">
<div className="order-3 overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)] xl:order-none">
<div className="relative min-h-[390px] bg-[#1f334f]">
<ImageNext
src="/banner.webp"
......@@ -266,7 +266,7 @@ export default function Page() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-[#fbfbfa]">
<div className="flex min-h-screen items-center justify-center bg-white">
<Spinner className="size-8" />
</div>
}
......
......@@ -42,7 +42,7 @@ function VideoPageContent() {
};
return (
<div className="min-h-screen bg-[#fbfbfa]">
<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">
<div className="mb-8">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-4xl">
......@@ -126,7 +126,7 @@ export default function Page() {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-[#fbfbfa]">
<div className="flex min-h-screen items-center justify-center bg-white">
<Spinner className="size-8" />
</div>
}
......
......@@ -19,10 +19,10 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] }
const isActive = (href: string) => pathname === href;
return (
<div className="border-t border-gray-200 bg-white py-2">
<div className="w-full px-4 sm:px-6 lg:px-8">
<div className="py-3">
<div className="flex max-w-full items-center gap-3 overflow-x-auto pb-1">
<div className="border-t border-gray-200 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-10">
<div className="pt-6">
<div className="client-category-scrollbar flex max-w-full items-center gap-3 overflow-x-auto overflow-y-hidden pb-1 pl-0.5 pr-2">
{categories.map((category) => {
const href = resolveHref(category);
const menu = { id: category.id, name: category.name, link: href };
......@@ -37,6 +37,29 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] }
</div>
</div>
</div>
<style jsx global>{`
.client-category-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(22, 85, 157, 0.35) transparent;
}
.client-category-scrollbar::-webkit-scrollbar {
height: 6px;
}
.client-category-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.client-category-scrollbar::-webkit-scrollbar-thumb {
background: rgba(22, 85, 157, 0.28);
border-radius: 999px;
}
.client-category-scrollbar:hover::-webkit-scrollbar-thumb {
background: rgba(22, 85, 157, 0.48);
}
`}</style>
</div>
);
};
......
......@@ -40,7 +40,7 @@ export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; ac
href={normalizedLink}
className={menuItemTriggerClass(variant)}
>
<span className="relative z-10 truncate">{menu.name}</span>
<span className={cn("relative z-10", variant === "main" ? "truncate" : "")}>{menu.name}</span>
{variant === 'main' ? <span className="menu-item-underline" aria-hidden="true" /> : null}
</Link>
)
......@@ -66,7 +66,7 @@ export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; ac
function menuItemTriggerClass(variant: 'main' | 'secondary') {
if (variant === 'secondary') {
return cn(
'inline-flex h-[36px] items-center justify-center rounded-full border border-[#d6dfeb] bg-white px-5 text-[13px] font-medium leading-none text-[#5f6b7d] shadow-none transition-colors duration-150',
'inline-flex min-h-[38px] max-w-[280px] items-center justify-center rounded-full border border-[#d6dfeb] bg-white px-5 py-2 text-center text-[13px] font-medium leading-[1.25] text-[#5f6b7d] shadow-none transition-colors duration-150 sm:max-w-none sm:whitespace-nowrap',
'hover:border-[#c5d2e3] hover:bg-[#f7faff] hover:text-[#1b5aa1]',
'aria-selected:border-[#16559d] aria-selected:bg-[#16559d] aria-selected:text-white',
'focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
......
......@@ -3,7 +3,11 @@ const DEFAULT_BACKEND_ORIGIN = "https://vietprodev.duckdns.org/gateway/vcci-news
const normalizeOrigin = (value?: string | null) => value?.trim().replace(/\/+$/, "") || "";
const readOrigin = (key: "NEXT_PUBLIC_BACKEND_HOST" | "NEXT_PUBLIC_FRONTEND_HOST") => {
const envOrigin = normalizeOrigin(process.env[key]);
const envOrigin = normalizeOrigin(
key === "NEXT_PUBLIC_BACKEND_HOST"
? process.env.NEXT_PUBLIC_BACKEND_HOST
: process.env.NEXT_PUBLIC_FRONTEND_HOST,
);
if (envOrigin) return envOrigin;
if (key === "NEXT_PUBLIC_BACKEND_HOST" && process.env.NODE_ENV === "production") {
return DEFAULT_BACKEND_ORIGIN;
......
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