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

Merge branch 'fix/header' into 'develop-news'

super update UI

See merge request !60
parents 4b4dbccb 80e61988
......@@ -5,6 +5,7 @@ import { addMonths, format, getDay, startOfMonth, subMonths } from "date-fns";
import dayjs from "dayjs";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useMemo, useState } from "react";
import { cn } from "@/lib/utils";
const weekDays = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"];
......@@ -17,7 +18,13 @@ const isTrainingEvent = (item: HomePostItem) =>
return key.includes("đào tạo") || key.includes("dao-tao");
});
function EventsCalendar() {
function EventsCalendar({
className,
compact = false,
}: {
className?: string;
compact?: boolean;
}) {
const { eventCalendarPosts } = useHomePosts();
const firstEventDate = eventCalendarPosts[0]?.registrationDeadline
......@@ -71,18 +78,36 @@ function EventsCalendar() {
const highlightedEvent = selectedEvents[0] ?? monthEvents[0];
return (
<aside className="w-full rounded-[28px] bg-white p-4 text-[#24469c] shadow-[0_18px_38px_rgba(16,61,130,0.16)] md:p-5 xl:w-[28%] xl:min-w-[320px]">
<aside
className={cn(
"w-full rounded-[28px] bg-white text-[#24469c] shadow-[0_18px_38px_rgba(16,61,130,0.16)]",
compact ? "p-4" : "p-4 md:p-5",
className ?? "xl:w-[28%] xl:min-w-[320px]",
)}
>
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="client-section-title uppercase">
<div className="min-w-0">
<h2
className={cn(
"uppercase",
compact
? "text-[26px] font-bold leading-tight tracking-normal"
: "client-section-title",
)}
>
Lịch sự kiện
</h2>
<p className="mt-1.5 text-[12px] uppercase tracking-[0.28em] text-[#7f8eab]">
<p
className={cn(
"mt-1.5 text-[12px] uppercase text-[#7f8eab]",
compact ? "tracking-[0.18em]" : "tracking-[0.28em]",
)}
>
{`THÁNG ${format(currentMonth, "MM/yyyy")}`}
</p>
</div>
<div className="flex gap-2">
<div className="flex shrink-0 gap-2">
<button
type="button"
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
......@@ -100,8 +125,8 @@ function EventsCalendar() {
</div>
</div>
<div className="mt-3 h-[4px] w-[60px] rounded-full bg-[#f7b500]" />
<div className="mt-4 border-t border-[#ebf0f8] pt-3.5">
<div className={cn("h-[4px] w-[60px] rounded-full bg-[#f7b500]", compact ? "mt-2.5" : "mt-3")} />
<div className={cn("border-t border-[#ebf0f8] pt-3.5", compact ? "mt-3" : "mt-4")}>
<div className="grid grid-cols-7 gap-y-2.5 text-center text-[11px] font-semibold uppercase text-[#9aabc6]">
{weekDays.map((day) => (
<div key={day}>{day}</div>
......@@ -159,7 +184,7 @@ function EventsCalendar() {
</div>
</div>
<div className="mt-4 flex items-center gap-5 text-[12px] font-medium text-[#45608f]">
<div className="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-[12px] font-medium text-[#45608f]">
<div className="flex items-center gap-2">
<span className="h-2.5 w-2.5 rounded-full bg-[#1e3f9a]" />
<span>Sự kiện</span>
......
......@@ -5,11 +5,17 @@ import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import memberImages from "@/constants/memberImages";
import Link from "next/link";
const MEMBER_CONNECTION_FALLBACK_IMAGE = "/home/20-2048x1365.webp";
function Members() {
const { memberConnectionPosts, categoryLinks, categoryNames } = useHomePosts();
const featuredConnection = memberConnectionPosts[0];
const sectionLink =
categoryLinks.get(categoryNames.ketNoiHoiVien.toLowerCase()) ?? "/hoi-vien/ket-noi-hoi-vien";
const connectionImage =
featuredConnection?.thumbnail?.url ?? MEMBER_CONNECTION_FALLBACK_IMAGE;
const connectionImageAlt =
featuredConnection?.thumbnail?.alt || featuredConnection?.title || "VCCI HCM";
return (
<section className="flex flex-col gap-5 pb-8 xl:flex-row xl:items-stretch">
......@@ -62,26 +68,20 @@ function Members() {
</div>
</div>
{featuredConnection ? (
<Link
href={featuredConnection.externalLink}
className="block overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
>
<div className="aspect-[1.25/1] overflow-hidden rounded-[20px]">
<ImageNext
src={featuredConnection.thumbnail?.url ?? "/thumbnail.png"}
alt={featuredConnection.thumbnail?.alt || featuredConnection.title}
width={520}
height={420}
className="h-full w-full object-cover"
/>
</div>
</Link>
) : (
<div className="overflow-hidden rounded-[20px] bg-[#eef3fb] shadow-[0_16px_32px_rgba(31,59,124,0.08)]">
<div className="aspect-[1.25/1] rounded-[20px] bg-[#e3ebf8]" />
<Link
href={sectionLink}
className="block overflow-hidden rounded-[20px] shadow-[0_16px_32px_rgba(31,59,124,0.12)]"
>
<div className="aspect-[1.25/1] overflow-hidden rounded-[20px]">
<ImageNext
src={connectionImage}
alt={connectionImageAlt}
width={520}
height={420}
className="h-full w-full object-cover"
/>
</div>
)}
</Link>
</aside>
</section>
);
......
......@@ -22,7 +22,7 @@ const videos = [
function VideoAndPartners() {
return (
<section className="flex flex-col gap-6 pb-10 xl:flex-row xl:items-start">
<section className="flex flex-col gap-6 pb-10 xl:flex-row xl:items-stretch">
<div className="flex-1">
<div className="mb-5 flex items-start justify-between gap-3">
<div>
......@@ -75,7 +75,7 @@ function VideoAndPartners() {
</div>
</div>
<aside className="w-full xl:w-[43%]">
<aside className="flex w-full flex-col xl:w-[43%]">
<div className="mb-5 flex items-start justify-between gap-3">
<div>
<h2 className="client-section-title uppercase text-[#24469c]">
......@@ -85,11 +85,11 @@ function VideoAndPartners() {
</div>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 xl:h-[318px] xl:grid-rows-2">
{partnerImages.slice(0, 6).map((src, index) => (
<div
key={src}
className="flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)]"
className="flex h-[96px] items-center justify-center rounded-[14px] border border-[#edf1f7] bg-white px-5 py-4 shadow-[0_8px_20px_rgba(31,59,124,0.05)] xl:h-auto"
>
<ImageNext
src={src}
......
......@@ -29,7 +29,7 @@ const Page = () => {
</Link>
</div> */}
<section className="flex flex-col lg:flex-row pb-16 gap-5 mb-0">
<section className="flex flex-col lg:flex-row pb-8 gap-5 mb-0">
<News />
<QuickLinks />
</section >
......
......@@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/react-query";
import { Spinner } from "@/components/ui";
import ArticlePage from "./templates/ArticlePage";
import ArticleDetailPage from "./templates/ArticleDetailPage";
import CatalogPage from "./templates/CatalogPage";
import InformationPage from "./templates/InformationPage";
import {
fetchDynamicCategories,
......@@ -98,6 +99,18 @@ export default function DynamicPage() {
}
if (resolvedCategory?.type === "news") {
if (
resolvedCategory.slug === "an-pham" ||
resolvedCategory.slug === "thu-vien-tai-lieu"
) {
return (
<CatalogPage
category={resolvedCategory}
allCategories={categoryQuery.data ?? []}
/>
);
}
return (
<ArticlePage
category={resolvedCategory}
......
......@@ -2,12 +2,9 @@
import dayjs from "dayjs";
import parse from "html-react-parser";
import EventCalendar from "@/components/base/event-calendar";
import ListCategory from "@/components/base/list-category";
import {
buildDynamicCategoryMenu,
getDynamicPostBodyHtml,
} from "./data";
import ImageNext from "@/components/shared/image-next";
import EventsCalendar from "@/app/(main)/(home)/components/events-calendar";
import { getDynamicPostBodyHtml } from "./data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
type ArticleDetailPageProps = {
......@@ -19,35 +16,128 @@ type ArticleDetailPageProps = {
export default function ArticleDetailPage({
post,
category,
allCategories,
}: ArticleDetailPageProps) {
const categoryMenu = category
? buildDynamicCategoryMenu(category, allCategories)
: [];
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";
return (
<div className="container w-full flex justify-center items-center pb-10">
<div className="flex flex-col gap-5 w-full">
{categoryMenu.length > 0 ? <ListCategory categories={categoryMenu} /> : <br />}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
<main className="lg:col-span-2 bg-white border rounded-md p-8">
<div className="pb-5 text-primary text-2xl leading-normal font-medium">
{post.title}
</div>
<div className="flex items-center gap-2 text-sm mb-4">
<span className="text-base text-blue-700">
{dayjs(post.release_at ?? post.published_at ?? post.created_at).format("DD/MM/YYYY")}
<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="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">
<span className="rounded-full bg-[#eaf0ff] px-2.5 py-1 font-semibold text-[#1f4fa3]">
{primaryCategory}
</span>
<span className="text-[#9aa3ad]">{publishedDate}</span>
</div>
<hr className="my-5" />
<div className="flex-1 text-app-grey text-base overflow-hidden">
<div className="prose tiptap max-w-none overflow-hidden">
<h1 className="max-w-4xl 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-4xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8">
{post.summary}
</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="article-detail-content prose tiptap max-w-none overflow-hidden">
{parse(getDynamicPostBodyHtml(post))}
</div>
</div>
<div className="article-detail-styles">
<style jsx global>{`
.article-detail-content {
color: #1f2937;
font-size: 16px;
line-height: 1.85;
}
.article-detail-content p,
.article-detail-content div {
margin: 0 0 18px;
}
.article-detail-content h1,
.article-detail-content h2,
.article-detail-content h3,
.article-detail-content h4,
.article-detail-content h5,
.article-detail-content h6 {
margin: 0 0 18px;
color: #111827;
font-weight: 700;
line-height: 1.45;
}
.article-detail-content img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
margin: 24px auto 10px;
border-radius: 14px;
}
.article-detail-content figure {
margin: 28px 0;
}
.article-detail-content figcaption,
.article-detail-content .wp-caption-text {
margin-top: 10px;
color: #6b7280;
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.article-detail-content a {
color: #14519f;
font-weight: 600;
}
.article-detail-content ul,
.article-detail-content ol {
margin: 18px 0;
padding-left: 24px;
}
.article-detail-content li {
margin: 8px 0;
}
`}</style>
</div>
</main>
<aside className="space-y-6">
<EventCalendar />
<aside className="space-y-5">
<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]">
<ImageNext
src="/banner.webp"
alt="Đối tác quảng bá"
width={640}
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 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
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
......
'use client';
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { Spinner } from "@/components/ui";
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 {
buildPostFilters,
fetchDynamicPostList,
resolveDynamicPostImage,
} from "./data";
import type { DynamicCategoryRouteItem } from "./types";
type CatalogPageProps = {
category: DynamicCategoryRouteItem;
allCategories: DynamicCategoryRouteItem[];
};
const getCatalogImageClassName = (index: number) =>
index % 4 === 0
? "object-cover"
: index % 4 === 1
? "object-contain bg-[#f0f3f8]"
: index % 4 === 2
? "object-cover object-center"
: "object-contain bg-[#eef4fb]";
export default function CatalogPage({ category, allCategories }: CatalogPageProps) {
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();
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 postsQuery = useQuery({
queryKey: ["catalog-posts", category.id, page, pageSize, keyword],
queryFn: () =>
fetchDynamicPostList({
page,
pageSize,
filters: buildPostFilters([
`category.id==${category.id}`,
"is_hidden==false",
"is_active==true",
"status==published",
"type==news",
keyword ? `title@=${keyword}` : null,
]),
}),
staleTime: 60 * 1000,
});
const totalPages = postsQuery.data?.totalPages ?? 1;
const currentPage = Math.min(page, totalPages);
const paginatedPosts = postsQuery.data?.rows ?? [];
return (
<div className="min-h-screen bg-[#fbfbfa]">
{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="mb-8">
<h1 className="text-3xl font-bold leading-tight text-[#111827] md:text-4xl">
{category.name}
</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, index) => {
return (
<Link
key={item.id}
href={item.external_link}
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">
<ImageNext
src={resolveDynamicPostImage(item.thumbnail)}
alt={item.title}
width={520}
height={693}
className={`h-full w-full transition-transform duration-500 group-hover:scale-[1.03] ${getCatalogImageClassName(index)}`}
/>
</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\u01b0a c\u00f3 b\u00e0i vi\u1ebft ph\u00f9 h\u1ee3p trong danh m\u1ee5c n\u00e0y."}
</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="order-1 space-y-5 xl:order-2 xl:w-[320px] xl:pt-0">
<form
className="rounded-[22px] border border-[#edf1f5] bg-white p-5 shadow-[0_14px_34px_rgba(17,24,39,0.05)]"
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>
<div className="overflow-hidden rounded-[22px] shadow-[0_18px_42px_rgba(17,24,39,0.12)]">
<div className="relative min-h-[390px] bg-[#1f334f]">
<ImageNext
src="/banner.webp"
alt="Đối tác quảng bá"
width={640}
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 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
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
)}
</div>
);
}
......@@ -37,24 +37,24 @@ export default function EventDetailPage() {
return notFound();
}
return (
<div className='container w-full flex justify-center items-center pb-10'>
<div className='container flex w-full items-center justify-center px-4 pb-10 sm:px-6 lg:px-10'>
{isLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<div className='flex flex-col gap-5 w-full'>
<div className='flex w-full flex-col gap-5'>
<ListCategory categories={category?.responseData?.children} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
<main className="lg:col-span-2 bg-white border rounded-md py-10 px-5 md:px-20">
<div className="grid grid-cols-1 gap-5 xl:grid-cols-[minmax(0,1fr)_340px]">
<main className="min-w-0 rounded-md border bg-white px-4 py-6 sm:px-6 md:px-10 lg:px-14 lg:py-10">
<div className='pb-5 text-primary text-2xl leading-normal font-medium'>
{eventsDetail?.responseData?.rows[0]?.name}
</div>
<hr className="py-2" />
{/* Top summary with image + details */}
<div className="flex flex-col md:flex-row gap-6 my-6">
<div className="w-full lg:w-1/2 bg-gray-50 rounded-md overflow-hidden">
<div className="my-6 flex flex-col gap-6 md:flex-row">
<div className="w-full overflow-hidden rounded-md bg-gray-50 md:w-1/2">
{eventsDetail?.responseData?.rows[0].image ? (
<div className="w-full h-52 relative ">
<EventImage
......@@ -67,7 +67,7 @@ export default function EventDetailPage() {
)}
</div>
<div className="w-full lg:w-1/2 bg-white border rounded-md p-3 md:p-6">
<div className="w-full rounded-md border bg-white p-3 md:w-1/2 md:p-6">
<div className="flex flex-col gap-3">
<div className="text-sm text-gray-500 flex flex-row items-center gap-2">
<Clock className="h-5 w-5 text-yellow-500" />
......@@ -112,13 +112,13 @@ export default function EventDetailPage() {
</div>
{/* Full description */}
<div className="prose tiptap overflow-hidden">
<div className="prose tiptap max-w-none overflow-hidden">
{parse(eventsDetail?.responseData?.rows[0]?.description ?? "")}
</div>
</main>
{/* Sidebar */}
<aside className="space-y-6">
<aside className="min-w-0 space-y-6">
<EventCalendar />
<div className="bg-white border rounded-md overflow-hidden">
<div className="w-full h-75 relative bg-gray-100">
......@@ -159,4 +159,4 @@ function EventImage({ src, alt }: EventImageProps) {
}}
/>
);
}
\ No newline at end of file
}
......@@ -61,17 +61,17 @@ export default function EventPage() {
//template
return (
<>
<div className="min-h-screen container mx-auto">
<div className="container mx-auto min-h-screen px-4 py-6 sm:px-6 lg:px-10">
{eventsLoading ? (
<div className="flex justify-center items-center w-full h-64">
<Spinner />
</div>
) : (
<div className="w-full flex flex-col gap-5">
<div className="flex w-full flex-col gap-5">
<ListCategory categories={categoriesPage?.responseData?.children} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<main className="lg:col-span-2 bg-background">
<div className="pb-5 overflow-hidden">
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_340px]">
<main className="min-w-0 bg-background">
<div className="overflow-hidden pb-5">
{events?.responseData?.rows?.map((item) => (
<CardEvents
key={item.id}
......@@ -92,7 +92,7 @@ export default function EventPage() {
</div>
</div>
</main>
<aside className="space-y-6">
<aside className="min-w-0 space-y-6">
<ListFilter onSearch={setSubmitSearch} />
<EventCalendar />
</aside>
......
'use client';
import dayjs from "dayjs";
import parse from "html-react-parser";
import ListCategory from "@/components/base/list-category";
import {
buildDynamicCategoryMenu,
getDynamicPostBodyHtml,
} from "./data";
import { getDynamicPostBodyHtml } from "./data";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
type InformationPageProps = {
......@@ -17,24 +14,102 @@ type InformationPageProps = {
export default function InformationPage({
post,
category,
allCategories,
}: InformationPageProps) {
const categoryMenu = buildDynamicCategoryMenu(category, allCategories);
const publishedDate = dayjs(
post.release_at ?? post.published_at ?? post.created_at,
).format("DD/MM/YYYY");
return (
<div className="container w-full flex justify-center items-center pb-10">
<div className="flex flex-col gap-5 w-full">
{categoryMenu.length > 0 ? <ListCategory categories={categoryMenu} /> : <br />}
<main className="bg-white border rounded-md py-10 px-5 md:px-20 lg:px-20">
<div className="text-primary text-2xl leading-normal font-bold">
{post.title}
<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">
<main className="w-full">
<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>
<hr className="my-5" />
<div className="flex-1 text-app-grey text-base overflow-hidden">
<div className="prose tiptap max-w-none overflow-hidden">
<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">
{post.summary}
</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="page-detail-content prose tiptap max-w-none overflow-hidden">
{parse(getDynamicPostBodyHtml(post))}
</div>
</div>
<div className="page-detail-styles">
<style jsx global>{`
.page-detail-content {
color: #1f2937;
font-size: 16px;
line-height: 1.85;
}
.page-detail-content p,
.page-detail-content div {
margin: 0 0 18px;
}
.page-detail-content h1,
.page-detail-content h2,
.page-detail-content h3,
.page-detail-content h4,
.page-detail-content h5,
.page-detail-content h6 {
margin: 0 0 18px;
color: #111827;
font-weight: 700;
line-height: 1.45;
}
.page-detail-content img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
margin: 24px auto 10px;
border-radius: 14px;
}
.page-detail-content figure {
margin: 28px 0;
}
.page-detail-content figcaption,
.page-detail-content .wp-caption-text {
margin-top: 10px;
color: #6b7280;
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.page-detail-content a {
color: #14519f;
font-weight: 600;
}
.page-detail-content ul,
.page-detail-content ol {
margin: 18px 0;
padding-left: 24px;
}
.page-detail-content li {
margin: 8px 0;
}
`}</style>
</div>
</main>
</div>
</div>
......
......@@ -143,12 +143,12 @@ function Header() {
{"\u0110\u0103ng K\u00fd H\u1ed9i Vi\u00ean"}
</Link>
</div>
<Link
{/* <Link
className="px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
href="/site-map"
>
Sitemap
</Link>
</Link> */}
<Link
className="px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
href="https://vccihcm.vn/lien-he"
......@@ -252,13 +252,15 @@ function Header() {
</div>
<div
className={`overflow-hidden border-t border-slate-200 bg-white transition-all duration-300 lg:hidden ${
toggleMenu ? "max-h-[520px] opacity-100" : "max-h-0 opacity-0"
className={`fixed left-0 right-0 top-[80px] z-40 border-t border-slate-200 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"
}`}
>
<div className="px-4 py-3">
<div className="flex h-full flex-col overflow-y-auto overscroll-contain px-4 py-3">
<input
className="h-11 w-full rounded-md border border-slate-200 px-4 text-sm outline-none placeholder:text-slate-400 focus:border-[#2f57ff]"
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"
placeholder={"T\u00ecm ki\u1ebfm"}
onKeyDown={(e) => {
......@@ -270,34 +272,34 @@ function Header() {
}
}}
/>
</div>
<div className="pb-3">
{menuItems.map((category) => (
<div key={category.id} className="border-t border-slate-100 first:border-t-0">
<Link
href={category.url || "#"}
className="block px-5 py-3 text-[15px] font-medium text-slate-700 transition hover:bg-slate-50 hover:text-[#2f57ff]"
onClick={() => setToggleMenu(false)}
>
{category.name}
</Link>
{category.children.length > 0 ? (
<div className="pb-2 pl-8 pr-5">
{category.children.map((child) => (
<Link
key={child.id}
href={child.url || "#"}
className="block py-2 text-sm text-slate-500 transition hover:text-[#2f57ff]"
onClick={() => setToggleMenu(false)}
>
{child.name}
</Link>
))}
</div>
) : null}
</div>
))}
<div className="pb-6">
{menuItems.map((category) => (
<div key={category.id} className="border-t border-slate-100 first:border-t-0">
<Link
href={category.url || "#"}
className="block px-5 py-3 text-[15px] font-medium text-slate-700 transition hover:bg-slate-50 hover:text-[#2f57ff]"
onClick={() => setToggleMenu(false)}
>
{category.name}
</Link>
{category.children.length > 0 ? (
<div className="pb-2 pl-8 pr-5">
{category.children.map((child) => (
<Link
key={child.id}
href={child.url || "#"}
className="block py-2 text-sm text-slate-500 transition hover:text-[#2f57ff]"
onClick={() => setToggleMenu(false)}
>
{child.name}
</Link>
))}
</div>
) : null}
</div>
))}
</div>
</div>
</div>
</header>
......
This diff is collapsed.
......@@ -556,7 +556,8 @@ export default function AdminBaseConfigPage() {
return (
<div className="space-y-8">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-5">
<TabsList className="h-auto rounded-2xl bg-[#eaf2ff] p-1.5">
<div className="overflow-x-auto pb-1">
<TabsList className="h-auto min-w-max rounded-2xl bg-[#eaf2ff] p-1.5">
<TabsTrigger
value="branding"
className="rounded-xl px-4 py-2.5 text-sm font-semibold text-slate-600 hover:text-[#063e8e] data-[state=active]:bg-white data-[state=active]:text-[#063e8e]"
......@@ -581,7 +582,8 @@ export default function AdminBaseConfigPage() {
>
Mạng xã hội
</TabsTrigger>
</TabsList>
</TabsList>
</div>
<TabsContent value="branding" className="mt-0">
<Card className="rounded-[30px] border-[#063e8e]/10 shadow-sm">
......@@ -636,9 +638,9 @@ export default function AdminBaseConfigPage() {
</div>
</CardHeader>
<CardContent className="space-y-6">
<CardContent className="space-y-6 px-4 sm:px-6">
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.3fr)_360px]">
<div className="rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#f8fbff] to-white p-5">
<div className="rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#f8fbff] to-white p-4 sm:p-5">
<div className="relative flex min-h-[320px] items-center justify-center overflow-hidden rounded-[24px] border border-dashed border-[#063e8e]/18 bg-[#eef4ff]">
{currentLogoMedia ? (
<div className="relative h-[220px] w-[220px]">
......@@ -658,7 +660,7 @@ export default function AdminBaseConfigPage() {
</div>
</div>
<div className="space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5">
<div className="space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5">
{currentLogo ? (
<div className="space-y-4 rounded-3xl border border-[#063e8e]/12 bg-white p-5 text-sm text-slate-600 shadow-sm">
<div>
......@@ -765,8 +767,8 @@ export default function AdminBaseConfigPage() {
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5">
<CardContent className="space-y-6 px-4 sm:px-6">
<div className="rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5">
<div className="relative aspect-[16/6] overflow-hidden rounded-[24px] border border-[#063e8e]/12 bg-[#eef4ff]">
{currentBannerMedia ? (
<SafeNextImage
......@@ -819,7 +821,7 @@ export default function AdminBaseConfigPage() {
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{sortedBanners.map((item, index) => (
<ConfigItemPreview
key={item.id}
......@@ -833,7 +835,7 @@ export default function AdminBaseConfigPage() {
</div>
{currentBanner ? (
<div className="grid gap-4 md:grid-cols-4">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div className="rounded-2xl border border-[#063e8e]/10 bg-white px-4 py-4">
<div className="text-xs uppercase tracking-[0.14em] text-gray-500">Tên banner</div>
<div className="mt-2 font-semibold text-[#163b73]">{currentBanner.name}</div>
......@@ -899,8 +901,8 @@ export default function AdminBaseConfigPage() {
</div>
</CardHeader>
<CardContent className="grid gap-6 lg:grid-cols-[360px_minmax(0,1fr)]">
<div className="space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5">
<CardContent className="grid gap-6 px-4 sm:px-6 lg:grid-cols-[360px_minmax(0,1fr)]">
<div className="space-y-4 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5">
<div className="text-sm font-semibold uppercase tracking-[0.15em] text-[#4b74b8]">
Danh sách chi nhánh
</div>
......@@ -917,7 +919,7 @@ export default function AdminBaseConfigPage() {
</div>
</div>
<div className="space-y-5 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5">
<div className="space-y-5 rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5">
{currentBranch ? (
<>
<div className="space-y-2">
......@@ -983,7 +985,7 @@ export default function AdminBaseConfigPage() {
)}
</div>
<div className="hidden rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#063e8e] to-[#0f4a9f] p-6 text-white shadow-[0_16px_30px_rgba(6,62,142,0.18)]">
<div className="hidden rounded-[28px] border border-[#063e8e]/10 bg-gradient-to-br from-[#063e8e] to-[#0f4a9f] p-6 text-white shadow-[0_16px_30px_rgba(6,62,142,0.18)] lg:block">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-white/75">
Preview chi nhánh
</div>
......@@ -1034,16 +1036,16 @@ export default function AdminBaseConfigPage() {
className="rounded-xl bg-[#163b73] text-white hover:bg-[#163b73]/90"
>
<Save className="mr-2 h-4 w-4" />
Luu cấu hình
Lưu cấu hình
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-4 px-4 sm:px-6">
{sortedSocials.map((item) => (
<div
key={item.id}
className="rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-5"
className="rounded-[28px] border border-[#063e8e]/10 bg-[#f8fbff] p-4 sm:p-5"
>
<div className="grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)_180px] lg:items-end">
<div className="flex items-center gap-3 rounded-2xl border border-[#063e8e]/10 bg-white px-4 py-4">
......
......@@ -12,15 +12,35 @@ import { useSidebarStore } from '@/hooks/use-admin-sidebar';
import { cn } from '@/lib/utils';
function AdminShell({ children }: { children: React.ReactNode }) {
const { isOpen } = useSidebarStore();
const { close, isOpen } = useSidebarStore();
React.useEffect(() => {
const mediaQuery = window.matchMedia('(max-width: 1023px)');
const syncSidebar = () => {
if (mediaQuery.matches) close();
};
syncSidebar();
mediaQuery.addEventListener('change', syncSidebar);
return () => mediaQuery.removeEventListener('change', syncSidebar);
}, [close]);
return (
<div className="min-h-screen bg-white">
<div className="min-h-screen overflow-x-hidden bg-white">
<AdminSidebar />
{isOpen ? (
<button
type="button"
aria-label="Close sidebar"
className="fixed inset-0 z-30 bg-slate-950/35 backdrop-blur-[1px] lg:hidden"
onClick={close}
/>
) : null}
<div
className={cn(
'transition-all duration-300',
isOpen ? 'pl-72' : 'pl-24',
'min-w-0 transition-all duration-300',
isOpen ? 'lg:pl-72' : 'lg:pl-24',
)}
>
<AdminHeader />
......
......@@ -52,6 +52,19 @@
--tiptap-rose: oklch(0.72 0.17 13);
}
html,
body {
max-width: 100%;
overflow-x: hidden;
}
img,
video,
canvas,
svg {
max-width: 100%;
}
@layer components {
.tiptap {
@apply border-gray-200;
......
......@@ -17,7 +17,7 @@ export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
const gridClassName =
className ??
(items.length === 3
? "grid grid-cols-1 gap-4 md:grid-cols-3"
? "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3"
: "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4");
return (
......@@ -28,9 +28,9 @@ export function AdminStatsGrid({ items, className }: AdminStatsGridProps) {
className="rounded-2xl border border-[#063e8e]/15 bg-white px-5 py-4 shadow-sm"
>
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<div className="min-w-0 space-y-2">
<p className="text-sm font-medium text-gray-700">{item.label}</p>
<div className="text-3xl font-semibold leading-none text-black">{item.value}</div>
<div className="break-words text-2xl font-semibold leading-none text-black sm:text-3xl">{item.value}</div>
</div>
{item.icon ? (
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#063e8e]/10">
......
......@@ -33,25 +33,25 @@ export function AdminTableLayout({
return (
<div className="space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 flex-col gap-3 lg:flex-row lg:items-center">
<div className="flex min-w-0 flex-1 flex-col gap-3 lg:flex-row lg:items-center">
<Input
value={searchValue}
placeholder={searchPlaceholder}
onChange={(event) => onSearchChange(event.target.value)}
className="max-w-sm rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700"
className="w-full rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700 sm:max-w-sm"
/>
{filters}
</div>
{actionLabel || actionMeta ? (
<div className="flex items-center gap-3 self-start sm:self-auto">
<div className="flex w-full flex-wrap items-center gap-3 self-start sm:w-auto sm:self-auto">
{actionMeta}
{actionLabel ? (
<Button
type="button"
disabled={actionDisabled}
onClick={onActionClick}
className="bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
className="w-full bg-[#063e8e] text-white hover:bg-[#063e8e]/90 sm:w-auto"
>
{actionIcon ?? <Plus className="mr-2 h-4 w-4" />}
{actionLabel}
......@@ -61,9 +61,9 @@ export function AdminTableLayout({
) : null}
</div>
<div className="overflow-hidden rounded-xl border border-[#063e8e]/20 bg-white shadow-sm [&_tbody_td:not(:last-child)]:border-r [&_tbody_td:not(:last-child)]:border-[#063e8e]/20 [&_thead_th:not(:last-child)]:border-r [&_thead_th:not(:last-child)]:border-white/15">
<div className="overflow-x-auto rounded-xl border border-[#063e8e]/20 bg-white shadow-sm [&_table]:min-w-[760px] [&_tbody_td:not(:last-child)]:border-r [&_tbody_td:not(:last-child)]:border-[#063e8e]/20 [&_thead_th:not(:last-child)]:border-r [&_thead_th:not(:last-child)]:border-white/15">
{children}
</div>
</div>
);
}
\ No newline at end of file
}
......@@ -67,8 +67,8 @@ export function AdminHeader() {
return (
<header className="sticky top-0 z-30 border-b border-[#063e8e]/15 bg-background/95 shadow-sm backdrop-blur supports-backdrop-filter:bg-background/80">
<div className="flex h-16 items-center justify-between px-4 lg:px-6">
<div className="flex items-center gap-4">
<div className="flex min-h-16 items-center justify-between gap-3 px-3 py-2 sm:px-4 lg:px-6">
<div className="flex min-w-0 items-center gap-2 sm:gap-4">
<Button
variant="ghost"
size="icon"
......@@ -78,11 +78,11 @@ export function AdminHeader() {
>
<Menu className="h-5 w-5" />
</Button>
<h1 className="text-xl font-bold text-[#063e8e]">{title}</h1>
<h1 className="truncate text-base font-bold text-[#063e8e] sm:text-xl">{title}</h1>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 rounded-full border border-[#063e8e]/10 bg-[#f8fbff] px-3 py-1.5 text-sm font-medium text-[#163b73]">
<div className="flex shrink-0 items-center gap-2 sm:gap-3">
<div className="hidden items-center gap-2 rounded-full border border-[#063e8e]/10 bg-[#f8fbff] px-3 py-1.5 text-sm font-medium text-[#163b73] sm:flex">
<ShieldCheck className="h-4 w-4 text-[#063e8e]" />
<span>{formatRoles(currentUser?.roles)}</span>
</div>
......@@ -93,7 +93,7 @@ export function AdminHeader() {
className="border-[#063e8e]/15 text-[#063e8e]"
>
<LogOut className="h-4 w-4" />
Đăng xuất
<span className="hidden sm:inline">Đăng xuất</span>
</Button>
</div>
</div>
......
......@@ -70,7 +70,7 @@ const membersReservedSegments = new Set(['fields', 'regions']);
export function AdminSidebar() {
const pathname = usePathname();
const { isOpen } = useSidebarStore();
const { close, isOpen } = useSidebarStore();
const [expandedGroups, setExpandedGroups] = React.useState<Record<string, boolean>>({});
const isItemActive = React.useCallback(
......@@ -93,17 +93,22 @@ export function AdminSidebar() {
const toggleGroup = (name: string) =>
setExpandedGroups((previous) => ({ ...previous, [name]: !previous[name] }));
const handleMobileNavigate = () => {
if (window.innerWidth < 1024) close();
};
return (
<aside
className={cn(
'fixed left-0 top-0 z-40 h-screen border-r border-[#063e8e]/10 bg-gradient-to-b from-[#f6f9ff] via-[#edf4ff] to-[#f8fbff] shadow-[0_18px_45px_rgba(6,62,142,0.08)] transition-all duration-300',
isOpen ? 'w-72' : 'w-24',
'fixed left-0 top-0 z-40 h-dvh border-r border-[#063e8e]/10 bg-gradient-to-b from-[#f6f9ff] via-[#edf4ff] to-[#f8fbff] shadow-[0_18px_45px_rgba(6,62,142,0.08)] transition-all duration-300',
isOpen ? 'w-72 translate-x-0 lg:w-72' : '-translate-x-full lg:w-24 lg:translate-x-0',
)}
>
<div className="flex h-full flex-col">
<div className={cn('px-4 pb-4 pt-5', !isOpen && 'px-3')}>
<Link
href="/admin/base-config"
onClick={handleMobileNavigate}
className={cn(
'flex items-center backdrop-blur-sm',
isOpen
......@@ -189,6 +194,7 @@ export function AdminSidebar() {
<Link
key={child.name}
href={child.href}
onClick={handleMobileNavigate}
className={cn(
'group relative flex rounded-2xl px-4 py-3 text-sm leading-6 transition-all',
childActive
......@@ -212,6 +218,7 @@ export function AdminSidebar() {
<Link
key={item.name}
href={item.href || '#'}
onClick={handleMobileNavigate}
title={!isOpen ? item.name : undefined}
className={cn(
'flex items-center rounded-2xl text-sm font-medium transition-all duration-200',
......@@ -233,6 +240,7 @@ export function AdminSidebar() {
<div className="rounded-[28px] border border-white/80 bg-white/95 p-4 shadow-[0_14px_32px_rgba(6,62,142,0.08)]">
<Link
href="/"
onClick={handleMobileNavigate}
className="flex items-center gap-3 text-sm font-semibold text-[#063e8e] transition hover:opacity-80"
>
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-[#edf4ff] text-[#063e8e]">
......@@ -250,6 +258,7 @@ export function AdminSidebar() {
) : (
<Link
href="/"
onClick={handleMobileNavigate}
title="Về trang chủ"
className="mx-auto flex h-14 w-14 items-center justify-center rounded-[22px] border border-white/80 bg-white/95 text-[#063e8e] shadow-sm transition hover:bg-white"
>
......
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