Skip to content

Commit 4e1d609

Browse files
committed
Change marddown to richtext editor and renderer
1 parent 0c697b5 commit 4e1d609

File tree

11 files changed

+847
-1674
lines changed

11 files changed

+847
-1674
lines changed

app/[locale]/(user)/collaboratives/components/Details.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import Image from 'next/image';
44
import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui';
55
import { useState } from 'react';
6-
import ReactMarkdown from 'react-markdown';
7-
import rehypeRaw from 'rehype-raw';
8-
import remarkGfm from 'remark-gfm';
96

107
import { Icons } from '@/components/icons';
8+
import { RichTextRenderer } from '@/components/RichTextRenderer';
119
import Metadata from './Metadata';
1210

1311
const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
@@ -142,14 +140,10 @@ const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
142140
</div>
143141
)}
144142
<div className="mt-6 lg:mt-10">
145-
<div className="prose-h1:text-3xl prose-h2:text-2xl prose-h3:text-xl prose-p:leading-relaxed prose-a:text-blue-400 hover:prose-a:text-blue-300 prose-code:bg-gray-800 prose-code:rounded prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-700 prose-blockquote:border-l-blue-500 prose-th:bg-gray-800 prose-img:rounded-lg prose prose-lg prose-invert mt-4 max-w-none prose-headings:text-white prose-p:text-white prose-a:underline prose-blockquote:text-white prose-strong:text-white prose-em:text-white prose-code:px-1 prose-code:py-0.5 prose-code:text-white prose-code:before:content-none prose-code:after:content-none prose-pre:text-white prose-ol:text-white prose-ul:text-white prose-li:text-white prose-li:marker:text-white prose-table:text-white prose-thead:border-white prose-tr:border-white prose-th:border-white prose-th:text-white prose-td:border-white prose-td:text-white prose-hr:border-white">
146-
<ReactMarkdown
147-
remarkPlugins={[remarkGfm]}
148-
rehypePlugins={[rehypeRaw]}
149-
>
150-
{data.collaborativeBySlug.summary}
151-
</ReactMarkdown>
152-
</div>
143+
<RichTextRenderer
144+
content={data.collaborativeBySlug.summary}
145+
className="text-white"
146+
/>
153147
</div>
154148
</div>
155149
</div>

app/[locale]/(user)/usecases/components/Details.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import React, { useState } from 'react';
44
import Image from 'next/image';
55
import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui';
6-
import ReactMarkdown from 'react-markdown';
7-
import rehypeRaw from 'rehype-raw';
8-
import remarkGfm from 'remark-gfm';
96

107
import { Icons } from '@/components/icons';
8+
import { RichTextRenderer } from '@/components/RichTextRenderer';
119
import Metadata from './Metadata';
1210

1311
const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
@@ -92,14 +90,7 @@ const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
9290
</div>
9391
)}
9492
<div className="mt-6 lg:mt-10">
95-
<div className="prose-h1:text-3xl prose-h2:text-2xl prose-h3:text-xl prose-p:leading-relaxed prose-a:text-blue-600 hover:prose-a:text-blue-700 prose-code:bg-gray-200 prose-code:rounded prose-pre:bg-gray-100 prose-pre:border prose-pre:border-gray-300 prose-blockquote:border-l-blue-500 prose-th:bg-gray-100 prose-img:rounded-lg prose prose-lg mt-4 max-w-none prose-headings:text-gray-900 prose-p:text-gray-800 prose-a:underline prose-blockquote:text-gray-700 prose-strong:text-gray-900 prose-em:text-gray-800 prose-code:px-1 prose-code:py-0.5 prose-code:text-gray-900 prose-code:before:content-none prose-code:after:content-none prose-pre:text-gray-900 prose-ol:text-gray-800 prose-ul:text-gray-800 prose-li:text-gray-800 prose-li:marker:text-gray-600 prose-table:text-gray-800 prose-thead:border-gray-300 prose-tr:border-gray-300 prose-th:border-gray-300 prose-th:text-gray-900 prose-td:border-gray-300 prose-td:text-gray-800 prose-hr:border-gray-300">
96-
<ReactMarkdown
97-
remarkPlugins={[remarkGfm]}
98-
rehypePlugins={[rehypeRaw]}
99-
>
100-
{data.useCase.summary}
101-
</ReactMarkdown>
102-
</div>
93+
<RichTextRenderer content={data.useCase.summary} />
10394
</div>
10495
</div>
10596
</div>

app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DropZone, Select, TextField, toast } from 'opub-ui';
1010
import { GraphQL } from '@/lib/api';
1111
import { useEditStatus } from '../../context';
1212
import Metadata from '../metadata/page';
13+
import { RichTextEditor } from '@/components/RichTextEditor';
1314

1415
const UpdateCollaborativeMutation: any = graphql(`
1516
mutation updateCollaborative($data: CollaborativeInputPartial!) {
@@ -227,14 +228,13 @@ const Details = () => {
227228
return (
228229
<div className=" flex flex-col gap-6">
229230
<div>
230-
<TextField
231+
<RichTextEditor
231232
label="Summary *"
232-
name="summary"
233233
value={formData.summary}
234-
multiline={7}
235-
helpText={`Character limit: ${formData?.summary?.length}/10000`}
236-
onChange={(e) => handleChange('summary', e)}
234+
onChange={(value) => handleChange('summary', value)}
237235
onBlur={() => handleSave(formData)}
236+
placeholder="Enter collaborative summary with rich formatting..."
237+
helpText={`Character limit: ${formData?.summary?.length || 0}/10000`}
238238
/>
239239
</div>
240240
<div className="flex flex-wrap gap-6 md:flex-nowrap lg:flex-nowrap">

app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { DropZone, Select, TextField, toast } from 'opub-ui';
1212
import { GraphQL } from '@/lib/api';
1313
import { useEditStatus } from '../../context';
1414
import Metadata from '../metadata/page';
15+
import { RichTextEditor } from '@/components/RichTextEditor';
1516

1617
const UpdateUseCaseMutation: any = graphql(`
1718
mutation updateUseCase($data: UseCaseInputPartial!) {
@@ -224,14 +225,13 @@ const Details = () => {
224225
return (
225226
<div className=" flex flex-col gap-6">
226227
<div>
227-
<TextField
228+
<RichTextEditor
228229
label="Summary *"
229-
name="summary"
230230
value={formData.summary}
231-
multiline={7}
232-
helpText={`Character limit: ${formData?.summary?.length}/10000`}
233-
onChange={(e) => handleChange('summary', e)}
231+
onChange={(value) => handleChange('summary', value)}
234232
onBlur={() => handleSave(formData)}
233+
placeholder="Enter use case summary with rich formatting..."
234+
helpText={`Character limit: ${formData?.summary?.length || 0}/10000`}
235235
/>
236236
</div>
237237
<div className="flex flex-wrap gap-6 md:flex-nowrap lg:flex-nowrap">
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
'use client';
2+
3+
import React, { useMemo, useState } from 'react';
4+
import dynamic from 'next/dynamic';
5+
import 'react-quill/dist/quill.snow.css';
6+
7+
interface RichTextEditorProps {
8+
value: string;
9+
onChange: (value: string) => void;
10+
onBlur?: () => void;
11+
placeholder?: string;
12+
label?: string;
13+
helpText?: string;
14+
readOnly?: boolean;
15+
showPreview?: boolean;
16+
}
17+
18+
const RichTextEditor: React.FC<RichTextEditorProps> = ({
19+
value,
20+
onChange,
21+
onBlur,
22+
placeholder = 'Enter description...',
23+
label,
24+
helpText,
25+
readOnly = false,
26+
showPreview = true,
27+
}) => {
28+
const [isPreview, setIsPreview] = useState(false);
29+
// Dynamically import ReactQuill to avoid SSR issues
30+
const ReactQuill = useMemo(
31+
() =>
32+
dynamic(() => import('react-quill'), {
33+
ssr: false,
34+
loading: () => <div className="h-40 animate-pulse bg-gray-100 rounded" />,
35+
}),
36+
[]
37+
);
38+
39+
// Custom image handler to use URLs instead of base64
40+
const imageHandler = function(this: any) {
41+
const url = prompt('Enter image URL:');
42+
if (url) {
43+
const quill = this.quill;
44+
const range = quill.getSelection();
45+
if (range) {
46+
quill.insertEmbed(range.index, 'image', url);
47+
}
48+
}
49+
};
50+
51+
const modules = useMemo(
52+
() => ({
53+
toolbar: readOnly
54+
? false
55+
: {
56+
container: [
57+
[{ header: [1, 2, 3, false] }],
58+
['bold', 'italic', 'underline', 'strike'],
59+
[{ list: 'ordered' }, { list: 'bullet' }],
60+
[{ indent: '-1' }, { indent: '+1' }],
61+
['link', 'image'],
62+
[{ align: [] }],
63+
['clean'],
64+
['preview'], // Custom preview button
65+
],
66+
handlers: {
67+
preview: () => setIsPreview(!isPreview),
68+
image: imageHandler,
69+
},
70+
},
71+
clipboard: {
72+
matchVisual: false,
73+
},
74+
}),
75+
[readOnly, isPreview]
76+
);
77+
78+
const formats = [
79+
'header',
80+
'bold',
81+
'italic',
82+
'underline',
83+
'strike',
84+
'list',
85+
'bullet',
86+
'indent',
87+
'link',
88+
'image',
89+
'align',
90+
];
91+
92+
return (
93+
<div className="w-full">
94+
{label && (
95+
<label className="mb-2 block text-sm font-medium text-gray-700">
96+
{label}
97+
</label>
98+
)}
99+
100+
{isPreview ? (
101+
<div className="min-h-[200px] rounded-md border border-gray-300 bg-white">
102+
<div className="flex items-center justify-between border-b border-gray-300 bg-gray-50 px-4 py-2">
103+
<span className="text-sm font-medium text-gray-700">Preview</span>
104+
<button
105+
type="button"
106+
onClick={() => setIsPreview(false)}
107+
className="flex items-center gap-1 rounded px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200"
108+
>
109+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
111+
</svg>
112+
Back to Edit
113+
</button>
114+
</div>
115+
<div className="p-4">
116+
<div
117+
className="ql-editor preview-content"
118+
dangerouslySetInnerHTML={{ __html: value || '<p class="text-gray-400">No content to preview</p>' }}
119+
/>
120+
</div>
121+
</div>
122+
) : (
123+
<div className={`rich-text-editor ${readOnly ? 'read-only' : ''}`}>
124+
<ReactQuill
125+
theme="snow"
126+
value={value || ''}
127+
onChange={onChange}
128+
onBlur={onBlur}
129+
modules={modules}
130+
formats={formats}
131+
placeholder={placeholder}
132+
readOnly={readOnly}
133+
className={readOnly ? 'quill-readonly' : ''}
134+
/>
135+
</div>
136+
)}
137+
138+
{helpText && (
139+
<p className="mt-1 text-sm text-gray-500">{helpText}</p>
140+
)}
141+
<style jsx global>{`
142+
.rich-text-editor .quill {
143+
background: white;
144+
border-radius: 4px;
145+
}
146+
147+
.rich-text-editor .ql-container {
148+
min-height: 200px;
149+
font-size: 14px;
150+
border-bottom-left-radius: 4px;
151+
border-bottom-right-radius: 4px;
152+
}
153+
154+
.rich-text-editor .ql-toolbar {
155+
border-top-left-radius: 4px;
156+
border-top-right-radius: 4px;
157+
background: #f8f9fa;
158+
}
159+
160+
.rich-text-editor.read-only .ql-toolbar {
161+
display: none;
162+
}
163+
164+
.rich-text-editor.read-only .ql-container {
165+
border: none;
166+
}
167+
168+
.rich-text-editor .ql-editor {
169+
min-height: 200px;
170+
}
171+
172+
.rich-text-editor .ql-editor.ql-blank::before {
173+
color: #9ca3af;
174+
font-style: normal;
175+
}
176+
177+
/* Custom preview button in toolbar */
178+
.rich-text-editor .ql-toolbar .ql-preview {
179+
width: auto !important;
180+
}
181+
182+
.rich-text-editor .ql-toolbar .ql-preview::before {
183+
content: '👁️ Preview';
184+
font-size: 13px;
185+
padding: 0 8px;
186+
}
187+
188+
.rich-text-editor .ql-toolbar .ql-preview:hover {
189+
color: #2563eb;
190+
}
191+
192+
.rich-text-editor .ql-toolbar button.ql-preview {
193+
background: transparent;
194+
border: none;
195+
cursor: pointer;
196+
display: inline-flex;
197+
align-items: center;
198+
gap: 4px;
199+
}
200+
201+
.preview-content {
202+
padding: 0;
203+
min-height: 200px;
204+
}
205+
206+
.preview-content p {
207+
margin-bottom: 1em;
208+
}
209+
210+
.preview-content h1 {
211+
font-size: 2em;
212+
font-weight: bold;
213+
margin-bottom: 0.5em;
214+
}
215+
216+
.preview-content h2 {
217+
font-size: 1.5em;
218+
font-weight: bold;
219+
margin-bottom: 0.5em;
220+
}
221+
222+
.preview-content h3 {
223+
font-size: 1.25em;
224+
font-weight: bold;
225+
margin-bottom: 0.5em;
226+
}
227+
228+
.preview-content ul,
229+
.preview-content ol {
230+
padding-left: 1.5em;
231+
margin-bottom: 1em;
232+
}
233+
234+
.preview-content li {
235+
margin-bottom: 0.25em;
236+
}
237+
238+
.preview-content a {
239+
color: #3b82f6;
240+
text-decoration: underline;
241+
}
242+
243+
.preview-content a:hover {
244+
color: #2563eb;
245+
}
246+
247+
.preview-content img {
248+
max-width: 100%;
249+
height: auto;
250+
margin: 1em 0;
251+
}
252+
253+
.preview-content strong {
254+
font-weight: bold;
255+
}
256+
257+
.preview-content em {
258+
font-style: italic;
259+
}
260+
261+
.preview-content u {
262+
text-decoration: underline;
263+
}
264+
265+
.preview-content s {
266+
text-decoration: line-through;
267+
}
268+
`}</style>
269+
</div>
270+
);
271+
};
272+
273+
export default RichTextEditor;

components/RichTextEditor/index.ts

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

0 commit comments

Comments
 (0)