Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions app/[locale]/(user)/collaboratives/components/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
import Image from 'next/image';
import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';

import { Icons } from '@/components/icons';
import { RichTextRenderer } from '@/components/RichTextRenderer';
import Metadata from './Metadata';

const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
Expand Down Expand Up @@ -142,14 +140,10 @@ const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
</div>
)}
<div className="mt-6 lg:mt-10">
<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">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{data.collaborativeBySlug.summary}
</ReactMarkdown>
</div>
<RichTextRenderer
content={data.collaborativeBySlug.summary}
className="text-white"
/>
</div>
</div>
</div>
Expand Down
13 changes: 2 additions & 11 deletions app/[locale]/(user)/usecases/components/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';

import { Icons } from '@/components/icons';
import { RichTextRenderer } from '@/components/RichTextRenderer';
import Metadata from './Metadata';

const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
Expand Down Expand Up @@ -92,14 +90,7 @@ const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
</div>
)}
<div className="mt-6 lg:mt-10">
<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">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{data.useCase.summary}
</ReactMarkdown>
</div>
<RichTextRenderer content={data.useCase.summary} />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DropZone, Select, TextField, toast } from 'opub-ui';
import { GraphQL } from '@/lib/api';
import { useEditStatus } from '../../context';
import Metadata from '../metadata/page';
import { RichTextEditor } from '@/components/RichTextEditor';

const UpdateCollaborativeMutation: any = graphql(`
mutation updateCollaborative($data: CollaborativeInputPartial!) {
Expand Down Expand Up @@ -227,14 +228,13 @@ const Details = () => {
return (
<div className=" flex flex-col gap-6">
<div>
<TextField
<RichTextEditor
label="Summary *"
name="summary"
value={formData.summary}
multiline={7}
helpText={`Character limit: ${formData?.summary?.length}/10000`}
onChange={(e) => handleChange('summary', e)}
onChange={(value) => handleChange('summary', value)}
onBlur={() => handleSave(formData)}
placeholder="Enter collaborative summary with rich formatting..."
helpText={`Character limit: ${formData?.summary?.length || 0}/10000`}
/>
</div>
<div className="flex flex-wrap gap-6 md:flex-nowrap lg:flex-nowrap">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DropZone, Select, TextField, toast } from 'opub-ui';
import { GraphQL } from '@/lib/api';
import { useEditStatus } from '../../context';
import Metadata from '../metadata/page';
import { RichTextEditor } from '@/components/RichTextEditor';

const UpdateUseCaseMutation: any = graphql(`
mutation updateUseCase($data: UseCaseInputPartial!) {
Expand Down Expand Up @@ -224,14 +225,13 @@ const Details = () => {
return (
<div className=" flex flex-col gap-6">
<div>
<TextField
<RichTextEditor
label="Summary *"
name="summary"
value={formData.summary}
multiline={7}
helpText={`Character limit: ${formData?.summary?.length}/10000`}
onChange={(e) => handleChange('summary', e)}
onChange={(value) => handleChange('summary', value)}
onBlur={() => handleSave(formData)}
placeholder="Enter use case summary with rich formatting..."
helpText={`Character limit: ${formData?.summary?.length || 0}/10000`}
/>
</div>
<div className="flex flex-wrap gap-6 md:flex-nowrap lg:flex-nowrap">
Expand Down
273 changes: 273 additions & 0 deletions components/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
'use client';

import React, { useMemo, useState } from 'react';
import dynamic from 'next/dynamic';
import 'react-quill/dist/quill.snow.css';

interface RichTextEditorProps {
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
placeholder?: string;
label?: string;
helpText?: string;
readOnly?: boolean;
showPreview?: boolean;
}

const RichTextEditor: React.FC<RichTextEditorProps> = ({
value,
onChange,
onBlur,
placeholder = 'Enter description...',
label,
helpText,
readOnly = false,
showPreview = true,
}) => {
const [isPreview, setIsPreview] = useState(false);
// Dynamically import ReactQuill to avoid SSR issues
const ReactQuill = useMemo(
() =>
dynamic(() => import('react-quill'), {
ssr: false,
loading: () => <div className="h-40 animate-pulse bg-gray-100 rounded" />,
}),
[]
);

// Custom image handler to use URLs instead of base64
const imageHandler = function(this: any) {
const url = prompt('Enter image URL:');
if (url) {
const quill = this.quill;
const range = quill.getSelection();
if (range) {
quill.insertEmbed(range.index, 'image', url);
}
}
};

const modules = useMemo(
() => ({
toolbar: readOnly
? false
: {
container: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
['link', 'image'],
[{ align: [] }],
['clean'],
['preview'], // Custom preview button
],
handlers: {
preview: () => setIsPreview(!isPreview),
image: imageHandler,
},
},
clipboard: {
matchVisual: false,
},
}),
[readOnly, isPreview]
);

const formats = [
'header',
'bold',
'italic',
'underline',
'strike',
'list',
'bullet',
'indent',
'link',
'image',
'align',
];

return (
<div className="w-full">
{label && (
<label className="mb-2 block text-sm font-medium text-gray-700">
{label}
</label>
)}

{isPreview ? (
<div className="min-h-[200px] rounded-md border border-gray-300 bg-white">
<div className="flex items-center justify-between border-b border-gray-300 bg-gray-50 px-4 py-2">
<span className="text-sm font-medium text-gray-700">Preview</span>
<button
type="button"
onClick={() => setIsPreview(false)}
className="flex items-center gap-1 rounded px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
</svg>
Back to Edit
</button>
</div>
<div className="p-4">
<div
className="ql-editor preview-content"
dangerouslySetInnerHTML={{ __html: value || '<p class="text-gray-400">No content to preview</p>' }}
/>
</div>
</div>
) : (
<div className={`rich-text-editor ${readOnly ? 'read-only' : ''}`}>
<ReactQuill
theme="snow"
value={value || ''}
onChange={onChange}
onBlur={onBlur}
modules={modules}
formats={formats}
placeholder={placeholder}
readOnly={readOnly}
className={readOnly ? 'quill-readonly' : ''}
/>
</div>
)}

{helpText && (
<p className="mt-1 text-sm text-gray-500">{helpText}</p>
)}
<style jsx global>{`
.rich-text-editor .quill {
background: white;
border-radius: 4px;
}

.rich-text-editor .ql-container {
min-height: 200px;
font-size: 14px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}

.rich-text-editor .ql-toolbar {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background: #f8f9fa;
}

.rich-text-editor.read-only .ql-toolbar {
display: none;
}

.rich-text-editor.read-only .ql-container {
border: none;
}

.rich-text-editor .ql-editor {
min-height: 200px;
}

.rich-text-editor .ql-editor.ql-blank::before {
color: #9ca3af;
font-style: normal;
}

/* Custom preview button in toolbar */
.rich-text-editor .ql-toolbar .ql-preview {
width: auto !important;
}

.rich-text-editor .ql-toolbar .ql-preview::before {
content: '👁️ Preview';
font-size: 13px;
padding: 0 8px;
}

.rich-text-editor .ql-toolbar .ql-preview:hover {
color: #2563eb;
}

.rich-text-editor .ql-toolbar button.ql-preview {
background: transparent;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}

.preview-content {
padding: 0;
min-height: 200px;
}

.preview-content p {
margin-bottom: 1em;
}

.preview-content h1 {
font-size: 2em;
font-weight: bold;
margin-bottom: 0.5em;
}

.preview-content h2 {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 0.5em;
}

.preview-content h3 {
font-size: 1.25em;
font-weight: bold;
margin-bottom: 0.5em;
}

.preview-content ul,
.preview-content ol {
padding-left: 1.5em;
margin-bottom: 1em;
}

.preview-content li {
margin-bottom: 0.25em;
}

.preview-content a {
color: #3b82f6;
text-decoration: underline;
}

.preview-content a:hover {
color: #2563eb;
}

.preview-content img {
max-width: 100%;
height: auto;
margin: 1em 0;
}

.preview-content strong {
font-weight: bold;
}

.preview-content em {
font-style: italic;
}

.preview-content u {
text-decoration: underline;
}

.preview-content s {
text-decoration: line-through;
}
`}</style>
</div>
);
};

export default RichTextEditor;
1 change: 1 addition & 0 deletions components/RichTextEditor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as RichTextEditor } from './RichTextEditor';
Loading