"use client" import React, { useCallback, useState, useEffect } from 'react' import { motion } from 'framer-motion' import { Button } from './button' import { Separator } from './separator' import { Toggle } from './toggle' import { Popover, PopoverContent, PopoverTrigger } from './popover' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip' import { cn } from '../../lib/utils' import { Bold, Italic, Underline, Strikethrough, AlignLeft, AlignCenter, AlignRight, AlignJustify, List, ListOrdered, Quote, Link, Image as ImageIcon, Code, Type, Palette, Undo, Redo, Eye, Edit } from 'lucide-react' export interface SimpleEditorProps { value?: string onChange?: (value: string) => void placeholder?: string disabled?: boolean className?: string minHeight?: number maxHeight?: number showToolbar?: boolean showPreview?: boolean } interface ToolbarButtonProps { icon: React.ReactNode tooltip: string active?: boolean onClick: () => void disabled?: boolean } const ToolbarButton = ({ icon, tooltip, active, onClick, disabled }: ToolbarButtonProps) => ( onClick()} disabled={disabled} className="h-8 w-8 p-0" > {icon}

{tooltip}

) const ColorPicker = ({ onColorSelect }: { onColorSelect: (color: string) => void }) => { const colors = [ '#000000', '#374151', '#6B7280', '#9CA3AF', '#EF4444', '#F59E0B', '#EAB308', '#22C55E', '#3B82F6', '#6366F1', '#8B5CF6', '#EC4899' ] return (
{colors.map(color => (
) } export const SimpleEditor = React.forwardRef( ({ value = '', onChange, placeholder = 'Start writing...', disabled = false, className, minHeight = 200, maxHeight = 500, showToolbar = true, showPreview = true, ...props }, ref) => { const [content, setContent] = useState(value) const [isPreview, setIsPreview] = useState(false) const [sourceContent, setSourceContent] = useState(value) const [selection, setSelection] = useState<{ start: number; end: number } | null>(null) const editorRef = React.useRef(null) const sourceRef = React.useRef(null) // Format state tracking const [formatState, setFormatState] = useState({ bold: false, italic: false, underline: false, strikethrough: false, alignLeft: true, alignCenter: false, alignRight: false, alignJustify: false, orderedList: false, unorderedList: false, quote: false, code: false }) const updateContent = useCallback((newContent: string) => { setContent(newContent) onChange?.(newContent) }, [onChange]) const saveSelection = useCallback(() => { const selection = window.getSelection() if (selection && selection.rangeCount > 0) { return selection.getRangeAt(0) } return null }, []) const restoreSelection = useCallback((range: Range | null) => { if (!range) return const selection = window.getSelection() if (selection) { selection.removeAllRanges() selection.addRange(range) } }, []) const executeCommand = useCallback((command: string, value?: string) => { if (disabled) return // Focus the editor first if (editorRef.current) { editorRef.current.focus() } // Save current selection before executing command const savedRange = saveSelection() // Execute the command const result = document.execCommand(command, false, value) if (!result) { console.warn(`Command '${command}' failed to execute`) } // Update content state to trigger re-render if (editorRef.current) { const newContent = editorRef.current.innerHTML updateContent(newContent) } // Update format state setFormatState({ bold: document.queryCommandState('bold'), italic: document.queryCommandState('italic'), underline: document.queryCommandState('underline'), strikethrough: document.queryCommandState('strikeThrough'), alignLeft: document.queryCommandState('justifyLeft'), alignCenter: document.queryCommandState('justifyCenter'), alignRight: document.queryCommandState('justifyRight'), alignJustify: document.queryCommandState('justifyFull'), orderedList: document.queryCommandState('insertOrderedList'), unorderedList: document.queryCommandState('insertUnorderedList'), quote: document.queryCommandState('formatBlock'), code: document.queryCommandState('formatBlock') }) // Keep focus on editor if (editorRef.current) { editorRef.current.focus() } }, [disabled, saveSelection, updateContent]) const handleInput = useCallback((e: React.FormEvent) => { const newContent = e.currentTarget.innerHTML updateContent(newContent) }, [updateContent]) const handleKeyDown = useCallback((e: React.KeyboardEvent) => { // Handle keyboard shortcuts if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'b': e.preventDefault() executeCommand('bold') break case 'i': e.preventDefault() executeCommand('italic') break case 'u': e.preventDefault() executeCommand('underline') break case 'z': e.preventDefault() if (e.shiftKey) { executeCommand('redo') } else { executeCommand('undo') } break } } // Handle Tab for indentation if (e.key === 'Tab') { e.preventDefault() executeCommand('insertHTML', '    ') } }, [executeCommand]) const insertLink = useCallback(() => { const url = prompt('Enter URL:') if (url) { executeCommand('createLink', url) } }, [executeCommand]) const insertImage = useCallback(() => { const url = prompt('Enter image URL:') if (url) { executeCommand('insertImage', url) } }, [executeCommand]) const formatHeading = useCallback((level: string) => { executeCommand('formatBlock', ``) }, [executeCommand]) const setTextColor = useCallback((color: string) => { // First try the modern way const selection = window.getSelection() if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0) const span = document.createElement('span') span.style.color = color try { range.surroundContents(span) } catch (e) { // If surroundContents fails, use execCommand executeCommand('foreColor', color) } // Update content if (editorRef.current) { const newContent = editorRef.current.innerHTML updateContent(newContent) } } }, [executeCommand, updateContent]) // Convert HTML to plain text for preview const getPlainText = (html: string) => { if (typeof window === 'undefined') { // Server-side: simple regex-based HTML stripping return html .replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/ /g, ' ') // Replace   with space .replace(/&/g, '&') // Replace & with & .replace(/</g, '<') // Replace < with < .replace(/>/g, '>') // Replace > with > .replace(/"/g, '"') // Replace " with " .replace(/'/g, "'") // Replace ' with ' } // Client-side: use DOM for accurate conversion const div = document.createElement('div') div.innerHTML = html return div.textContent || div.innerText || '' } React.useEffect(() => { if (editorRef.current && content && content !== editorRef.current.innerHTML) { editorRef.current.innerHTML = content } }, [content]) // Sync source content when value prop changes React.useEffect(() => { if (value !== undefined && value !== content) { setContent(value) setSourceContent(value) } }, [value, content]) const toolbar = showToolbar && ( {/* Text Formatting */} {!isPreview && ( <>
} tooltip="Bold (Ctrl+B)" active={formatState.bold} onClick={() => executeCommand('bold')} disabled={disabled} /> } tooltip="Italic (Ctrl+I)" active={formatState.italic} onClick={() => executeCommand('italic')} disabled={disabled} /> } tooltip="Underline (Ctrl+U)" active={formatState.underline} onClick={() => executeCommand('underline')} disabled={disabled} /> } tooltip="Strikethrough" active={formatState.strikethrough} onClick={() => executeCommand('strikeThrough')} disabled={disabled} />
{/* Font Size / Heading */} {/* Alignment */}
} tooltip="Align Left" active={formatState.alignLeft} onClick={() => executeCommand('justifyLeft')} disabled={disabled} /> } tooltip="Align Center" active={formatState.alignCenter} onClick={() => executeCommand('justifyCenter')} disabled={disabled} /> } tooltip="Align Right" active={formatState.alignRight} onClick={() => executeCommand('justifyRight')} disabled={disabled} /> } tooltip="Justify" active={formatState.alignJustify} onClick={() => executeCommand('justifyFull')} disabled={disabled} />
{/* Lists */}
} tooltip="Bullet List" active={formatState.unorderedList} onClick={() => executeCommand('insertUnorderedList')} disabled={disabled} /> } tooltip="Numbered List" active={formatState.orderedList} onClick={() => executeCommand('insertOrderedList')} disabled={disabled} /> } tooltip="Quote" active={formatState.quote} onClick={() => executeCommand('formatBlock', 'blockquote')} disabled={disabled} /> } tooltip="Code Block" active={formatState.code} onClick={() => executeCommand('formatBlock', 'pre')} disabled={disabled} />
{/* Indentation */}
←} tooltip="Decrease Indent" onClick={() => executeCommand('outdent')} disabled={disabled} /> →} tooltip="Increase Indent" onClick={() => executeCommand('indent')} disabled={disabled} />
{/* Insert */}
} tooltip="Insert Link" onClick={insertLink} disabled={disabled} /> } tooltip="Insert Image" onClick={insertImage} disabled={disabled} />
{/* Color */} {/* Undo/Redo */}
} tooltip="Undo (Ctrl+Z)" onClick={() => executeCommand('undo')} disabled={disabled} /> } tooltip="Redo (Ctrl+Shift+Z)" onClick={() => executeCommand('redo')} disabled={disabled} />
)} {/* Preview Toggle */} {showPreview && ( <> {!isPreview && } : } tooltip={isPreview ? "Edit Mode" : "View Source Code"} active={isPreview} onClick={() => { if (isPreview) { // Apply source changes to content when switching back to edit mode updateContent(sourceContent) } else { // Update source content when switching to preview mode setSourceContent(content) } setIsPreview(!isPreview) }} disabled={disabled} /> )}
) return (
{toolbar}
{isPreview ? (