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
7 changes: 7 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,10 @@
text-align: left;
}
.markdown-body th { font-weight: 600; background-color: var(--muted); }
.markdown-body img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 0.5rem 0;
display: block;
}
75 changes: 73 additions & 2 deletions src/components/markdown-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
"use client";

import { useState } from "react";
import { useRef, useState } from "react";
import type { ReactNode } from "react";
import ReactMarkdown from "react-markdown";
import { cn } from "@/lib/utils";

// Imagem vira base64 embutido no próprio markdown (data URL) — sem upload,
// sem depender de storage externo. Guarda-chuva de 2MB no arquivo original
// pra não inflar o campo de texto com um base64 gigante (~2.7MB de texto).
const MAX_PASTED_IMAGE_BYTES = 2 * 1024 * 1024;

function fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);

Check warning on line 17 in src/components/markdown-editor.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Expected the Promise rejection reason to be an Error.

See more on https://sonarcloud.io/project/issues?id=devs-tocantins_front-engajamento&issues=AZ-06Oodpueqzc71czeB&open=AZ-06Oodpueqzc71czeB&pullRequest=56
reader.readAsDataURL(file);
});
}

// ─── Markdown renderer ────────────────────────────────────────────────────────

export function MarkdownContent({
Expand Down Expand Up @@ -43,6 +57,51 @@
maxLength,
}: MarkdownEditorProps) {
const [preview, setPreview] = useState(false);
const [pasteError, setPasteError] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);

const insertAtCursor = (text: string) => {
const el = textareaRef.current;
if (!el) {
onChange(value + text);
return;
}
const start = el.selectionStart ?? value.length;
const end = el.selectionEnd ?? value.length;
const next = value.slice(0, start) + text + value.slice(end);
onChange(next);
requestAnimationFrame(() => {
el.focus();
const cursor = start + text.length;
el.setSelectionRange(cursor, cursor);
});
};

const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
const imageItem = Array.from(e.clipboardData.items).find((item) =>
item.type.startsWith("image/")
);
if (!imageItem) return;

e.preventDefault();
setPasteError("");
const file = imageItem.getAsFile();
if (!file) return;

if (file.size > MAX_PASTED_IMAGE_BYTES) {
setPasteError(
"Imagem muito grande (máx. 2 MB). Reduza o tamanho e tente colar de novo."
);
return;
}

try {
const dataUrl = await fileToDataUrl(file);
insertAtCursor(`\n![imagem](${dataUrl})\n`);
} catch {
setPasteError("Não foi possível colar a imagem. Tente novamente.");
}
};

const toggle = (
<div className="flex rounded-md border overflow-hidden text-xs shrink-0">
Expand Down Expand Up @@ -115,8 +174,13 @@
</div>
) : (
<textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
onChange={(e) => {
onChange(e.target.value);
if (pasteError) setPasteError("");
}}
onPaste={handlePaste}
placeholder={placeholder}
rows={rows}
onInput={(e) => {
Expand All @@ -133,6 +197,13 @@
/>
)}

{!preview && (
<p className="text-xs text-muted-foreground">
Dica: cole (Ctrl+V) uma imagem copiada pra inserir direto no texto.
</p>
)}

{pasteError && <p className="text-xs text-destructive">{pasteError}</p>}
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
);
Expand Down
Loading