mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Adjusted
This commit is contained in:
@@ -1,18 +1,457 @@
|
||||
// components/generic/CMS/blocks/TextBlock.jsx
|
||||
// components/generic/CMS/Blocks/TextBlock.jsx
|
||||
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Bold, Italic, Underline,
|
||||
AlignLeft, AlignCenter, AlignRight, AlignJustify,
|
||||
List, ListOrdered, Indent, Outdent,
|
||||
Link2, FileText, Table,
|
||||
} from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AssetPickerSheet } from "../AssetPickerSheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function TextBlock({ content, onUpdate }) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label>Content</Label>
|
||||
<Textarea
|
||||
placeholder="Enter text content..."
|
||||
rows={4}
|
||||
value={content.body ?? ""}
|
||||
onChange={(e) => onUpdate({ ...content, body: e.target.value })}
|
||||
/>
|
||||
// ─── Toolbar button ───────────────────────────────────────────────────────────
|
||||
|
||||
function ToolbarBtn({ title, onMouseDown, active, children }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
onMouseDown={onMouseDown}
|
||||
className={cn(
|
||||
"h-7 w-7 flex items-center justify-center rounded text-muted-foreground",
|
||||
"hover:bg-accent hover:text-accent-foreground transition-colors shrink-0",
|
||||
active && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Toolbar divider ──────────────────────────────────────────────────────────
|
||||
|
||||
function Divider() {
|
||||
return <span className="w-px h-4 bg-border mx-0.5 shrink-0" />;
|
||||
}
|
||||
|
||||
// ─── Format options ───────────────────────────────────────────────────────────
|
||||
|
||||
const FORMAT_OPTIONS = [
|
||||
{ label: "Paragraph", val: "p" },
|
||||
{ label: "Heading 1", val: "h1" },
|
||||
{ label: "Heading 2", val: "h2" },
|
||||
{ label: "Heading 3", val: "h3" },
|
||||
];
|
||||
|
||||
// ─── Shared styles ────────────────────────────────────────────────────────────
|
||||
|
||||
export const WYSIWYG_STYLES = `
|
||||
.wysiwyg-editor { line-height: 1.7; }
|
||||
|
||||
.wysiwyg-editor h1, .wysiwyg-preview h1 { font-size: 1.875rem; font-weight: 700; margin: 1.25rem 0 0.5rem; line-height: 1.2; }
|
||||
.wysiwyg-editor h2, .wysiwyg-preview h2 { font-size: 1.5rem; font-weight: 600; margin: 1.25rem 0 0.5rem; line-height: 1.3; }
|
||||
.wysiwyg-editor h3, .wysiwyg-preview h3 { font-size: 1.25rem; font-weight: 600; margin: 1.25rem 0 0.5rem; line-height: 1.4; }
|
||||
|
||||
.wysiwyg-editor p,
|
||||
.wysiwyg-preview p {
|
||||
margin: 0 0 0.85rem 0;
|
||||
text-align: justify;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.wysiwyg-editor ul, .wysiwyg-preview ul { list-style: disc; padding-left: 1.5rem; margin: 0.5rem 0 0.85rem; }
|
||||
.wysiwyg-editor ol, .wysiwyg-preview ol { list-style: decimal; padding-left: 1.5rem; margin: 0.5rem 0 0.85rem; }
|
||||
|
||||
.wysiwyg-editor li, .wysiwyg-preview li {
|
||||
margin-bottom: 0.4rem;
|
||||
line-height: 1.75;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.wysiwyg-editor a, .wysiwyg-preview a { color: hsl(var(--primary)); text-decoration: underline; }
|
||||
|
||||
.wysiwyg-editor a.doc-link,
|
||||
.wysiwyg-preview a.doc-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.8125rem;
|
||||
text-decoration: none;
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
.wysiwyg-editor a.doc-link::before,
|
||||
.wysiwyg-preview a.doc-link::before {
|
||||
content: "📄";
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.wysiwyg-editor table,
|
||||
.wysiwyg-preview table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin: 0.75rem 0;
|
||||
font-size: 0.875rem;
|
||||
table-layout: fixed;
|
||||
border: 1.5px solid #cbd5e1;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wysiwyg-editor th,
|
||||
.wysiwyg-preview th {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.75rem;
|
||||
word-break: break-word;
|
||||
box-shadow: inset -1.5px -1.5px 0 #cbd5e1, inset 1.5px 1.5px 0 #cbd5e1;
|
||||
}
|
||||
|
||||
.wysiwyg-editor td,
|
||||
.wysiwyg-preview td {
|
||||
padding: 0.45rem 0.75rem;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
min-width: 2rem;
|
||||
box-shadow: inset -1.5px -1.5px 0 #cbd5e1, inset 1.5px 1.5px 0 #cbd5e1;
|
||||
}
|
||||
|
||||
.wysiwyg-preview tr:nth-child(even) td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.wysiwyg-editor td:focus,
|
||||
.wysiwyg-editor th:focus {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: -2px;
|
||||
background: hsl(var(--accent) / 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
// ─── Table picker popover ─────────────────────────────────────────────────────
|
||||
// A small grid that lets the user hover to choose rows × columns (up to 8×8),
|
||||
// then click to confirm. Rendered inline in the toolbar.
|
||||
|
||||
const MAX_ROWS = 8;
|
||||
const MAX_COLS = 8;
|
||||
|
||||
function TablePicker({ onInsert, onClose }) {
|
||||
const [hovered, setHovered] = useState({ r: 0, c: 0 });
|
||||
|
||||
return (
|
||||
// Overlay — clicking outside closes the picker without inserting
|
||||
<div className="fixed inset-0 z-50" onMouseDown={onClose}>
|
||||
<div
|
||||
className="absolute z-50 bg-popover border rounded-lg shadow-lg p-3 flex flex-col gap-2"
|
||||
style={{ top: "var(--table-picker-y, 40px)", left: "var(--table-picker-x, 0px)" }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Label */}
|
||||
<p className="text-xs text-muted-foreground select-none">
|
||||
{hovered.r > 0 && hovered.c > 0
|
||||
? `${hovered.r} × ${hovered.c} table`
|
||||
: "Hover to select size"}
|
||||
</p>
|
||||
|
||||
{/* Grid */}
|
||||
<div
|
||||
className="grid gap-0.5"
|
||||
style={{ gridTemplateColumns: `repeat(${MAX_COLS}, 1.25rem)` }}
|
||||
>
|
||||
{Array.from({ length: MAX_ROWS }, (_, r) =>
|
||||
Array.from({ length: MAX_COLS }, (_, c) => (
|
||||
<div
|
||||
key={`${r}-${c}`}
|
||||
onMouseEnter={() => setHovered({ r: r + 1, c: c + 1 })}
|
||||
onClick={() => {
|
||||
onInsert(hovered.r, hovered.c);
|
||||
onClose();
|
||||
}}
|
||||
className={cn(
|
||||
"h-5 w-5 rounded-sm border cursor-pointer transition-colors",
|
||||
r < hovered.r && c < hovered.c
|
||||
? "bg-primary/20 border-primary/50"
|
||||
: "bg-muted border-border"
|
||||
)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── RichTextEditor ───────────────────────────────────────────────────────────
|
||||
|
||||
export function RichTextEditor({ blockId, value, onChange }) {
|
||||
const editorRef = useRef(null);
|
||||
const initializedFor = useRef(null);
|
||||
const savedRange = useRef(null);
|
||||
const tableButtonRef = useRef(null);
|
||||
|
||||
const [docPickerOpen, setDocPickerOpen] = useState(false);
|
||||
const [tablePickerOpen, setTablePickerOpen] = useState(false);
|
||||
|
||||
// ── Seed innerHTML exactly once per blockId ────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
if (initializedFor.current === blockId) return;
|
||||
editorRef.current.innerHTML = value ?? "";
|
||||
initializedFor.current = blockId;
|
||||
}, [blockId, value]);
|
||||
|
||||
// ── Position the table picker below its toolbar button ────────────────────
|
||||
useEffect(() => {
|
||||
if (!tablePickerOpen || !tableButtonRef.current) return;
|
||||
const rect = tableButtonRef.current.getBoundingClientRect();
|
||||
document.documentElement.style.setProperty("--table-picker-y", `${rect.bottom + 6}px`);
|
||||
document.documentElement.style.setProperty("--table-picker-x", `${rect.left}px`);
|
||||
}, [tablePickerOpen]);
|
||||
|
||||
// ── execCommand helpers ────────────────────────────────────────────────────
|
||||
|
||||
const exec = (cmd, val = null) => {
|
||||
editorRef.current?.focus();
|
||||
document.execCommand(cmd, false, val);
|
||||
};
|
||||
|
||||
const applyFormat = (val) => {
|
||||
editorRef.current?.focus();
|
||||
document.execCommand("formatBlock", false, `<${val}>`);
|
||||
};
|
||||
|
||||
const isActive = (cmd) => {
|
||||
try { return document.queryCommandState(cmd); } catch { return false; }
|
||||
};
|
||||
|
||||
// ── URL link ──────────────────────────────────────────────────────────────
|
||||
|
||||
const insertUrlLink = () => {
|
||||
const url = window.prompt("Enter URL:", "https://");
|
||||
if (url) exec("createLink", url);
|
||||
};
|
||||
|
||||
// ── Document link ─────────────────────────────────────────────────────────
|
||||
|
||||
const openDocPicker = () => {
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
savedRange.current = sel.getRangeAt(0).cloneRange();
|
||||
}
|
||||
setDocPickerOpen(true);
|
||||
};
|
||||
|
||||
const handleDocSelect = (asset) => {
|
||||
setDocPickerOpen(false);
|
||||
const url = asset.file_url;
|
||||
const label = asset.display_name ?? "Document";
|
||||
|
||||
editorRef.current?.focus();
|
||||
const sel = window.getSelection();
|
||||
if (savedRange.current && sel) {
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(savedRange.current);
|
||||
}
|
||||
|
||||
const html = `<a href="${url}" class="doc-link" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
||||
document.execCommand("insertHTML", false, html);
|
||||
onChange(editorRef.current.innerHTML);
|
||||
savedRange.current = null;
|
||||
};
|
||||
|
||||
// ── Table insertion ────────────────────────────────────────────────────────
|
||||
// Builds a <table> with a header row (th) + (rows-1) data rows.
|
||||
// Each cell is contenteditable (inherited from the editor).
|
||||
// A paragraph after the table lets the user continue typing below it.
|
||||
|
||||
const insertTable = (rows, cols) => {
|
||||
if (rows < 1 || cols < 1) return;
|
||||
|
||||
// Save cursor before opening picker collapses it
|
||||
editorRef.current?.focus();
|
||||
const sel = window.getSelection();
|
||||
if (savedRange.current && sel) {
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(savedRange.current);
|
||||
}
|
||||
|
||||
// Build header row
|
||||
const headerCells = Array.from({ length: cols })
|
||||
.map((_, i) => `<th>Column ${i + 1}</th>`)
|
||||
.join("");
|
||||
|
||||
// Build body rows
|
||||
const bodyRows = Array.from({ length: rows - 1 })
|
||||
.map(() =>
|
||||
`<tr>${Array.from({ length: cols }).map(() => "<td><br></td>").join("")}</tr>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const tableHTML = `
|
||||
<table>
|
||||
<thead><tr>${headerCells}</tr></thead>
|
||||
<tbody>${bodyRows}</tbody>
|
||||
</table>
|
||||
<p><br></p>
|
||||
`;
|
||||
|
||||
document.execCommand("insertHTML", false, tableHTML);
|
||||
onChange(editorRef.current.innerHTML);
|
||||
savedRange.current = null;
|
||||
};
|
||||
|
||||
const openTablePicker = () => {
|
||||
// Save current cursor so we can restore it after the picker closes
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
savedRange.current = sel.getRangeAt(0).cloneRange();
|
||||
}
|
||||
setTablePickerOpen(true);
|
||||
};
|
||||
|
||||
// ── Toolbar groups ─────────────────────────────────────────────────────────
|
||||
|
||||
const GROUPS = [
|
||||
[
|
||||
{ cmd: "bold", Icon: Bold, title: "Bold" },
|
||||
{ cmd: "italic", Icon: Italic, title: "Italic" },
|
||||
{ cmd: "underline", Icon: Underline, title: "Underline" },
|
||||
],
|
||||
[
|
||||
{ cmd: "justifyLeft", Icon: AlignLeft, title: "Align left" },
|
||||
{ cmd: "justifyCenter", Icon: AlignCenter, title: "Align center" },
|
||||
{ cmd: "justifyRight", Icon: AlignRight, title: "Align right" },
|
||||
{ cmd: "justifyFull", Icon: AlignJustify, title: "Justify" },
|
||||
],
|
||||
[
|
||||
{ cmd: "insertUnorderedList", Icon: List, title: "Bullet list" },
|
||||
{ cmd: "insertOrderedList", Icon: ListOrdered, title: "Numbered list" },
|
||||
{ cmd: "indent", Icon: Indent, title: "Indent" },
|
||||
{ cmd: "outdent", Icon: Outdent, title: "Outdent" },
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border rounded-md overflow-hidden focus-within:ring-2 focus-within:ring-ring">
|
||||
|
||||
{/* ── Toolbar ── */}
|
||||
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b bg-muted/40">
|
||||
|
||||
{/* Block format select */}
|
||||
<select
|
||||
className="h-7 text-xs border rounded px-1 mr-0.5 bg-background cursor-pointer shrink-0"
|
||||
defaultValue="p"
|
||||
onChange={(e) => applyFormat(e.target.value)}
|
||||
>
|
||||
{FORMAT_OPTIONS.map((o) => (
|
||||
<option key={o.val} value={o.val}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Formatting groups */}
|
||||
{GROUPS.map((group, gi) => (
|
||||
<div key={gi} className="flex items-center gap-0.5">
|
||||
{group.map(({ cmd, Icon, title }) => (
|
||||
<ToolbarBtn
|
||||
key={cmd}
|
||||
title={title}
|
||||
active={isActive(cmd)}
|
||||
onMouseDown={(e) => { e.preventDefault(); exec(cmd); }}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</ToolbarBtn>
|
||||
))}
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* URL link */}
|
||||
<ToolbarBtn
|
||||
title="Insert URL link"
|
||||
onMouseDown={(e) => { e.preventDefault(); insertUrlLink(); }}
|
||||
>
|
||||
<Link2 className="h-3.5 w-3.5" />
|
||||
</ToolbarBtn>
|
||||
|
||||
{/* Document link */}
|
||||
<ToolbarBtn
|
||||
title="Embed document link"
|
||||
onMouseDown={(e) => { e.preventDefault(); openDocPicker(); }}
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
</ToolbarBtn>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Insert table */}
|
||||
<ToolbarBtn
|
||||
title="Insert table"
|
||||
active={tablePickerOpen}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
tablePickerOpen ? setTablePickerOpen(false) : openTablePicker();
|
||||
}}
|
||||
>
|
||||
<span ref={tableButtonRef} className="flex items-center justify-center">
|
||||
<Table className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</ToolbarBtn>
|
||||
</div>
|
||||
|
||||
{/* ── Editable area ── */}
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
onInput={() => onChange(editorRef.current.innerHTML)}
|
||||
className="wysiwyg-editor min-h-[120px] px-3 m-0 py-0 text-sm focus:outline-none"
|
||||
/>
|
||||
|
||||
<style>{WYSIWYG_STYLES}</style>
|
||||
</div>
|
||||
|
||||
{/* Table size picker */}
|
||||
{tablePickerOpen && (
|
||||
<TablePicker
|
||||
onInsert={insertTable}
|
||||
onClose={() => setTablePickerOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Document asset picker */}
|
||||
<AssetPickerSheet
|
||||
open={docPickerOpen}
|
||||
onOpenChange={setDocPickerOpen}
|
||||
fileType="document"
|
||||
onSelect={handleDocSelect}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── TextBlock ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function TextBlock({ content, onUpdate, blockId }) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label>Content</Label>
|
||||
<RichTextEditor
|
||||
blockId={blockId}
|
||||
value={content.body ?? ""}
|
||||
onChange={(html) => onUpdate({ ...content, body: html })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
// components/generic/CMS/blocks/TextImageBlock.jsx
|
||||
// components/generic/CMS/Blocks/TextImageBlock.jsx
|
||||
|
||||
import { useState } from "react";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -12,15 +11,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { RichTextEditor } from "./TextBlock"; // reuse the shared editor
|
||||
import { AssetPickerSheet } from "../AssetPickerSheet";
|
||||
|
||||
export function TextImageBlock({ content, onUpdate }) {
|
||||
export function TextImageBlock({ content, onUpdate, blockId }) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* ── Layout ── */}
|
||||
{/* ── Image position ── */}
|
||||
<div className="space-y-1.5 max-w-[200px]">
|
||||
<Label>Image Position</Label>
|
||||
<Select
|
||||
@@ -35,19 +35,15 @@ export function TextImageBlock({ content, onUpdate }) {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className={[
|
||||
"grid gap-4",
|
||||
"grid-cols-1 md:grid-cols-2",
|
||||
].join(" ")}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
{/* ── Text side ── */}
|
||||
{/* ── Text side — WYSIWYG ── */}
|
||||
<div className="space-y-1.5">
|
||||
<Label>Content</Label>
|
||||
<Textarea
|
||||
placeholder="Enter text content..."
|
||||
rows={5}
|
||||
<RichTextEditor
|
||||
blockId={`${blockId}_text`}
|
||||
value={content.body ?? ""}
|
||||
onChange={(e) => onUpdate({ ...content, body: e.target.value })}
|
||||
onChange={(html) => onUpdate({ ...content, body: html })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -108,4 +104,4 @@ export function TextImageBlock({ content, onUpdate }) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
// components/generic/CMS/Blocks/TextVideoBlock.jsx
|
||||
|
||||
import { useState } from "react";
|
||||
import { VideoIcon } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -9,9 +10,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { RichTextEditor } from "./TextBlock"; // reuse the shared editor
|
||||
import { AssetPickerSheet } from "../AssetPickerSheet";
|
||||
|
||||
export function TextVideoBlock({ content, onUpdate }) {
|
||||
export function TextVideoBlock({ content, onUpdate, blockId }) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
const thumb = content.thumbnail_url ?? null;
|
||||
@@ -19,7 +21,7 @@ export function TextVideoBlock({ content, onUpdate }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* ── Layout ── */}
|
||||
{/* ── Video position ── */}
|
||||
<div className="space-y-1.5 max-w-[200px]">
|
||||
<Label>Video Position</Label>
|
||||
<Select
|
||||
@@ -36,14 +38,13 @@ export function TextVideoBlock({ content, onUpdate }) {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
{/* ── Text side ── */}
|
||||
{/* ── Text side — WYSIWYG ── */}
|
||||
<div className="space-y-1.5">
|
||||
<Label>Content</Label>
|
||||
<Textarea
|
||||
placeholder="Enter text content..."
|
||||
rows={5}
|
||||
<RichTextEditor
|
||||
blockId={`${blockId}_text`}
|
||||
value={content.body ?? ""}
|
||||
onChange={(e) => onUpdate({ ...content, body: e.target.value })}
|
||||
onChange={(html) => onUpdate({ ...content, body: html })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -105,4 +106,4 @@ export function TextVideoBlock({ content, onUpdate }) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user