Skip to content

Commit f5df478

Browse files
authored
Merge pull request #422 from hypercerts-org/feat/markdown_support
Markdown editor and preview
2 parents 9e52170 + cfaf9b7 commit f5df478

File tree

6 files changed

+841
-52
lines changed

6 files changed

+841
-52
lines changed

app/globals.css

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,148 @@
9797
--tag-border-radius: 3px;
9898
--tag-inset-shadow-size: 1.5em;
9999
}
100+
101+
/* Markdown Editor Styles */
102+
.w-md-editor {
103+
background-color: rgb(255, 255, 255) !important; /* bg-white */
104+
border: 1px solid hsl(214.3 31.8% 91.4%) !important;
105+
border-radius: 0.5rem !important; /* rounded-md */
106+
}
107+
108+
.w-md-editor > div {
109+
border: none !important;
110+
}
111+
112+
.w-md-editor-text-pre > code,
113+
.w-md-editor-text-input {
114+
font-size: 0.875rem !important; /* text-sm */
115+
font-weight: 500 !important; /* font-medium */
116+
background-color: transparent !important;
117+
color: rgb(15 23 42) !important; /* text-slate-900 */
118+
}
119+
120+
.w-md-editor-toolbar {
121+
background-color: rgb(255, 255, 255) !important; /* bg-white */
122+
border-bottom: 1px solid hsl(214.3 31.8% 91.4%) !important;
123+
}
124+
125+
.w-md-editor-toolbar-divider {
126+
background-color: rgb(226 232 240) !important; /* bg-slate-200 */
127+
}
128+
129+
.w-md-editor-toolbar button {
130+
color: rgb(51 65 85) !important; /* text-slate-700 */
131+
}
132+
133+
.w-md-editor-toolbar button:hover {
134+
background-color: rgb(226 232 240) !important; /* bg-slate-200 */
135+
}
136+
137+
.w-md-editor-content {
138+
background-color: rgb(255, 255, 255) !important; /* bg-white */
139+
color: rgb(15 23 42) !important; /* text-slate-900 */
140+
}
141+
142+
.wmde-markdown {
143+
background-color: rgb(255, 255, 255) !important; /* bg-white */
144+
color: rgb(15 23 42) !important; /* text-slate-900 */
145+
}
146+
147+
/* Markdown Image Styles */
148+
.wmde-markdown img {
149+
max-width: 100%;
150+
height: auto;
151+
max-height: 300px; /* Fixed max height */
152+
object-fit: contain;
153+
display: block;
154+
margin: 1rem auto; /* Center the image with some vertical spacing */
155+
border-radius: 0.5rem; /* Optional: rounded corners */
156+
}
157+
158+
/* For smaller screens */
159+
@media (max-width: 768px) {
160+
.wmde-markdown img {
161+
max-height: 200px; /* Smaller height on mobile */
162+
}
163+
}
164+
165+
.w-md-editor-preview img {
166+
max-width: 33%;
167+
height: auto;
168+
max-height: 50vh;
169+
object-fit: contain;
170+
}
171+
172+
/* Preview specific styles */
173+
.wmde-markdown p,
174+
.wmde-markdown table td {
175+
font-family: var(--font-sans) !important;
176+
}
177+
178+
/* Heading specific styles */
179+
.wmde-markdown h1,
180+
.wmde-markdown h2,
181+
.wmde-markdown h3,
182+
.wmde-markdown h4,
183+
.wmde-markdown h5,
184+
.wmde-markdown h6,
185+
.wmde-markdown table th {
186+
font-family: var(--font-serif) !important;
187+
color: rgb(15 23 42) !important; /* text-slate-900 */
188+
margin: 1.5rem 0 1rem 0 !important;
189+
}
190+
191+
.wmde-markdown h1 {
192+
font-size: 1.875rem !important; /* text-4xl */
193+
line-height: 2.5rem !important;
194+
}
195+
196+
.wmde-markdown h2 {
197+
font-size: 1.5rem !important; /* text-3xl */
198+
line-height: 2.25rem !important;
199+
}
200+
201+
.wmde-markdown h3 {
202+
font-size: 1.5rem !important; /* text-2xl */
203+
line-height: 2rem !important;
204+
}
205+
206+
/* Table specific styles */
207+
.w-md-editor-text-input table,
208+
.wmde-markdown table {
209+
background-color: rgb(255, 255, 255) !important; /* bg-white */
210+
border-color: rgb(226 232 240) !important; /* border-slate-200 */
211+
}
212+
213+
.w-md-editor-text-input table th,
214+
.wmde-markdown table th {
215+
font-weight: bold !important; /* font-semibold */
216+
color: rgb(51 65 85) !important; /* text-slate-700 */
217+
background-color: rgb(248 250 252) !important; /* bg-slate-50 */
218+
}
219+
220+
.wmde-markdown-color table tr,
221+
.wmde-markdown table tr {
222+
font-weight: normal !important;
223+
background-color: rgb(255, 255, 255) !important; /* bg-slate-50 */
224+
}
225+
226+
/* List Styles */
227+
.wmde-markdown ul {
228+
list-style-type: disc !important;
229+
padding-left: 1.5rem !important;
230+
margin: 1rem 0 !important;
231+
}
232+
233+
.wmde-markdown ol {
234+
list-style-type: decimal !important;
235+
padding-left: 1.5rem !important;
236+
margin: 1rem 0 !important;
237+
}
238+
239+
.wmde-markdown li {
240+
display: list-item !important;
241+
margin: 0.25rem 0 !important;
242+
}
243+
244+
/* Heading Styles */

components/hypercert/hypercert-minting-form/form-steps.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ import {
1111
PopoverContent,
1212
PopoverTrigger,
1313
} from "@/components/ui/popover";
14+
import MDEditor, { commands } from "@uiw/react-md-editor";
1415
import {
1516
ArrowLeftIcon,
1617
ArrowRightIcon,
1718
CalendarIcon,
18-
Plus,
19+
Loader2,
1920
Trash2Icon,
2021
X,
21-
Loader2,
2222
} from "lucide-react";
2323
import { RefObject, useMemo, useState } from "react";
24+
import rehypeSanitize from "rehype-sanitize";
2425

2526
import CreateAllowlistDialog from "@/components/allowlist/create-allowlist-dialog";
2627
import ConnectDialog from "@/components/connect-dialog";
@@ -110,11 +111,42 @@ const GeneralInformation = ({ form }: FormStepsProps) => {
110111
<FormItem>
111112
<FormLabel>Description</FormLabel>
112113
<FormControl>
113-
<Textarea {...field} maxLength={5000} />
114+
<div className="rounded-md">
115+
<MDEditor
116+
value={field.value}
117+
onChange={(value) => field.onChange(value || "")}
118+
preview="edit"
119+
height={300}
120+
textareaProps={{
121+
placeholder:
122+
"Please enter the description of your project. We support plain text input and Markdown formats",
123+
maxLength: 5000,
124+
}}
125+
commands={[
126+
commands.bold,
127+
commands.italic,
128+
commands.title1,
129+
commands.title2,
130+
commands.title3,
131+
commands.divider,
132+
commands.link,
133+
commands.image,
134+
commands.quote,
135+
commands.table,
136+
commands.checkedListCommand,
137+
commands.unorderedListCommand,
138+
commands.orderedListCommand,
139+
commands.codeBlock,
140+
]}
141+
previewOptions={{
142+
rehypePlugins: [[rehypeSanitize]],
143+
}}
144+
/>
145+
</div>
114146
</FormControl>
115147
<FormMessage />
116148
<FormDescription>
117-
Describe your project: why it was created, and how it works
149+
Describe your project: why it was created, and how it works.
118150
</FormDescription>
119151
</FormItem>
120152
)}

components/hypercert/hypercert-minting-form/index.tsx

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { useEffect, useRef, useState } from "react";
44
import useIsWriteable from "@/hooks/useIsWriteable";
55
import { useMintHypercert } from "@/hypercerts/hooks/useMintHypercert";
66
import { useLocalStorage } from "react-use";
7-
import { FieldErrors, useForm, useWatch } from "react-hook-form";
7+
import { type FieldErrors, useForm, useWatch } from "react-hook-form";
88
import { zodResolver } from "@hookform/resolvers/zod";
99
import { toast } from "@/components/ui/use-toast";
1010
import {
1111
formatHypercertData,
12-
HypercertMetadata,
12+
type HypercertMetadata,
1313
TransferRestrictions,
1414
} from "@hypercerts-org/sdk";
1515
import { DEFAULT_NUM_UNITS } from "@/configs/hypercerts";
@@ -36,7 +36,7 @@ const formSchema = z.object({
3636
.string()
3737
.trim()
3838
.min(10, { message: "We need a longer description for your hypercert" })
39-
.max(5000, "max 5000 characters"),
39+
.max(10000, "max 10000 characters"),
4040
link: z
4141
.string()
4242
.url("Please enter a valid link")
@@ -301,22 +301,6 @@ export function HypercertMintingForm({
301301
return;
302302
}
303303

304-
// if (writeableErrors) {
305-
// Object.values(writeableErrors).forEach((error) => {
306-
// if (error) {
307-
// toast({
308-
// variant: "destructive",
309-
// title: "Sorry! We can't start the mint...",
310-
// description: error,
311-
// });
312-
// }
313-
// });
314-
// if (!writeable) {
315-
// resetWriteableErrors();
316-
// return;
317-
// }
318-
// }
319-
320304
if (!isBlueprint) {
321305
await mintHypercert({
322306
metaData: formattedMetadata.data!,
@@ -354,22 +338,6 @@ export function HypercertMintingForm({
354338
}
355339
};
356340

357-
// if (writeableErrors) {
358-
// Object.values(writeableErrors).forEach((error) => {
359-
// if (error) {
360-
// toast({
361-
// variant: "destructive",
362-
// title: "Sorry! We can't start the mint...",
363-
// description: error,
364-
// });
365-
// }
366-
// });
367-
// if (!writeable) {
368-
// resetWriteableErrors();
369-
// return;
370-
// }
371-
// }
372-
373341
return (
374342
<section className="flex flex-col-reverse lg:flex-row space-x-4 items-stretch md:justify-start">
375343
<section className="flex flex-col space-y-4 flex-1 md:pr-5 md:border-r-[1.5px] md:border-slate-200">

components/read-more.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
"use client";
22

3-
import ReactMarkdown from "react-markdown";
4-
5-
import { containsMarkdown } from "@/lib/utils";
63
import {
74
Collapsible,
85
CollapsibleContent,
96
CollapsibleTrigger,
107
} from "./ui/collapsible";
118

129
import { useState } from "react";
10+
import MDEditor from "@uiw/react-md-editor";
1311

1412
export default function ReadMore({
1513
text,
@@ -20,25 +18,42 @@ export default function ReadMore({
2018
}) {
2119
const [isOpen, setIsOpen] = useState(false);
2220

21+
console.log(text);
22+
2323
if (!text) {
2424
return null;
2525
}
2626

27-
if (containsMarkdown(text)) {
28-
return <ReactMarkdown>{text}</ReactMarkdown>;
29-
}
30-
3127
if (text.length <= length) {
32-
return <div>{text}</div>;
28+
return <MDEditor.Markdown source={text} />;
3329
}
3430

35-
const cutoffText = text.substring(0, length) + "...";
31+
// Find the last safe breakpoint before any markdown syntax
32+
const findLastBreakpoint = (text: string, maxLength: number) => {
33+
const substring = text.substring(0, maxLength);
34+
35+
// Look for markdown syntax to avoid cutting in the middle
36+
const markdownStart = substring.search(/(!?\[|#{1,6}\s|```)/);
37+
const searchEnd = markdownStart > 0 ? markdownStart : maxLength;
38+
39+
// Find the last line break or sentence end
40+
const lastLineBreak = substring.lastIndexOf("\n", searchEnd);
41+
const lastPeriod = substring.lastIndexOf(". ", searchEnd);
42+
const lastBreak = Math.max(lastLineBreak, lastPeriod);
43+
44+
return lastBreak > 0 ? lastBreak + 1 : searchEnd;
45+
};
46+
47+
const cutoffPoint = findLastBreakpoint(text, length);
48+
const cutoffText = text.substring(0, cutoffPoint);
3649

3750
return (
3851
<div className="flex flex-col">
39-
{!isOpen && cutoffText}
52+
{!isOpen && <MDEditor.Markdown source={cutoffText} />}
4053
<Collapsible onOpenChange={setIsOpen}>
41-
<CollapsibleContent className="text-pretty">{text}</CollapsibleContent>
54+
<CollapsibleContent>
55+
<MDEditor.Markdown source={text} />
56+
</CollapsibleContent>
4257
<CollapsibleTrigger className="text-blue-600 hover:underline text-xs lg:text-sm">
4358
{isOpen ? "Read less" : "Read more"}
4459
</CollapsibleTrigger>

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@tanstack/react-table": "^8.17.3",
4848
"@types/lodash": "^4.17.5",
4949
"@types/validator": "^13.12.2",
50+
"@uiw/react-md-editor": "^4.0.5",
5051
"@vercel/analytics": "^1.4.1",
5152
"@vercel/flags": "^3.1.0",
5253
"@wagmi/core": "^2.16.3",
@@ -67,8 +68,9 @@
6768
"react-day-picker": "^8.10.1",
6869
"react-dom": "^18.3.1",
6970
"react-hook-form": "^7.51.3",
70-
"react-markdown": "^9.0.1",
71+
"react-markdown": "^9.0.3",
7172
"react-use": "^17.5.1",
73+
"rehype-sanitize": "^6.0.0",
7274
"remeda": "^2.0.3",
7375
"server-only": "^0.0.1",
7476
"sharp": "^0.33.5",

0 commit comments

Comments
 (0)