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
78 changes: 78 additions & 0 deletions docs/frontend/rich-text-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Rich Text Editor (#795)

`frontend/components/markdown/RichTextEditor.tsx` replaces the plain `Input`
previously used for project descriptions. It gives contributors a formatting
toolbar, image support, and a live preview while keeping **markdown** as the
stored format.

## Why markdown

- Descriptions are persisted on-chain as part of `workDescription`, so a portable,
plain-text format avoids HTML escaping problems and payload bloat.
- Rendering reuses `MarkdownContent`, which already sanitizes output with
`rehype-sanitize` and `remark-gfm` — no new dependencies were added.

## Usage

```tsx
import { Controller } from 'react-hook-form';
import { RichTextEditor } from '@/components/markdown/RichTextEditor';

<Controller
name="description"
control={control}
render={({ field }) => (
<RichTextEditor
id="description"
value={field.value ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
maxLength={5000}
placeholder="Describe the work"
/>
)}
/>;
```

### Props

| Prop | Type | Description |
|------|------|-------------|
| `value` | `string` | Markdown source (required) |
| `onChange` | `(value: string) => void` | Called on every edit (required) |
| `id` / `name` | `string` | Wired to the textarea for labels and forms |
| `placeholder` | `string` | Placeholder text |
| `disabled` | `boolean` | Disables the toolbar and textarea |
| `className` | `string` | Extra container classes |
| `maxLength` | `number` | Shows a counter and truncates input |
| `allowImages` | `boolean` | Toggle image insertion (default `true`) |
| `onBlur` | `() => void` | Forwarded to the textarea (react-hook-form support) |
| `aria-invalid` / `aria-describedby` | — | Forwarded for validation messaging |

## supported formatting

Bold, italic, strikethrough, `##`/`###` headings, bullet and numbered lists,
block quotes, fenced code blocks, and links. Keyboard shortcuts match common
editors: `Cmd/Ctrl+B`, `Cmd/Ctrl+I`, `Cmd/Ctrl+K`.

## Images

Images can be inserted three ways:

1. **Toolbar** — choose *Insert image* and provide a URL plus alt text.
2. **Paste** — paste an image from the clipboard (≤ 5MB).
3. **Drag & drop** — drop an image file onto the textarea (≤ 5MB).

Pasted/dropped files are inlined as data URLs. For production uploads, prefer
the server-side `POST /api/v1/uploads` endpoint and reference the returned URL.

## Preview

The *Preview* toggle renders the markdown through `MarkdownContent` with
sanitization enabled, so raw HTML and unsafe links are stripped before display.

## Tests

`frontend/components/markdown/__tests__/RichTextEditor.test.tsx` covers
formatting, list prefixes, image insertion/validation, preview rendering, the
character limit, and the disabled state.
21 changes: 16 additions & 5 deletions frontend/app/[locale]/dashboard/projects/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
Expand All @@ -22,6 +22,7 @@ import { toast } from 'sonner';
import { useAgenticPay } from '@/lib/hooks/useAgenticPay';
import { useAccount } from 'wagmi';
import { ConfirmModal } from '@/components/transaction/ConfirmModal';
import { RichTextEditor } from '@/components/markdown/RichTextEditor';
import { parseEther } from 'viem';

type PendingTransaction = {
Expand Down Expand Up @@ -136,6 +137,7 @@ export default function CreateProjectPage() {

const {
register,
control,
handleSubmit,
formState: { errors },
watch,
Expand Down Expand Up @@ -248,10 +250,19 @@ export default function CreateProjectPage() {

<div>
<Label htmlFor="description">Description</Label>
<Input
id="description"
{...register('description')}
placeholder="Brief description of work"
<Controller
name="description"
control={control}
render={({ field }) => (
<RichTextEditor
id="description"
value={field.value ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
maxLength={5000}
placeholder="Describe the work — supports **markdown**, images, and formatting"
/>
)}
/>
</div>

Expand Down
21 changes: 16 additions & 5 deletions frontend/app/dashboard/projects/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
Expand All @@ -22,6 +22,7 @@ import { toast } from 'sonner';
import { useAgenticPay } from '@/lib/hooks/useAgenticPay';
import { useAccount } from 'wagmi';
import { ConfirmModal } from '@/components/transaction/ConfirmModal';
import { RichTextEditor } from '@/components/markdown/RichTextEditor';
import { parseEther } from 'viem';

type PendingTransaction = {
Expand Down Expand Up @@ -136,6 +137,7 @@ export default function CreateProjectPage() {

const {
register,
control,
handleSubmit,
formState: { errors },
watch,
Expand Down Expand Up @@ -248,10 +250,19 @@ export default function CreateProjectPage() {

<div>
<Label htmlFor="description">Description</Label>
<Input
id="description"
{...register('description')}
placeholder="Brief description of work"
<Controller
name="description"
control={control}
render={({ field }) => (
<RichTextEditor
id="description"
value={field.value ?? ''}
onChange={field.onChange}
onBlur={field.onBlur}
maxLength={5000}
placeholder="Describe the work — supports **markdown**, images, and formatting"
/>
)}
/>
</div>

Expand Down
2 changes: 1 addition & 1 deletion frontend/components/markdown/MarkdownContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function MarkdownContent({
customStyle={{ margin: 0, borderRadius: '0.375rem', fontSize: '0.8125rem' }}
>
{code}
SyntaxHighlighter>
</SyntaxHighlighter>
);
}
return (
Expand Down
Loading
Loading