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

fix ui

parent 379eb4cb
...@@ -14,6 +14,36 @@ const weekDays = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"]; ...@@ -14,6 +14,36 @@ const weekDays = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"];
const formatDateTime = (value: string) => const formatDateTime = (value: string) =>
value ? dayjs(value).format("DD/MM/YYYY HH:mm") : "Đang cập nhật"; value ? dayjs(value).format("DD/MM/YYYY HH:mm") : "Đang cập nhật";
const getEventDateRange = (item: HomePostItem) => {
const startedAt = item.startedAt ? dayjs(item.startedAt) : null;
const endedAt = item.endedAt ? dayjs(item.endedAt) : null;
if (startedAt && endedAt) {
const dates: string[] = [];
let current = startedAt;
while (current.isBefore(endedAt) || current.isSame(endedAt, "day")) {
dates.push(current.format("YYYY-MM-DD"));
current = current.add(1, "day");
}
return dates;
}
if (startedAt) {
return [startedAt.format("YYYY-MM-DD")];
}
if (endedAt) {
return [endedAt.format("YYYY-MM-DD")];
}
// Fallback to registrationDeadline
if (item.registrationDeadline) {
return [dayjs(item.registrationDeadline).format("YYYY-MM-DD")];
}
return [];
};
const isTrainingEvent = (item: HomePostItem) => const isTrainingEvent = (item: HomePostItem) =>
item.categories.some((category) => { item.categories.some((category) => {
const key = `${category.name} ${category.slug} ${category.url}`.toLowerCase(); const key = `${category.name} ${category.slug} ${category.url}`.toLowerCase();
...@@ -39,19 +69,19 @@ function EventsCalendar({ ...@@ -39,19 +69,19 @@ function EventsCalendar({
}) { }) {
const today = dayjs(); const today = dayjs();
const todayKey = today.format("YYYY-MM-DD"); const todayKey = today.format("YYYY-MM-DD");
const todayMonth = today.month();
const todayYear = today.year();
const [currentMonth, setCurrentMonth] = useState(new Date()); const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDateKey, setSelectedDateKey] = useState<string | null>(null); const [selectedDateKey, setSelectedDateKey] = useState<string | null>(null);
const eventCalendarQuery = useEventCalendarPosts(currentMonth); const eventCalendarQuery = useEventCalendarPosts(currentMonth);
const monthEvents = eventCalendarQuery.data ?? []; const monthEvents = useMemo(() => eventCalendarQuery.data ?? [], [eventCalendarQuery.data]);
useEffect(() => {
const viewingCurrentMonth = const viewingCurrentMonth =
currentMonth.getMonth() === todayMonth && currentMonth.getFullYear() === todayYear; currentMonth.getMonth() === today.month() && currentMonth.getFullYear() === today.year();
const defaultSelectedKey = viewingCurrentMonth ? todayKey : null;
setSelectedDateKey(viewingCurrentMonth ? todayKey : null); // Initialize selectedDateKey when month changes
}, [currentMonth, todayKey, todayMonth, todayYear]); useEffect(() => {
setSelectedDateKey(defaultSelectedKey);
}, [defaultSelectedKey]);
const days = useMemo(() => { const days = useMemo(() => {
const monthStart = startOfMonth(currentMonth); const monthStart = startOfMonth(currentMonth);
...@@ -70,17 +100,23 @@ function EventsCalendar({ ...@@ -70,17 +100,23 @@ function EventsCalendar({
const map = new Map<string, HomePostItem[]>(); const map = new Map<string, HomePostItem[]>();
monthEvents.forEach((item) => { monthEvents.forEach((item) => {
const key = dayjs(item.registrationDeadline).format("YYYY-MM-DD"); const eventDates = getEventDateRange(item);
const existing = map.get(key) ?? [];
eventDates.forEach((dateKey) => {
const existing = map.get(dateKey) ?? [];
if (!existing.some((e) => e.id === item.id)) {
existing.push(item); existing.push(item);
}
map.set( map.set(
key, dateKey,
existing.sort( existing.sort((left, right) => {
(left, right) => const leftStart = dayjs(left.startedAt || left.endedAt || left.registrationDeadline).valueOf();
dayjs(left.registrationDeadline).valueOf() - dayjs(right.registrationDeadline).valueOf(), const rightStart = dayjs(right.startedAt || right.endedAt || right.registrationDeadline).valueOf();
), return leftStart - rightStart;
}),
); );
}); });
});
return map; return map;
}, [monthEvents]); }, [monthEvents]);
...@@ -242,7 +278,7 @@ function EventsCalendar({ ...@@ -242,7 +278,7 @@ function EventsCalendar({
</p> </p>
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#6f84aa]"> <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#6f84aa]">
<span>Hạn đăng ký: {formatDateTime(item.registrationDeadline)}</span> <span>Hạn đăng ký: {formatDateTime(item.registrationDeadline)}</span>
<span>Chi phí: {item.participationFee || "Đang cập nhật"}</span> <span>Chi phí: {item.participationFee || "Miễn phí"}</span>
</div> </div>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#6f84aa]"> <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#6f84aa]">
<span>Địa điểm: {item.location || "Đang cập nhật"}</span> <span>Địa điểm: {item.location || "Đang cập nhật"}</span>
...@@ -314,7 +350,7 @@ function EventsCalendar({ ...@@ -314,7 +350,7 @@ function EventsCalendar({
</Link> </Link>
<p> <p>
Hạn đăng ký: {formatDateTime(item.registrationDeadline)} · Chi phí:{" "} Hạn đăng ký: {formatDateTime(item.registrationDeadline)} · Chi phí:{" "}
{item.participationFee || "Đang cập nhật"} {item.participationFee || "Miễn phí"}
</p> </p>
<p>Địa điểm: {item.location || "Đang cập nhật"}</p> <p>Địa điểm: {item.location || "Đang cập nhật"}</p>
</div> </div>
......
'use client'; 'use client';
import ImageNext from "@/components/shared/image-next"; import ImageNext from "@/components/shared/image-next";
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts"; import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
...@@ -7,7 +7,8 @@ import Link from "next/link"; ...@@ -7,7 +7,8 @@ import Link from "next/link";
function Events() { function Events() {
const { eventPosts, categoryLinks, categoryNames } = useHomePosts(); const { eventPosts, categoryLinks, categoryNames } = useHomePosts();
const eventItems = eventPosts; // Reverse so newest event is first (featured card)
const eventItems = [...eventPosts].reverse();
const [featuredEvent, ...sideEvents] = eventItems; const [featuredEvent, ...sideEvents] = eventItems;
const sideSlots = Array.from({ length: 4 }, (_, index) => sideEvents[index] ?? null); const sideSlots = Array.from({ length: 4 }, (_, index) => sideEvents[index] ?? null);
const eventsLink = const eventsLink =
...@@ -47,11 +48,35 @@ function Events() { ...@@ -47,11 +48,35 @@ function Events() {
/> />
</div> </div>
<div className="p-3 pt-2.5"> <div className="flex flex-col p-3 pt-2.5">
<h3 className="line-clamp-2 text-[16px] font-extrabold uppercase leading-[1.28] text-[#22459b] md:text-[18px]"> <h3 className="text-[16px] font-bold uppercase leading-[1.28] text-[#22459b] line-clamp-2 md:text-[18px]">
{featuredEvent.title} {featuredEvent.title}
</h3> </h3>
<p className="mt-1.5 text-[13px] text-[#90a0bd]"> {(() => {
const rawText = featuredEvent.contentText || featuredEvent.summary || "";
const textOnly = rawText
.replace(/\[caption[^\]]*\].*?\[\/caption\]/gi, "")
.replace(/<figure[^>]*>.*?<\/figure>/gi, "")
.replace(/<img[^>]*>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/"/g, '"')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/–/g, "–")
.replace(/\s+/g, " ")
.trim();
return textOnly.length > 10 ? (
<p className="mt-2 line-clamp-1 text-[13px] leading-normal text-[#5f6f86]">
{textOnly.substring(0, 150)}
</p>
) : null;
})()}
<p className="mt-auto pt-2 text-[13px] text-[#90a0bd]">
{dayjs( {dayjs(
featuredEvent.startedAt || featuredEvent.publishedAt || featuredEvent.createdAt, featuredEvent.startedAt || featuredEvent.publishedAt || featuredEvent.createdAt,
).format("DD/MM/YYYY")} ).format("DD/MM/YYYY")}
...@@ -87,9 +112,33 @@ function Events() { ...@@ -87,9 +112,33 @@ function Events() {
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<h4 className="line-clamp-2 text-[15px] font-semibold leading-[1.35] text-white"> <h4 className="line-clamp-1 text-[15px] font-semibold leading-[1.35] text-white">
{item.title} {item.title}
</h4> </h4>
{(() => {
const rawText = item.contentText || item.summary || "";
const textOnly = rawText
.replace(/\[caption[^\]]*\].*?\[\/caption\]/gi, "")
.replace(/<figure[^>]*>.*?<\/figure>/gi, "")
.replace(/<img[^>]*>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/"/g, '"')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/–/g, "–")
.replace(/\s+/g, " ")
.trim();
return textOnly.length > 10 ? (
<p className="mt-1 line-clamp-1 text-[12px] text-white/78">
{textOnly.substring(0, 80)}
</p>
) : null;
})()}
<p className="mt-1 text-[12px] text-white/78"> <p className="mt-1 text-[12px] text-white/78">
{dayjs(item.startedAt || item.publishedAt || item.createdAt).format( {dayjs(item.startedAt || item.publishedAt || item.createdAt).format(
"DD/MM/YYYY", "DD/MM/YYYY",
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import ImageNext from "@/components/shared/image-next"; import ImageNext from "@/components/shared/image-next";
import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts"; import { useHomePosts } from "@/app/(main)/(home)/lib/use-home-posts";
import stripImagesAndHtml from "@/helpers/stripImageAndHtml";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Link from "next/link"; import Link from "next/link";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
...@@ -76,7 +75,7 @@ function News() { ...@@ -76,7 +75,7 @@ function News() {
href={featuredArticle.externalLink} href={featuredArticle.externalLink}
className="block h-full overflow-hidden rounded-[22px] border border-[#dbe4f2] bg-white shadow-[0_8px_24px_rgba(31,59,124,0.08)]" className="block h-full overflow-hidden rounded-[22px] border border-[#dbe4f2] bg-white shadow-[0_8px_24px_rgba(31,59,124,0.08)]"
> >
<div className="aspect-[1.75/1] overflow-hidden"> <div className="aspect-[1.4/1] overflow-hidden">
<ImageNext <ImageNext
src={featuredArticle.thumbnail?.url ?? "/thumbnail.png"} src={featuredArticle.thumbnail?.url ?? "/thumbnail.png"}
alt={featuredArticle.thumbnail?.alt || featuredArticle.title} alt={featuredArticle.thumbnail?.alt || featuredArticle.title}
...@@ -95,8 +94,27 @@ function News() { ...@@ -95,8 +94,27 @@ function News() {
{featuredArticle.title} {featuredArticle.title}
</h3> </h3>
<p className="line-clamp-2 text-[13px] leading-[1.45] text-[#6c7b96]"> <p className="line-clamp-3 text-[13px] leading-[1.45] text-[#6c7b96]">
{stripImagesAndHtml(featuredArticle.summary)} {(() => {
const rawText = featuredArticle.contentText || featuredArticle.summary || "";
const textOnly = rawText
.replace(/\[caption[^\]]*\].*?\[\/caption\]/gi, "")
.replace(/<figure[^>]*>.*?<\/figure>/gi, "")
.replace(/<img[^>]*>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/"/g, '"')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/–/g, "–")
.replace(/\s+/g, " ")
.trim();
return textOnly || "-";
})()}
</p> </p>
<p className="text-[14px] text-[#8a9bb6]"> <p className="text-[14px] text-[#8a9bb6]">
...@@ -129,9 +147,33 @@ function News() { ...@@ -129,9 +147,33 @@ function News() {
href={news.externalLink} href={news.externalLink}
className="block rounded-[18px] border border-[#dbe4f2] bg-white px-4 py-2.5 shadow-[0_8px_24px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_28px_rgba(31,59,124,0.12)] xl:flex-1" className="block rounded-[18px] border border-[#dbe4f2] bg-white px-4 py-2.5 shadow-[0_8px_24px_rgba(31,59,124,0.08)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_28px_rgba(31,59,124,0.12)] xl:flex-1"
> >
<h4 className="line-clamp-2 text-[15px] font-bold leading-[1.28] text-[#21408f]"> <h4 className="line-clamp-1 text-[15px] font-bold leading-[1.28] text-[#21408f]">
{news.title} {news.title}
</h4> </h4>
{(() => {
const rawText = news.contentText || news.summary || "";
const textOnly = rawText
.replace(/\[caption[^\]]*\].*?\[\/caption\]/gi, "")
.replace(/<figure[^>]*>.*?<\/figure>/gi, "")
.replace(/<img[^>]*>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/"/g, '"')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/–/g, "–")
.replace(/\s+/g, " ")
.trim();
return textOnly.length > 10 ? (
<p className="mt-1.5 line-clamp-2 text-[13px] leading-normal text-[#6c7b96]">
{textOnly}
</p>
) : null;
})()}
<p className="mt-1 text-[13px] text-[#8a9bb6]"> <p className="mt-1 text-[13px] text-[#8a9bb6]">
{dayjs(news.publishedAt || news.createdAt).format("DD/MM/YYYY")} {dayjs(news.publishedAt || news.createdAt).format("DD/MM/YYYY")}
</p> </p>
......
...@@ -42,6 +42,11 @@ type RawHomePost = { ...@@ -42,6 +42,11 @@ type RawHomePost = {
type?: string | null; type?: string | null;
categories?: RawHomeCategory[] | null; categories?: RawHomeCategory[] | null;
thumbnail?: RawHomeThumbnail | null; thumbnail?: RawHomeThumbnail | null;
content_structure?: {
post_content?: Array<{
content?: string | null;
}> | null;
} | null;
}; };
type HomeEnvelope<T> = { type HomeEnvelope<T> = {
...@@ -65,6 +70,7 @@ export type HomePostItem = { ...@@ -65,6 +70,7 @@ export type HomePostItem = {
title: string; title: string;
externalLink: string; externalLink: string;
summary: string; summary: string;
contentText: string;
createdAt: string; createdAt: string;
publishedAt: string; publishedAt: string;
startedAt: string; startedAt: string;
...@@ -262,16 +268,14 @@ function createCategoryPostsQuery(categoryId: string, pageSize: string) { ...@@ -262,16 +268,14 @@ function createCategoryPostsQuery(categoryId: string, pageSize: string) {
} }
function createEventCalendarQuery(currentMonth: Date) { function createEventCalendarQuery(currentMonth: Date) {
const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1); // We'll filter by date on client-side for more flexibility
const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0, 23, 59, 59, 999);
return new URLSearchParams({ return new URLSearchParams({
sortField: "registration_deadline", page: "1",
pageSize: "100",
sortField: "started_at",
sortOrder: "asc", sortOrder: "asc",
filters: [ filters: [
`category.id==(${HOME_CATEGORY_IDS.suKien}|${HOME_CATEGORY_IDS.daoTao})`, `category.id==(${HOME_CATEGORY_IDS.suKien}|${HOME_CATEGORY_IDS.daoTao})`,
`registration_deadline>=${dayjs(monthStart).format("YYYY-MM-DD HH:mm:ss")}`,
`registration_deadline<=${dayjs(monthEnd).format("YYYY-MM-DD HH:mm:ss")}`,
"is_hidden==false", "is_hidden==false",
"is_active==true", "is_active==true",
"type==news", "type==news",
...@@ -375,6 +379,12 @@ async function fetchHomePostsFromApi() { ...@@ -375,6 +379,12 @@ async function fetchHomePostsFromApi() {
title, title,
externalLink, externalLink,
summary: String(item.summary ?? item.content ?? ""), summary: String(item.summary ?? item.content ?? ""),
contentText: String(
item.content_structure?.post_content?.[0]?.content ??
item.summary ??
item.content ??
""
),
createdAt: String(item.created_at ?? ""), createdAt: String(item.created_at ?? ""),
publishedAt: String(item.published_at ?? item.release_at ?? item.created_at ?? ""), publishedAt: String(item.published_at ?? item.release_at ?? item.created_at ?? ""),
startedAt: String(item.started_at ?? ""), startedAt: String(item.started_at ?? ""),
...@@ -567,7 +577,11 @@ export function useEventCalendarPosts(currentMonth: Date) { ...@@ -567,7 +577,11 @@ export function useEventCalendarPosts(currentMonth: Date) {
const response = await useCustomClient<HomeEnvelope<HomePagedResult<RawHomePost>>>( const response = await useCustomClient<HomeEnvelope<HomePagedResult<RawHomePost>>>(
`/post?${queryParams.toString()}`, `/post?${queryParams.toString()}`,
); );
return (response.responseData?.rows ?? []).map((item) => {
const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0, 23, 59, 59, 999);
const mappedPosts = (response.responseData?.rows ?? []).map((item) => {
const categories = (item.categories ?? []) const categories = (item.categories ?? [])
.filter((category) => category?.id && category?.name) .filter((category) => category?.id && category?.name)
.map((category) => ({ .map((category) => ({
...@@ -591,6 +605,12 @@ export function useEventCalendarPosts(currentMonth: Date) { ...@@ -591,6 +605,12 @@ export function useEventCalendarPosts(currentMonth: Date) {
title, title,
externalLink, externalLink,
summary: String(item.summary ?? item.content ?? ""), summary: String(item.summary ?? item.content ?? ""),
contentText: String(
item.content_structure?.post_content?.[0]?.content ??
item.summary ??
item.content ??
""
),
createdAt: String(item.created_at ?? ""), createdAt: String(item.created_at ?? ""),
publishedAt: String(item.published_at ?? item.release_at ?? item.created_at ?? ""), publishedAt: String(item.published_at ?? item.release_at ?? item.created_at ?? ""),
startedAt: String(item.started_at ?? ""), startedAt: String(item.started_at ?? ""),
...@@ -613,6 +633,41 @@ export function useEventCalendarPosts(currentMonth: Date) { ...@@ -613,6 +633,41 @@ export function useEventCalendarPosts(currentMonth: Date) {
: null, : null,
} satisfies HomePostItem; } satisfies HomePostItem;
}); });
// Filter posts that have at least one date (startedAt, endedAt, or registrationDeadline)
// falling within the current month
return mappedPosts.filter((item) => {
const startedAt = item.startedAt ? dayjs(item.startedAt) : null;
const endedAt = item.endedAt ? dayjs(item.endedAt) : null;
const registrationDeadline = item.registrationDeadline ? dayjs(item.registrationDeadline) : null;
// If no dates at all, exclude
if (!startedAt && !endedAt && !registrationDeadline) return false;
const monthStartDay = dayjs(monthStart);
const monthEndDay = dayjs(monthEnd);
// Check if any date falls within the current month
const hasDateInMonth = (date: dayjs.Dayjs | null): boolean => {
return date !== null && !date.isBefore(monthStartDay, "day") && !date.isAfter(monthEndDay, "day");
};
// Check if event overlaps with current month (spans across the month)
const eventStartDate = startedAt || registrationDeadline;
const eventEndDate = endedAt || registrationDeadline || startedAt;
if (eventStartDate && eventEndDate) {
// Event overlaps with month if it starts before month end AND ends after month start
return !eventStartDate.isAfter(monthEndDay, "day") && !eventEndDate.isBefore(monthStartDay, "day");
}
// Fallback: check individual dates
if (startedAt && hasDateInMonth(startedAt)) return true;
if (endedAt && hasDateInMonth(endedAt)) return true;
if (registrationDeadline && hasDateInMonth(registrationDeadline)) return true;
return false;
});
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn("[useEventCalendarPosts] CMS unavailable, falling back to mock data", error); console.warn("[useEventCalendarPosts] CMS unavailable, falling back to mock data", error);
......
...@@ -5,10 +5,111 @@ import ImageNext from "@/components/shared/image-next"; ...@@ -5,10 +5,111 @@ import ImageNext from "@/components/shared/image-next";
import AppEditorContent from "@/components/shared/editor-content"; import AppEditorContent from "@/components/shared/editor-content";
import ListCategory from "@/components/base/list-category"; import ListCategory from "@/components/base/list-category";
import EventsCalendar from "@/app/(main)/(home)/components/events-calendar"; import EventsCalendar from "@/app/(main)/(home)/components/events-calendar";
import { Calendar, MapPin, Clock, DollarSign, Users, CreditCard } from "lucide-react";
import { buildDynamicCategoryMenu, findDisplayCategoryForPost } from "./data"; import { buildDynamicCategoryMenu, findDisplayCategoryForPost } from "./data";
import StructuredPostContent from "./StructuredPostContent"; import StructuredPostContent from "./StructuredPostContent";
import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types"; import type { DynamicCategoryRouteItem, DynamicPostItem } from "./types";
const formatDate = (value: string | null) =>
value ? dayjs(value).format("DD/MM/YYYY") : "";
const formatDateTime = (value: string | null) =>
value ? dayjs(value).format("DD/MM/YYYY HH:mm") : "";
const isEventOrTraining = (post: DynamicPostItem) => {
const eventCategories = ["Sự kiện", "Đào tạo", "su-kien", "dao-tao", "su_kien", "dao_tao"];
return post.categories.some((cat) =>
eventCategories.some(
(key) =>
cat.name.toLowerCase().includes(key.toLowerCase()) ||
(cat.slug && cat.slug.toLowerCase().includes(key.toLowerCase()))
)
);
};
const hasEventInfo = (post: DynamicPostItem) => {
return (
post.started_at ||
post.ended_at ||
post.registration_deadline ||
post.location
);
};
const EventInfoCard = ({ post }: { post: DynamicPostItem }) => {
if (!isEventOrTraining(post) || !hasEventInfo(post)) return null;
const startedAt = post.started_at;
const endedAt = post.ended_at;
const registrationDeadline = post.registration_deadline;
return (
<div className="mt-7 rounded-2xl border border-[#e3ebf8] bg-linear-to-br from-[#f8faff] to-white p-5 shadow-[0_8px_24px_rgba(36,70,156,0.1)]">
<div className="mb-4 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#24469c]">
<Calendar className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-lg font-bold text-[#24469c]">Thông tin sự kiện</h3>
<p className="text-xs text-[#7f8eab]">Event Information</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Row 1: Hạn đăng ký | Chi phí */}
{registrationDeadline && (
<div className="rounded-xl bg-white p-3 shadow-sm">
<div className="flex items-center gap-2 text-[#f5a400]">
<Users className="h-4 w-4" />
<span className="text-xs font-medium uppercase tracking-wide">Hạn đăng ký</span>
</div>
<p className="mt-1 text-sm font-semibold text-[#1f3768]">
{formatDateTime(registrationDeadline)}
</p>
</div>
)}
<div className="rounded-xl bg-white p-3 shadow-sm">
<div className="flex items-center gap-2 text-[#24469c]">
<CreditCard className="h-4 w-4" />
<span className="text-xs font-medium uppercase tracking-wide">Chi phí</span>
</div>
<p className="mt-1 text-sm font-semibold text-[#1f3768]">
{post.participation_fee || "Miễn phí"}
</p>
</div>
{/* Row 2: Ngày bắt đầu/kết thúc | Địa điểm */}
<div className="rounded-xl bg-white p-3 shadow-sm">
<div className="flex items-center gap-2 text-[#24469c]">
<Clock className="h-4 w-4" />
<span className="text-xs font-medium uppercase tracking-wide">Thời gian</span>
</div>
<p className="mt-1 text-sm font-semibold text-[#1f3768]">
{startedAt
? endedAt
? `${formatDate(startedAt)} - ${formatDate(endedAt)}`
: formatDate(startedAt)
: endedAt
? formatDate(endedAt)
: "-"}
</p>
</div>
<div className="rounded-xl bg-white p-3 shadow-sm">
<div className="flex items-center gap-2 text-[#e22f5a]">
<MapPin className="h-4 w-4" />
<span className="text-xs font-medium uppercase tracking-wide">Địa điểm</span>
</div>
<p className="mt-1 text-sm font-semibold text-[#1f3768] line-clamp-2">
{post.location || "-"}
</p>
</div>
</div>
</div>
);
};
type ArticleDetailPageProps = { type ArticleDetailPageProps = {
post: DynamicPostItem; post: DynamicPostItem;
category: DynamicCategoryRouteItem | null; category: DynamicCategoryRouteItem | null;
...@@ -53,6 +154,8 @@ export default function ArticleDetailPage({ ...@@ -53,6 +154,8 @@ export default function ArticleDetailPage({
</div> </div>
) : null} ) : null}
<EventInfoCard post={post} />
<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="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"> <div className="article-detail-content prose tiptap max-w-none overflow-hidden">
<StructuredPostContent post={post} /> <StructuredPostContent post={post} />
......
...@@ -20,6 +20,7 @@ type CategoryListResponse = { ...@@ -20,6 +20,7 @@ type CategoryListResponse = {
type RawPostCategory = { type RawPostCategory = {
id?: string | null; id?: string | null;
name?: string | null; name?: string | null;
slug?: string | null;
url?: string | null; url?: string | null;
type?: string | null; type?: string | null;
}; };
...@@ -56,6 +57,8 @@ type RawPostItem = { ...@@ -56,6 +57,8 @@ type RawPostItem = {
ended_at?: string | null; ended_at?: string | null;
expired_at?: string | null; expired_at?: string | null;
registration_deadline?: string | null; registration_deadline?: string | null;
location?: string | null;
participation_fee?: string | null;
is_featured?: boolean | null; is_featured?: boolean | null;
is_hidden?: boolean | null; is_hidden?: boolean | null;
is_active?: boolean | null; is_active?: boolean | null;
...@@ -216,6 +219,8 @@ const mapPost = (item: RawPostItem): DynamicPostItem => ({ ...@@ -216,6 +219,8 @@ const mapPost = (item: RawPostItem): DynamicPostItem => ({
ended_at: item.ended_at ?? null, ended_at: item.ended_at ?? null,
expired_at: item.expired_at ?? null, expired_at: item.expired_at ?? null,
registration_deadline: item.registration_deadline ?? null, registration_deadline: item.registration_deadline ?? null,
location: item.location ?? null,
participation_fee: item.participation_fee ?? null,
is_featured: Boolean(item.is_featured), is_featured: Boolean(item.is_featured),
is_hidden: Boolean(item.is_hidden), is_hidden: Boolean(item.is_hidden),
is_active: item.is_active !== false, is_active: item.is_active !== false,
...@@ -227,6 +232,7 @@ const mapPost = (item: RawPostItem): DynamicPostItem => ({ ...@@ -227,6 +232,7 @@ const mapPost = (item: RawPostItem): DynamicPostItem => ({
.map((category) => ({ .map((category) => ({
id: String(category.id), id: String(category.id),
name: String(category.name), name: String(category.name),
slug: String(category.slug ?? ""),
url: normalizePath(category.url), url: normalizePath(category.url),
type: String(category.type ?? ""), type: String(category.type ?? ""),
})), })),
......
'use client'; 'use client';
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { ArrowDownToLine, Globe2, MapPinned, Newspaper, TrendingUp } from "lucide-react"; import { Globe2, Newspaper, TrendingUp } from "lucide-react";
import ImageNext from "@/components/shared/image-next"; import ImageNext from "@/components/shared/image-next";
import type { DynamicPostItem } from "../types"; import type { DynamicPostItem } from "../types";
...@@ -35,14 +35,14 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -35,14 +35,14 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "dong-nam-a", key: "dong-nam-a",
label: "Đông Nam Á", label: "Đông Nam Á",
title: "Đông Nam Á", title: "Đông Nam Á",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Dong-Nam-A-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Dong-Nam-A-scaled.jpg",
imageAlt: "Bản đồ thị trường Đông Nam Á", imageAlt: "Bản đồ thị trường Đông Nam Á",
description: description:
"Khu vực trọng điểm dành cho doanh nghiệp theo dõi cơ hội thương mại, xuất nhập khẩu, chuỗi cung ứng và kết nối đối tác trong ASEAN.", "Khu vực trọng điểm dành cho doanh nghiệp theo dõi cơ hội thương mại, xuất nhập khẩu, chuỗi cung ứng và kết nối đối tác trong ASEAN.",
markets: [ markets: [
{ {
name: "Việt Nam", name: "Việt Nam",
href: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/VN-factsheet.pdf", href: "/ho-so-thi-truong/tai-lieu/VN-factsheet.pdf",
tone: "bg-[#da251d]", tone: "bg-[#da251d]",
}, },
{ name: "Lào", href: "#", tone: "bg-[#002868]" }, { name: "Lào", href: "#", tone: "bg-[#002868]" },
...@@ -52,7 +52,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -52,7 +52,7 @@ const REGION_CONFIGS: RegionConfig[] = [
{ name: "Malaysia", href: "#", tone: "bg-[#c00]" }, { name: "Malaysia", href: "#", tone: "bg-[#c00]" },
{ {
name: "Singapore", name: "Singapore",
href: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/SINGAPORE-2020.pdf", href: "/ho-so-thi-truong/tai-lieu/SINGAPORE-2020.pdf",
tone: "bg-[#df0000]", tone: "bg-[#df0000]",
}, },
{ name: "Philippines", href: "#", tone: "bg-[#0038a8]" }, { name: "Philippines", href: "#", tone: "bg-[#0038a8]" },
...@@ -61,7 +61,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -61,7 +61,7 @@ const REGION_CONFIGS: RegionConfig[] = [
], ],
featuredDocument: { featuredDocument: {
title: "Factsheet Việt Nam", title: "Factsheet Việt Nam",
href: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/VN-factsheet.pdf", href: "/ho-so-thi-truong/tai-lieu/VN-factsheet.pdf",
description: "Mở tài liệu tham khảo", description: "Mở tài liệu tham khảo",
}, },
}, },
...@@ -69,7 +69,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -69,7 +69,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "dong-bac-a", key: "dong-bac-a",
label: "Đông Bắc Á", label: "Đông Bắc Á",
title: "Đông Bắc Á", title: "Đông Bắc Á",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Dong-Bac-A-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Dong-Bac-A-scaled.jpg",
imageAlt: "Khu vực Đông Bắc Á", imageAlt: "Khu vực Đông Bắc Á",
description: description:
"Nhóm thị trường phù hợp để doanh nghiệp tiếp cận chuỗi giá trị công nghiệp, công nghệ, logistics và thương mại khu vực Đông Bắc Á.", "Nhóm thị trường phù hợp để doanh nghiệp tiếp cận chuỗi giá trị công nghiệp, công nghệ, logistics và thương mại khu vực Đông Bắc Á.",
...@@ -85,7 +85,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -85,7 +85,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "nam-a", key: "nam-a",
label: "Nam Á", label: "Nam Á",
title: "Nam Á", title: "Nam Á",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/d60bf053ad586e063749-scaled.jpg", image: "/ho-so-thi-truong/d60bf053ad586e063749-scaled.jpg",
imageAlt: "Khu vực Nam Á", imageAlt: "Khu vực Nam Á",
description: description:
"Không gian thị trường giàu tiềm năng với dân số lớn, tốc độ đô thị hóa nhanh và nhu cầu hợp tác thương mại đa ngành.", "Không gian thị trường giàu tiềm năng với dân số lớn, tốc độ đô thị hóa nhanh và nhu cầu hợp tác thương mại đa ngành.",
...@@ -101,7 +101,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -101,7 +101,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "tay-a", key: "tay-a",
label: "Tây Á", label: "Tây Á",
title: "Tây Á", title: "Tây Á",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/33a2e0fdbdf67ea827e7-scaled.jpg", image: "/ho-so-thi-truong/33a2e0fdbdf67ea827e7-scaled.jpg",
imageAlt: "Khu vực Tây Á", imageAlt: "Khu vực Tây Á",
description: description:
"Thị trường phù hợp với định hướng mở rộng đối tác năng lượng, xây dựng, thương mại dịch vụ và kết nối trung chuyển.", "Thị trường phù hợp với định hướng mở rộng đối tác năng lượng, xây dựng, thương mại dịch vụ và kết nối trung chuyển.",
...@@ -117,7 +117,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -117,7 +117,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "bac-my", key: "bac-my",
label: "Bắc Mỹ", label: "Bắc Mỹ",
title: "Bắc Mỹ", title: "Bắc Mỹ",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Bac-My-1-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Bac-My-1-scaled.jpg",
imageAlt: "Khu vực Bắc Mỹ", imageAlt: "Khu vực Bắc Mỹ",
description: description:
"Thị trường quy mô lớn, yêu cầu cao về tiêu chuẩn, truy xuất nguồn gốc và chiến lược tiếp cận bài bản.", "Thị trường quy mô lớn, yêu cầu cao về tiêu chuẩn, truy xuất nguồn gốc và chiến lược tiếp cận bài bản.",
...@@ -131,7 +131,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -131,7 +131,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "nam-my", key: "nam-my",
label: "Nam Mỹ", label: "Nam Mỹ",
title: "Nam Mỹ", title: "Nam Mỹ",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Nam-My-1-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Nam-My-1-scaled.jpg",
imageAlt: "Khu vực Nam Mỹ", imageAlt: "Khu vực Nam Mỹ",
description: description:
"Nhóm thị trường phù hợp để theo dõi nhu cầu hàng tiêu dùng, nông sản, logistics biển và liên kết chuỗi cung ứng mới.", "Nhóm thị trường phù hợp để theo dõi nhu cầu hàng tiêu dùng, nông sản, logistics biển và liên kết chuỗi cung ứng mới.",
...@@ -146,7 +146,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -146,7 +146,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "chau-au", key: "chau-au",
label: "Châu Âu", label: "Châu Âu",
title: "Châu Âu", title: "Châu Âu",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Chau-Au-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Chau-Au-scaled.jpg",
imageAlt: "Khu vực Châu Âu", imageAlt: "Khu vực Châu Âu",
description: description:
"Khu vực trọng tâm cho doanh nghiệp quan tâm đến EVFTA, tiêu chuẩn xanh, phát triển bền vững và thị trường giá trị cao.", "Khu vực trọng tâm cho doanh nghiệp quan tâm đến EVFTA, tiêu chuẩn xanh, phát triển bền vững và thị trường giá trị cao.",
...@@ -162,7 +162,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -162,7 +162,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "chau-uc", key: "chau-uc",
label: "Châu Úc", label: "Châu Úc",
title: "Châu Úc", title: "Châu Úc",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Chau-Uc-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Chau-Uc-scaled.jpg",
imageAlt: "Khu vực Châu Úc", imageAlt: "Khu vực Châu Úc",
description: description:
"Phù hợp với chiến lược tìm hiểu nhu cầu nhập khẩu ổn định, tiêu chuẩn chất lượng cao và hợp tác thương mại dài hạn.", "Phù hợp với chiến lược tìm hiểu nhu cầu nhập khẩu ổn định, tiêu chuẩn chất lượng cao và hợp tác thương mại dài hạn.",
...@@ -176,7 +176,7 @@ const REGION_CONFIGS: RegionConfig[] = [ ...@@ -176,7 +176,7 @@ const REGION_CONFIGS: RegionConfig[] = [
key: "chau-phi", key: "chau-phi",
label: "Châu Phi", label: "Châu Phi",
title: "Châu Phi", title: "Châu Phi",
image: "https://vcci-hcm.org.vn/wp-content/uploads/2022/06/Chau-Phi-1-scaled.jpg", image: "/ho-so-thi-truong/ban-do/Chau-Phi-1-scaled.jpg",
imageAlt: "Khu vực Châu Phi", imageAlt: "Khu vực Châu Phi",
description: description:
"Khu vực giàu dư địa tiếp cận thị trường mới cho hàng tiêu dùng, nông sản, vật liệu và hợp tác thương mại song phương.", "Khu vực giàu dư địa tiếp cận thị trường mới cho hàng tiêu dùng, nông sản, vật liệu và hợp tác thương mại song phương.",
......
...@@ -131,7 +131,7 @@ export default function MemberBenefitsPage() { ...@@ -131,7 +131,7 @@ export default function MemberBenefitsPage() {
<div> <div>
<div className="flex items-center gap-2 text-[#f5c21b]"> <div className="flex items-center gap-2 text-[#f5c21b]">
<Phone className="h-4 w-4" /> <Phone className="h-4 w-4" />
<span className="font-semibold">Phòng Hội viên và Đào tạo</span> <span className="font-semibold">Phòng Hội viên Đào tạo và Truyền thông</span>
</div> </div>
<p className="mt-1">C. Thanh Thủy</p> <p className="mt-1">C. Thanh Thủy</p>
<p>ĐT: 0903 909 796</p> <p>ĐT: 0903 909 796</p>
...@@ -160,9 +160,7 @@ export default function MemberBenefitsPage() { ...@@ -160,9 +160,7 @@ export default function MemberBenefitsPage() {
<span className="font-semibold">Địa chỉ</span> <span className="font-semibold">Địa chỉ</span>
</div> </div>
<p className="mt-1"> <p className="mt-1">
P. 306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, P. 306, Lầu 3, Tòa nhà VCCI, 171 Võ Thị Sáu, Phường Xuân Hoà, TP. HCM
<br />
Phường Xuân Hoà, TP. HCM
</p> </p>
</div> </div>
</div> </div>
......
...@@ -87,7 +87,7 @@ export default function MemberRegistrationPage({ post }: MemberRegistrationPageP ...@@ -87,7 +87,7 @@ export default function MemberRegistrationPage({ post }: MemberRegistrationPageP
<div className="mt-4 space-y-1 text-[18px] leading-9 text-[#1f2a44]"> <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 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>C. Thúy – ĐD: 0903 909 756</p>
<p>Email: luuthanhthuy72@yahoo.com; hoivien@vcci-hcm.org.vn;</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>Đ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> <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>
......
...@@ -19,6 +19,7 @@ export type DynamicCategoryMenuItem = { ...@@ -19,6 +19,7 @@ export type DynamicCategoryMenuItem = {
export type DynamicPostCategoryItem = { export type DynamicPostCategoryItem = {
id: string; id: string;
name: string; name: string;
slug: string;
url: string; url: string;
type: string; type: string;
}; };
...@@ -63,6 +64,8 @@ export type DynamicPostItem = { ...@@ -63,6 +64,8 @@ export type DynamicPostItem = {
ended_at: string | null; ended_at: string | null;
expired_at: string | null; expired_at: string | null;
registration_deadline: string | null; registration_deadline: string | null;
location: string | null;
participation_fee: string | null;
is_featured: boolean; is_featured: boolean;
is_hidden: boolean; is_hidden: boolean;
is_active: boolean; is_active: boolean;
......
...@@ -379,12 +379,12 @@ export default function HeaderCategoryPostsPage() { ...@@ -379,12 +379,12 @@ export default function HeaderCategoryPostsPage() {
{ {
kind: item.is_hidden ? "hidden" : "visible", kind: item.is_hidden ? "hidden" : "visible",
label: item.is_hidden label: item.is_hidden
? "B\u00e0i vi\u1ebft \u0111ang \u1ea9n" ? "Bài viết đang ẩn"
: "B\u00e0i vi\u1ebft \u0111ang hi\u1ec3n th\u1ecb", : "Bài viết đang hiển thị",
}, },
{ {
kind: "edit", kind: "edit",
label: "Ch\u1ec9nh s\u1eeda b\u00e0i vi\u1ebft", label: "Chỉnh sửa bài viết",
onClick: () => onClick: () =>
router.push( router.push(
`/admin/header-config/${categoryId}/posts/${item.id}?returnTo=${encodeURIComponent(listPath)}`, `/admin/header-config/${categoryId}/posts/${item.id}?returnTo=${encodeURIComponent(listPath)}`,
...@@ -392,7 +392,7 @@ export default function HeaderCategoryPostsPage() { ...@@ -392,7 +392,7 @@ export default function HeaderCategoryPostsPage() {
}, },
{ {
kind: "delete", kind: "delete",
label: "X\u00f3a b\u00e0i vi\u1ebft", label: "Xóa bài viết",
onClick: () => setDeleteTarget(item), onClick: () => setDeleteTarget(item),
}, },
]} ]}
...@@ -407,9 +407,8 @@ export default function HeaderCategoryPostsPage() { ...@@ -407,9 +407,8 @@ export default function HeaderCategoryPostsPage() {
{totalPages > 1 && ( {totalPages > 1 && (
<div className="flex items-center justify-between border-t border-[#063e8e]/10 px-4 py-3"> <div className="flex items-center justify-between border-t border-[#063e8e]/10 px-4 py-3">
<div className="text-sm text-gray-700"> <div className="text-sm text-gray-700">
{"Hi\u1ec3n th\u1ecb"} {(page - 1) * PAGE_SIZE + 1} {"\u0111\u1ebfn"}{" "} Hiển thị {(page - 1) * PAGE_SIZE + 1} đến{" "}
{Math.min(page * PAGE_SIZE, total)} {"c\u1ee7a"}{" "} {Math.min(page * PAGE_SIZE, total)} của {total} bài viết
{total} {"b\u00e0i vi\u1ebft"}
</div> </div>
<Pagination page={page} pageCount={totalPages} onChangePage={handlePageChange} /> <Pagination page={page} pageCount={totalPages} onChangePage={handlePageChange} />
</div> </div>
......
...@@ -16,6 +16,7 @@ export type HomePostItem = { ...@@ -16,6 +16,7 @@ export type HomePostItem = {
title: string; title: string;
externalLink: string; externalLink: string;
summary: string; summary: string;
contentText: string;
createdAt: string; createdAt: string;
publishedAt: string; publishedAt: string;
startedAt: string; startedAt: string;
...@@ -111,6 +112,7 @@ const buildPost = (params: BuildPostParams): HomePostItem => ({ ...@@ -111,6 +112,7 @@ const buildPost = (params: BuildPostParams): HomePostItem => ({
title: params.title, title: params.title,
externalLink: `/bai-viet/${params.id}`, externalLink: `/bai-viet/${params.id}`,
summary: params.summary, summary: params.summary,
contentText: params.content,
createdAt: params.publishedAt, createdAt: params.publishedAt,
publishedAt: params.publishedAt, publishedAt: params.publishedAt,
startedAt: params.startedAt ?? "", startedAt: params.startedAt ?? "",
......
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