Skip to content

Commit 4b1f8f3

Browse files
authored
Merge pull request #92 from hammercode-dev/feat/event-markdown-description
feat: config mdx component for client rendering text markdown
2 parents 9253e2a + 257e915 commit 4b1f8f3

8 files changed

Lines changed: 178 additions & 94 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { FC } from "react";
4+
import { MDXRemote } from "next-mdx-remote";
5+
import { Skeleton } from "@/components/ui/Skeleton";
6+
import { useMDX } from "@/hooks/useMDX";
7+
import { cn } from "@/lib/utils";
8+
9+
interface MDXContentProps {
10+
content?: string;
11+
className?: string;
12+
fallbackText?: string;
13+
showSkeleton?: boolean;
14+
skeletonHeight?: string;
15+
theme?: string;
16+
parseFrontmatter?: boolean;
17+
}
18+
19+
const MDXContent: FC<MDXContentProps> = ({
20+
content,
21+
className,
22+
fallbackText,
23+
showSkeleton = true,
24+
skeletonHeight = "h-20",
25+
theme = "github-dark",
26+
parseFrontmatter = true,
27+
}) => {
28+
const { mdxSource, isLoading } = useMDX(content, {
29+
theme,
30+
parseFrontmatter,
31+
});
32+
33+
if (isLoading && showSkeleton) {
34+
return <Skeleton className={cn(skeletonHeight, "w-full", className)} />;
35+
}
36+
37+
if (!mdxSource) {
38+
return (
39+
<div className={cn("prose prose-sm dark:prose-invert max-w-none", className)}>
40+
<p>{fallbackText || content}</p>
41+
</div>
42+
);
43+
}
44+
45+
return (
46+
<div className={cn("prose prose-sm dark:prose-invert max-w-none", className)}>
47+
<MDXRemote {...mdxSource} />
48+
</div>
49+
);
50+
};
51+
52+
export default MDXContent;

src/domains/Events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const eventSchema = z.object({
44
id: z.number(),
55
title: z.string(),
66
description: z.string(),
7+
// content: z.string().optional(),
78
slug: z.string().optional(),
89
author: z.string(),
910
image: z.string(),
@@ -12,6 +13,7 @@ export const eventSchema = z.object({
1213
location: z.string(),
1314
duration: z.string(),
1415
capacity: z.number(),
16+
date_event: z.string(),
1517
status: z.enum(["open", "soon", "closed"]),
1618
tags: z
1719
.array(

src/features/events/components/EventInfo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const EventInfo: FC<{ event: EventType; className?: string }> = ({ event, classN
1717
<div className="flex h-4 items-center gap-2">
1818
<CalendarRange className="size-4 text-slate-700 dark:text-slate-300" />
1919
<p className="text-sm font-semibold text-slate-700 capitalize dark:text-slate-300">
20-
{useFormatDate(event?.reservation_start_date)}
20+
{useFormatDate(event?.date_event)}
2121
</p>
2222
</div>
2323
<div className="flex h-4 items-center gap-2">
Lines changed: 23 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,33 @@
1-
import { useEffect, useState } from "react";
2-
import { toast } from "sonner";
1+
import { useQuery } from "@tanstack/react-query";
32
import { eventsService } from "@/services/events";
4-
import { EventType, UserEventType } from "@/domains/Events";
53

64
export const useEventById = (eventId: string) => {
7-
const [event, setEvent] = useState<EventType>({} as EventType);
8-
const [isLoading, setIsLoading] = useState<boolean>(false);
9-
10-
useEffect(() => {
11-
const getEvent = async () => {
12-
setIsLoading(true);
13-
try {
14-
const res = await eventsService.getEventById(eventId);
15-
setEvent(res.data);
16-
} catch (err) {
17-
toast.error(err instanceof Error ? err.message : "Something went wrong.");
18-
} finally {
19-
setIsLoading(false);
20-
}
21-
};
22-
23-
getEvent();
24-
}, [eventId, toast]);
25-
26-
return { event, isLoading };
5+
return useQuery({
6+
queryKey: ["getEventById", eventId],
7+
queryFn: async () => {
8+
const response = await eventsService.getEventById(eventId);
9+
return response.data;
10+
},
11+
enabled: !!eventId,
12+
});
2713
};
2814

2915
export const useEvents = () => {
30-
const [events, setEvents] = useState<EventType[]>([]);
31-
const [isLoading, setIsLoading] = useState<boolean>(false);
32-
33-
useEffect(() => {
34-
const getEvents = async () => {
35-
setIsLoading(true);
36-
try {
37-
const res = await eventsService.getEvents();
38-
setEvents(res.data);
39-
} catch (err) {
40-
toast.error(err instanceof Error ? err.message : "Something went wrong.");
41-
} finally {
42-
setIsLoading(false);
43-
}
44-
};
45-
46-
getEvents();
47-
}, [toast]);
48-
49-
return { events, isLoading };
16+
return useQuery({
17+
queryKey: ["events"],
18+
queryFn: async () => {
19+
const response = await eventsService.getEvents();
20+
return response.data;
21+
},
22+
});
5023
};
5124

5225
export const useMyEvents = (page: number = 1, limit: number = 10) => {
53-
const [myEvents, setMyEvents] = useState<UserEventType[]>([]);
54-
const [isLoading, setIsLoading] = useState<boolean>(false);
55-
56-
useEffect(() => {
57-
const getEvents = async () => {
58-
setIsLoading(true);
59-
try {
60-
const res = await eventsService.getMyEvents(page, limit);
61-
setMyEvents(res.data);
62-
} catch (err) {
63-
toast.error(err instanceof Error ? err.message : "Something went wrong.");
64-
} finally {
65-
setIsLoading(false);
66-
}
67-
};
68-
69-
getEvents();
70-
}, [toast]);
71-
72-
return { myEvents, isLoading };
26+
return useQuery({
27+
queryKey: ["myEvents", page, limit],
28+
queryFn: async () => {
29+
const response = await eventsService.getMyEvents(page, limit);
30+
return response.data;
31+
},
32+
});
7333
};

src/features/events/pages/PublicEventDetailPage.tsx

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import EventBreadcrumbs from "../components/EventBreadcrumb";
1111
import EventFormRegistration from "../components/EventFormRegistration";
1212
import { useEventById } from "../hooks/useEvent";
1313
import { Separator } from "@/components/ui/Separator";
14+
import MDXContent from "@/components/common/MDXContent";
1415

1516
interface EventDetailPageProp {
1617
eventId: string;
1718
}
1819

1920
const EventDetailPage: FC<EventDetailPageProp> = ({ eventId }) => {
2021
const t = useTranslations("EventsPage");
21-
const { event, isLoading } = useEventById(eventId);
22+
const { data: event, isLoading } = useEventById(eventId);
2223

2324
return (
2425
<div className="container mx-auto space-y-6 py-24">
@@ -41,7 +42,7 @@ const EventDetailPage: FC<EventDetailPageProp> = ({ eventId }) => {
4142
<EventBreadcrumbs />
4243
<div className="space-y-6">
4344
<h1 className="text-xl font-bold sm:text-3xl md:mt-8">{event?.title}</h1>
44-
{!isLoading ? (
45+
{!isLoading && event ? (
4546
<EventInfo event={event} className="lg:hidden" />
4647
) : (
4748
<Skeleton className="h-10 w-full rounded-lg" />
@@ -50,38 +51,18 @@ const EventDetailPage: FC<EventDetailPageProp> = ({ eventId }) => {
5051
<TitleContainer>
5152
<h2 className="font-semibold sm:text-xl">{t("EventDetail.desc-title")}</h2>
5253
</TitleContainer>
53-
<p className="text-sm text-slate-600 sm:text-base dark:text-slate-400">
54-
{event?.description} Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestiae, doloremque?
55-
Blanditiis a aspernatur eveniet, similique magni pariatur autem debitis odit suscipit laboriosam
56-
repellat consequuntur distinctio consequatur, doloribus ea deserunt? Voluptatibus quaerat, facere ipsa
57-
eum temporibus eaque ad commodi? Temporibus esse minima vitae nisi reprehenderit obcaecati doloremque
58-
voluptatibus autem accusantium delectus, voluptatem hic ipsam aspernatur voluptatum quod necessitatibus?
59-
Ad exercitationem molestiae voluptas dolorem excepturi, earum deserunt ab. Aspernatur molestias impedit
60-
repudiandae blanditiis eaque minima, a quasi laudantium cumque quo, neque possimus sunt, inventore
61-
minus. Quae eius facere cupiditate libero excepturi incidunt qui temporibus? Molestiae sint fugiat
62-
delectus. Alias ea doloremque totam veritatis fuga sequi labore, numquam unde, natus nostrum illum neque
63-
facilis laudantium corporis hic fugiat ullam. Voluptatum ut fuga placeat molestiae nobis quasi corrupti
64-
in, iure itaque quae tempora doloribus error dolore, totam quo rerum rem quos ex consequatur! Vel
65-
laudantium harum, libero inventore eum velit eveniet cumque, illum modi ducimus accusantium quas,
66-
mollitia distinctio ratione molestias ipsam impedit repudiandae itaque. Eligendi sed architecto ex
67-
explicabo nostrum aspernatur accusantium ratione veritatis delectus laudantium eius magnam voluptatibus
68-
autem, quas cum enim consequuntur veniam incidunt quisquam saepe aliquid sequi eveniet officiis atque!
69-
Inventore alias odit debitis sunt, animi ea maiores porro dolorem sint ipsa? Saepe, molestiae cumque
70-
voluptate, a quaerat ad, quia placeat explicabo animi ullam totam aliquam! Distinctio beatae aliquam eos
71-
dicta vero, placeat praesentium voluptas labore nesciunt illum at esse, ducimus recusandae veniam
72-
accusamus optio minima earum cumque. Nostrum corrupti fuga provident quibusdam repellendus, molestias ut
73-
vel aspernatur eum mollitia in quisquam praesentium minus doloremque esse? Fugiat, distinctio fuga
74-
repellendus pariatur illo fugit, quisquam odio neque, dignissimos reprehenderit eveniet. Aspernatur
75-
praesentium tempora perspiciatis excepturi, exercitationem, dolores non labore deserunt assumenda ab
76-
animi debitis obcaecati nemo est corrupti sint laudantium ipsa quibusdam explicabo nobis, saepe sunt
77-
nostrum perferendis optio! Ullam, quisquam!
78-
</p>
54+
<MDXContent
55+
content={event?.description}
56+
fallbackText={event?.description}
57+
theme="github-dark"
58+
skeletonHeight="h-20"
59+
/>
7960
</div>
8061
</div>
8162
</div>
8263
<div className="fixed right-0 bottom-0 left-0 flex w-full items-center justify-between gap-4 self-start rounded-lg bg-white lg:sticky lg:top-24 lg:flex-col lg:justify-start lg:bg-transparent lg:px-4 dark:bg-slate-950">
8364
<div className="hidden w-full space-y-6 rounded-lg border p-4 lg:block">
84-
{!isLoading ? <EventInfo event={event} /> : <Skeleton className="h-4 w-full rounded-lg" />}
65+
{!isLoading && event ? <EventInfo event={event} /> : <Skeleton className="h-4 w-full rounded-lg" />}
8566
</div>
8667
<div className="flex w-full flex-col gap-4 rounded-lg border-t px-6 py-4 sm:border">
8768
<div className="flex w-full items-center justify-between">
@@ -91,7 +72,7 @@ const EventDetailPage: FC<EventDetailPageProp> = ({ eventId }) => {
9172
<p className="text-sm font-bold dark:text-slate-200">{useFormatPrice(event?.price)}</p>
9273
</div>
9374
<Separator />
94-
<EventFormRegistration data={event} />
75+
{event && <EventFormRegistration data={event} />}
9576
</div>
9677
</div>
9778
</div>

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as useMDX } from "./useMDX";

src/hooks/useMDX.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useState, useEffect } from "react";
2+
import { MDXRemoteSerializeResult } from "next-mdx-remote";
3+
import { serialize } from "next-mdx-remote/serialize";
4+
import remarkGfm from "remark-gfm";
5+
import rehypePrettyCode from "rehype-pretty-code";
6+
7+
interface UseMDXOptions {
8+
parseFrontmatter?: boolean;
9+
theme?: string;
10+
}
11+
12+
export const useMDX = (content: string | undefined, options: UseMDXOptions = {}) => {
13+
const [mdxSource, setMdxSource] = useState<MDXRemoteSerializeResult | null>(null);
14+
const [isLoading, setIsLoading] = useState(false);
15+
const [error, setError] = useState<string | null>(null);
16+
17+
useEffect(() => {
18+
if (!content) {
19+
setMdxSource(null);
20+
setError(null);
21+
return;
22+
}
23+
24+
setIsLoading(true);
25+
setError(null);
26+
27+
serialize(content, {
28+
parseFrontmatter: options.parseFrontmatter || true,
29+
mdxOptions: {
30+
remarkPlugins: [remarkGfm],
31+
rehypePlugins: [
32+
[
33+
rehypePrettyCode,
34+
{
35+
theme: options.theme || "github-dark",
36+
keepBackground: false,
37+
},
38+
],
39+
],
40+
},
41+
})
42+
.then(setMdxSource)
43+
.catch((err) => {
44+
console.error("Error serializing MDX:", err);
45+
setError(err.message || "Failed to serialize MDX content");
46+
setMdxSource(null);
47+
})
48+
.finally(() => setIsLoading(false));
49+
}, [content, options.parseFrontmatter, options.theme]);
50+
51+
return {
52+
mdxSource,
53+
isLoading,
54+
error,
55+
};
56+
};
57+
58+
export default useMDX;

src/lib/mdx.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,33 @@ export async function getBlogsByCategory(category: string): Promise<BlogPost[]>
214214
const allBlogs = await getAllBlogs();
215215
return allBlogs.filter((blog) => blog.metadata.category?.toLowerCase() === category.toLowerCase());
216216
}
217+
218+
export const convertContentToMarkdown = async (text: string): Promise<{ content: React.ReactElement }> => {
219+
try {
220+
const { content } = await compileMDX({
221+
source: text,
222+
options: {
223+
mdxOptions: {
224+
remarkPlugins: [remarkGfm],
225+
rehypePlugins: [
226+
[
227+
rehypePrettyCode,
228+
{
229+
theme: "github-dark",
230+
keepBackground: false,
231+
},
232+
],
233+
],
234+
},
235+
parseFrontmatter: true,
236+
},
237+
});
238+
239+
return {
240+
content,
241+
};
242+
} catch (error) {
243+
console.log(error);
244+
throw error;
245+
}
246+
};

0 commit comments

Comments
 (0)