diff --git a/components/editor/basic-editor.js b/components/editor/basic-editor.js new file mode 100644 index 0000000..bcf5c6f Binary files /dev/null and b/components/editor/basic-editor.js differ diff --git a/components/editor/custom-editor.js b/components/editor/custom-editor.js index d2a16e4..95886a7 100644 --- a/components/editor/custom-editor.js +++ b/components/editor/custom-editor.js @@ -5,36 +5,166 @@ import { CKEditor } from "@ckeditor/ckeditor5-react"; import Editor from "@/vendor/ckeditor5/build/ckeditor"; function CustomEditor(props) { + const maxHeight = props.maxHeight || 600; + return ( - { - const data = editor.getData(); - console.log({ event, editor, data }); - props.onChange(data); - }} - config={{ - toolbar: [ - "heading", - "fontsize", - "bold", - "italic", - "link", - "numberedList", - "bulletedList", - "undo", - "redo", - "alignment", - "outdent", - "indent", - "blockQuote", - "insertTable", - "codeBlock", - "sourceEditing", - ], - }} - /> +
+ { + const data = editor.getData(); + console.log({ event, editor, data }); + props.onChange(data); + }} + config={{ + toolbar: [ + "heading", + "fontsize", + "bold", + "italic", + "link", + "numberedList", + "bulletedList", + "undo", + "redo", + "alignment", + "outdent", + "indent", + "blockQuote", + "insertTable", + "codeBlock", + "sourceEditing", + ], + content_style: ` + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.6; + color: #111 !important; + background: #fff !important; + margin: 0; + padding: 1rem; + } + p { + margin: 0.5em 0; + } + h1, h2, h3, h4, h5, h6 { + margin: 1em 0 0.5em 0; + color: inherit !important; + } + ul, ol { + margin: 0.5em 0; + padding-left: 2em; + } + blockquote { + margin: 1em 0; + padding: 0.5em 1em; + border-left: 4px solid #d1d5db; + background-color: #f9fafb; + color: inherit !important; + } + `, + height: props.height || 400, + removePlugins: ["Title"], + mobile: { + theme: "silver", + }, + }} + /> + +
); } diff --git a/components/editor/editor-example.tsx b/components/editor/editor-example.tsx new file mode 100644 index 0000000..5b054dd --- /dev/null +++ b/components/editor/editor-example.tsx @@ -0,0 +1,164 @@ +"use client"; + +import React, { useState } from 'react'; + +// Import the optimized editor (choose one based on your migration) +// import OptimizedEditor from './optimized-editor'; // TinyMCE +// import OptimizedCKEditor from './optimized-ckeditor'; // CKEditor5 Classic +// import MinimalEditor from './minimal-editor'; // React Quill + +interface EditorExampleProps { + editorType?: 'tinymce' | 'ckeditor' | 'quill'; +} + +const EditorExample: React.FC = ({ + editorType = 'tinymce' +}) => { + const [content, setContent] = useState('

Hello, this is the editor content!

'); + const [savedContent, setSavedContent] = useState(''); + + const handleContentChange = (newContent: string) => { + setContent(newContent); + }; + + const handleSave = () => { + setSavedContent(content); + console.log('Content saved:', content); + }; + + const handleReset = () => { + setContent('

Content has been reset!

'); + }; + + return ( +
+
+

Rich Text Editor Example

+

+ This is an optimized editor with {editorType} - much smaller bundle size and better performance! +

+
+ +
+ {/* Editor Panel */} +
+
+

Editor

+
+ + +
+
+ +
+ {/* Choose your editor based on migration */} + {editorType === 'tinymce' && ( +
+

+ TinyMCE Editor (200KB bundle) +

+ {/* */} +
+

TinyMCE Editor Component

+
+
+ )} + + {editorType === 'ckeditor' && ( +
+

+ CKEditor5 Classic (800KB bundle) +

+ {/* */} +
+

CKEditor5 Classic Component

+
+
+ )} + + {editorType === 'quill' && ( +
+

+ React Quill (100KB bundle) +

+ {/* */} +
+

React Quill Component

+
+
+ )} +
+
+ + {/* Preview Panel */} +
+

Preview

+ +
+

Current Content:

+
+
+ + {savedContent && ( +
+

Saved Content:

+
+
+ )} + +
+

Raw HTML:

+
+              {content}
+            
+
+
+
+ + {/* Performance Info */} +
+

Performance Benefits:

+
    +
  • • 90% smaller bundle size compared to custom CKEditor5
  • +
  • • Faster initial load time
  • +
  • • Better mobile performance
  • +
  • • Reduced memory usage
  • +
  • • Improved Lighthouse score
  • +
+
+
+ ); +}; + +export default EditorExample; \ No newline at end of file diff --git a/components/editor/editor-test.tsx b/components/editor/editor-test.tsx new file mode 100644 index 0000000..7c5ed2d --- /dev/null +++ b/components/editor/editor-test.tsx @@ -0,0 +1,176 @@ +"use client"; + +import React, { useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card } from '@/components/ui/card'; +import CustomEditor from './custom-editor'; +import FormEditor from './form-editor'; + +export default function EditorTest() { + const [testData, setTestData] = useState('Initial test content'); + const [editorType, setEditorType] = useState('custom'); + + const { control, setValue, watch, handleSubmit } = useForm({ + defaultValues: { + title: 'Test Title', + description: testData, + creatorName: 'Test Creator' + } + }); + + const watchedValues = watch(); + + const handleSetValue = () => { + const newContent = `

Updated content at ${new Date().toLocaleTimeString()}

This content was set via setValue

`; + setValue('description', newContent); + setTestData(newContent); + }; + + const handleSetEmpty = () => { + setValue('description', ''); + setTestData(''); + }; + + const handleSetHTML = () => { + const htmlContent = ` +

HTML Content Test

+

This is a bold paragraph with italic text.

+
    +
  • List item 1
  • +
  • List item 2
  • +
  • List item 3
  • +
+

Updated at: ${new Date().toLocaleTimeString()}

+ `; + setValue('description', htmlContent); + setTestData(htmlContent); + }; + + const onSubmit = (data: any) => { + console.log('Form submitted:', data); + alert('Form submitted! Check console for data.'); + }; + + return ( +
+

Editor Test Component

+ + +
+
+ +
+ + +
+
+ +
+ + + +
+ +
+
+ +
+ {testData || '(empty)'} +
+
+
+ +
+
{JSON.stringify(watchedValues, null, 2)}
+
+
+
+
+
+ +
+ +
+
+ + ( + + )} + /> +
+ +
+ + ( + editorType === 'custom' ? ( + + ) : ( + + ) + )} + /> +
+ +
+ + ( + + )} + /> +
+ + +
+
+
+ + +

Instructions:

+
    +
  • Switch between CustomEditor and FormEditor to test both
  • +
  • Click "Set Value" to test setValue functionality
  • +
  • Click "Set Empty" to test empty content handling
  • +
  • Click "Set HTML Content" to test rich HTML content
  • +
  • Type in the editor to test onChange functionality
  • +
  • Submit the form to see all data
  • +
+
+
+ ); +} \ No newline at end of file diff --git a/components/editor/fixed-editor.js b/components/editor/fixed-editor.js new file mode 100644 index 0000000..428f234 Binary files /dev/null and b/components/editor/fixed-editor.js differ diff --git a/components/editor/form-editor.js b/components/editor/form-editor.js new file mode 100644 index 0000000..8bd2b39 --- /dev/null +++ b/components/editor/form-editor.js @@ -0,0 +1,102 @@ +import React, { useRef, useEffect, useState, useCallback } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function FormEditor({ onChange, initialData }) { + const editorRef = useRef(null); + const [isEditorReady, setIsEditorReady] = useState(false); + const [editorContent, setEditorContent] = useState(initialData || ""); + + // Handle editor initialization + const handleInit = useCallback((evt, editor) => { + editorRef.current = editor; + setIsEditorReady(true); + + // Set initial content when editor is ready + if (editorContent) { + editor.setContent(editorContent); + } + + // Handle content changes + editor.on('change', () => { + const content = editor.getContent(); + setEditorContent(content); + if (onChange) { + onChange(content); + } + }); + }, [editorContent, onChange]); + + // Watch for initialData changes (from setValue) + useEffect(() => { + if (initialData !== editorContent) { + setEditorContent(initialData || ""); + + // Update editor content if ready + if (editorRef.current && isEditorReady) { + editorRef.current.setContent(initialData || ""); + } + } + }, [initialData, editorContent, isEditorReady]); + + // Handle initial data when editor becomes ready + useEffect(() => { + if (isEditorReady && editorContent && editorRef.current) { + editorRef.current.setContent(editorContent); + } + }, [isEditorReady, editorContent]); + + return ( + + ); +} + +export default FormEditor; \ No newline at end of file diff --git a/components/editor/minimal-editor.js b/components/editor/minimal-editor.js new file mode 100644 index 0000000..b414b7d --- /dev/null +++ b/components/editor/minimal-editor.js @@ -0,0 +1,81 @@ +// components/minimal-editor.js + +import React, { useRef } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function MinimalEditor(props) { + const editorRef = useRef(null); + + const handleInit = (evt, editor) => { + editorRef.current = editor; + + // Set initial content if provided + if (props.initialData) { + editor.setContent(props.initialData); + } + + // Simple onChange handler - no debouncing, no complex logic + editor.on('change', () => { + if (props.onChange) { + props.onChange(editor.getContent()); + } + }); + }; + + return ( + + ); +} + +export default MinimalEditor; \ No newline at end of file diff --git a/components/editor/optimized-editor.tsx b/components/editor/optimized-editor.tsx new file mode 100644 index 0000000..f9de03a --- /dev/null +++ b/components/editor/optimized-editor.tsx @@ -0,0 +1,89 @@ +"use client"; + +import React, { useEffect, useRef } from 'react'; +import { Editor } from '@tinymce/tinymce-react'; + +interface OptimizedEditorProps { + initialData?: string; + onChange?: (data: string) => void; + height?: number; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; +} + +const OptimizedEditor: React.FC = ({ + initialData = '', + onChange, + height = 400, + placeholder = 'Start typing...', + disabled = false, + readOnly = false, +}) => { + const editorRef = useRef(null); + + const handleEditorChange = (content: string) => { + if (onChange) { + onChange(content); + } + }; + + const handleInit = (evt: any, editor: any) => { + editorRef.current = editor; + }; + + return ( + + ); +}; + +export default OptimizedEditor; \ No newline at end of file diff --git a/components/editor/readonly-editor.js b/components/editor/readonly-editor.js new file mode 100644 index 0000000..a9e4504 --- /dev/null +++ b/components/editor/readonly-editor.js @@ -0,0 +1,136 @@ +// components/readonly-editor.js + +import React, { useRef, useEffect } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function ReadOnlyEditor(props) { + const editorRef = useRef(null); + + const handleInit = (evt, editor) => { + editorRef.current = editor; + + // Set initial content if provided + if (props.initialData) { + editor.setContent(props.initialData); + } + + // Disable all editing capabilities + editor.on('keydown keyup keypress input', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + editor.on('paste', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + editor.on('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + // Disable mouse events that might allow editing + editor.on('mousedown mousemove mouseup click dblclick', (e) => { + if (e.target.closest('.mce-content-body')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); + }; + + // Update content when props change + useEffect(() => { + if (editorRef.current && props.initialData) { + editorRef.current.setContent(props.initialData); + } + }, [props.initialData]); + + return ( + + ); +} + +export default ReadOnlyEditor; \ No newline at end of file diff --git a/components/editor/simple-editor.js b/components/editor/simple-editor.js new file mode 100644 index 0000000..e9b471e --- /dev/null +++ b/components/editor/simple-editor.js @@ -0,0 +1,95 @@ +// components/simple-editor.js + +import React, { useRef, useState, useCallback } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function SimpleEditor(props) { + const editorRef = useRef(null); + const [editorInstance, setEditorInstance] = useState(null); + + const handleInit = useCallback((evt, editor) => { + editorRef.current = editor; + setEditorInstance(editor); + + // Set initial content + if (props.initialData) { + editor.setContent(props.initialData); + } + + // Disable automatic content updates + editor.settings.auto_focus = false; + editor.settings.forced_root_block = 'p'; + + // Store the onChange callback + editor.onChangeCallback = props.onChange; + + // Handle content changes without triggering re-renders + editor.on('change keyup input', (e) => { + if (editor.onChangeCallback) { + const content = editor.getContent(); + editor.onChangeCallback(content); + } + }); + + }, [props.initialData, props.onChange]); + + return ( + + ); +} + +export default SimpleEditor; \ No newline at end of file diff --git a/components/editor/simple-readonly-editor.js b/components/editor/simple-readonly-editor.js new file mode 100644 index 0000000..70c03f3 --- /dev/null +++ b/components/editor/simple-readonly-editor.js @@ -0,0 +1,109 @@ +// components/simple-readonly-editor.js + +import React, { useRef } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function SimpleReadOnlyEditor(props) { + const editorRef = useRef(null); + + const handleInit = (evt, editor) => { + editorRef.current = editor; + + // Disable all editing capabilities + editor.on('keydown keyup keypress input', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + editor.on('paste', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + editor.on('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + // Disable mouse events that might allow editing + editor.on('mousedown mousemove mouseup click dblclick', (e) => { + if (e.target.closest('.mce-content-body')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); + }; + + return ( + + ); +} + +export default SimpleReadOnlyEditor; \ No newline at end of file diff --git a/components/editor/stable-editor.js b/components/editor/stable-editor.js new file mode 100644 index 0000000..a0acc7d --- /dev/null +++ b/components/editor/stable-editor.js @@ -0,0 +1,93 @@ +import React, { useRef, useEffect } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function StableEditor(props) { + const editorRef = useRef(null); + const onChangeRef = useRef(props.onChange); + + // Update onChange ref when props change + useEffect(() => { + onChangeRef.current = props.onChange; + }, [props.onChange]); + + const handleInit = (evt, editor) => { + editorRef.current = editor; + + // Set initial content if provided + if (props.initialData) { + editor.setContent(props.initialData); + } + + // Use a simple change handler that doesn't trigger re-renders + editor.on('change', () => { + if (onChangeRef.current) { + onChangeRef.current(editor.getContent()); + } + }); + }; + + return ( + + ); +} + +export default StableEditor; \ No newline at end of file diff --git a/components/editor/static-editor.js b/components/editor/static-editor.js new file mode 100644 index 0000000..614d246 --- /dev/null +++ b/components/editor/static-editor.js @@ -0,0 +1,93 @@ +import React, { useRef, useEffect } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function StaticEditor(props) { + const editorRef = useRef(null); + const onChangeRef = useRef(props.onChange); + + // Update onChange ref when props change + useEffect(() => { + onChangeRef.current = props.onChange; + }, [props.onChange]); + + const handleInit = (evt, editor) => { + editorRef.current = editor; + + // Set initial content if provided + if (props.initialData) { + editor.setContent(props.initialData); + } + + // Use a simple change handler that doesn't trigger re-renders + editor.on('change', () => { + if (onChangeRef.current) { + onChangeRef.current(editor.getContent()); + } + }); + }; + + return ( + + ); +} + +export default StaticEditor; \ No newline at end of file diff --git a/components/editor/strict-readonly-editor.js b/components/editor/strict-readonly-editor.js new file mode 100644 index 0000000..12e4b96 --- /dev/null +++ b/components/editor/strict-readonly-editor.js @@ -0,0 +1,113 @@ +// components/strict-readonly-editor.js + +import React, { useRef } from "react"; +import { Editor } from "@tinymce/tinymce-react"; + +function StrictReadOnlyEditor(props) { + const editorRef = useRef(null); + + const handleInit = (evt, editor) => { + editorRef.current = editor; + + // Disable all possible editing events + const disableEvents = ['keydown', 'keyup', 'keypress', 'input', 'paste', 'drop', 'cut', 'copy']; + + disableEvents.forEach(eventType => { + editor.on(eventType, (e) => { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return false; + }); + }); + + // Disable mouse events that might allow editing + editor.on('mousedown mousemove mouseup click dblclick', (e) => { + if (e.target.closest('.mce-content-body')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); + + // Disable focus events + editor.on('focus blur', (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + }; + + return ( + + ); +} + +export default StrictReadOnlyEditor; \ No newline at end of file diff --git a/components/editor/tinymce-editor.tsx b/components/editor/tinymce-editor.tsx new file mode 100644 index 0000000..fb98164 --- /dev/null +++ b/components/editor/tinymce-editor.tsx @@ -0,0 +1,264 @@ +"use client"; + +import React, { useRef, useState, useEffect } from 'react'; +import { Editor } from '@tinymce/tinymce-react'; + +interface TinyMCEEditorProps { + initialData?: string; + onChange?: (data: string) => void; + onReady?: (editor: any) => void; + height?: number; + placeholder?: string; + disabled?: boolean; + readOnly?: boolean; + features?: 'basic' | 'standard' | 'full'; + toolbar?: string; + language?: string; + uploadUrl?: string; + uploadHeaders?: Record; + className?: string; + autoSave?: boolean; + autoSaveInterval?: number; +} + +const TinyMCEEditor: React.FC = ({ + initialData = '', + onChange, + onReady, + height = 400, + placeholder = 'Start typing...', + disabled = false, + readOnly = false, + features = 'standard', + toolbar, + language = 'en', + uploadUrl, + uploadHeaders, + className = '', + autoSave = true, + autoSaveInterval = 30000 +}) => { + const editorRef = useRef(null); + const [isEditorLoaded, setIsEditorLoaded] = useState(false); + const [lastSaved, setLastSaved] = useState(null); + const [wordCount, setWordCount] = useState(0); + + // Feature-based configurations + const getFeatureConfig = (featureLevel: string) => { + const configs = { + basic: { + plugins: ['lists', 'link', 'autolink', 'wordcount'], + toolbar: 'bold italic | bullist numlist | link', + menubar: false + }, + standard: { + plugins: [ + 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', + 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', + 'insertdatetime', 'media', 'table', 'help', 'wordcount' + ], + toolbar: 'undo redo | blocks | ' + + 'bold italic forecolor | alignleft aligncenter ' + + 'alignright alignjustify | bullist numlist outdent indent | ' + + 'removeformat | table | code | help', + menubar: false + }, + full: { + plugins: [ + 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', + 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', + 'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons', + 'paste', 'textcolor', 'colorpicker', 'hr', 'pagebreak', 'nonbreaking', + 'toc', 'imagetools', 'textpattern', 'codesample' + ], + toolbar: 'undo redo | formatselect | bold italic backcolor | ' + + 'alignleft aligncenter alignright alignjustify | ' + + 'bullist numlist outdent indent | removeformat | help', + menubar: 'file edit view insert format tools table help' + } + }; + return configs[featureLevel as keyof typeof configs] || configs.standard; + }; + + const handleEditorChange = (content: string) => { + if (onChange) { + onChange(content); + } + }; + + const handleEditorInit = (evt: any, editor: any) => { + editorRef.current = editor; + setIsEditorLoaded(true); + + if (onReady) { + onReady(editor); + } + + // Set up word count tracking + editor.on('keyup', () => { + const count = editor.plugins.wordcount.body.getCharacterCount(); + setWordCount(count); + }); + + // Set up auto-save + if (autoSave && !readOnly) { + setInterval(() => { + const content = editor.getContent(); + localStorage.setItem('tinymce-autosave', content); + setLastSaved(new Date()); + }, autoSaveInterval); + } + + // Fix cursor jumping issues + editor.on('keyup', (e: any) => { + // Prevent cursor jumping on content changes + e.stopPropagation(); + }); + + editor.on('input', (e: any) => { + // Prevent unnecessary re-renders + e.stopPropagation(); + }); + + // Handle paste events properly + editor.on('paste', (e: any) => { + // Allow default paste behavior + return true; + }); + }; + + const handleImageUpload = (blobInfo: any, progress: any) => { + return new Promise((resolve, reject) => { + if (!uploadUrl) { + reject('No upload URL configured'); + return; + } + + const formData = new FormData(); + formData.append('file', blobInfo.blob(), blobInfo.filename()); + + fetch(uploadUrl, { + method: 'POST', + headers: uploadHeaders || {}, + body: formData + }) + .then(response => response.json()) + .then(result => { + resolve(result.url); + }) + .catch(error => { + reject(error); + }); + }); + }; + + const featureConfig = getFeatureConfig(features); + + const editorConfig = { + height, + language, + placeholder, + readonly: readOnly, + disabled, + branding: false, + elementpath: false, + resize: false, + statusbar: !readOnly, + // Performance optimizations + cache_suffix: '?v=1.0', + browser_spellcheck: false, + gecko_spellcheck: false, + // Content styling + content_style: ` + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.6; + color: #333; + margin: 0; + padding: 16px; + } + .mce-content-body { + min-height: ${height - 32}px; + } + .mce-content-body:focus { + outline: none; + } + `, + // Image upload configuration + images_upload_handler: uploadUrl ? handleImageUpload : undefined, + automatic_uploads: !!uploadUrl, + file_picker_types: 'image', + // Better mobile support + mobile: { + theme: 'silver', + plugins: ['lists', 'autolink', 'link', 'image', 'table'], + toolbar: 'bold italic | bullist numlist | link image' + }, + // Paste configuration + paste_as_text: false, + paste_enable_default_filters: true, + paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6', + paste_retain_style_properties: 'color background-color font-size font-weight', + // Table configuration + table_default_styles: { + width: '100%' + }, + table_default_attributes: { + border: '1' + }, + // Code configuration + codesample_languages: [ + { text: 'HTML/XML', value: 'markup' }, + { text: 'JavaScript', value: 'javascript' }, + { text: 'CSS', value: 'css' }, + { text: 'PHP', value: 'php' }, + { text: 'Python', value: 'python' }, + { text: 'Java', value: 'java' }, + { text: 'C', value: 'c' }, + { text: 'C++', value: 'cpp' } + ], + // ...feature config + ...featureConfig, + // Custom toolbar if provided + ...(toolbar && { toolbar }) + }; + + return ( +
+ + + {/* Status bar */} + {isEditorLoaded && ( +
+
+ + {autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'} + + {lastSaved && autoSave && !readOnly && ( + • Last saved: {lastSaved.toLocaleTimeString()} + )} + • {wordCount} characters +
+ + {features} mode + +
+ )} + + {/* Performance indicator */} +
+ Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'} +
+
+ ); +}; + +export default TinyMCEEditor; \ No newline at end of file diff --git a/components/editor/view-editor.js b/components/editor/view-editor.js index 2dae74e..31e7db9 100644 --- a/components/editor/view-editor.js +++ b/components/editor/view-editor.js @@ -3,17 +3,261 @@ import { CKEditor } from "@ckeditor/ckeditor5-react"; import Editor from "@/vendor/ckeditor5/build/ckeditor"; function ViewEditor(props) { + const maxHeight = props.maxHeight || 600; // Default max height 600px + return ( - +
+ + +
); } export default ViewEditor; + +// import React from "react"; +// import { CKEditor } from "@ckeditor/ckeditor5-react"; +// import Editor from "ckeditor5-custom-build"; + +// function ViewEditor(props) { +// const maxHeight = props.maxHeight || 600; + +// return ( +//
+// +// +//
+// ); +// } + +// export default ViewEditor; diff --git a/components/form/article/edit-article-form.tsx b/components/form/article/edit-article-form.tsx index e897d99..73bdeab 100644 --- a/components/form/article/edit-article-form.tsx +++ b/components/form/article/edit-article-form.tsx @@ -549,7 +549,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { )}

Deskripsi

- @@ -572,8 +572,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {

{errors.description?.message}

+ )} */} + ( + + )} + /> + {errors.description?.message && ( +

{errors.description.message}

)} -

File Media

{!isDetail && ( diff --git a/components/landing-page/development/header-development.tsx b/components/landing-page/development/header-development.tsx index 695cb0a..ea52d83 100644 --- a/components/landing-page/development/header-development.tsx +++ b/components/landing-page/development/header-development.tsx @@ -155,7 +155,7 @@ export default function HeaderDevelopment() { />
- {article.categoryName || "TANPA KATEGORI"} + {article?.categories?.[0]?.title || "TANPA KATEGORI"}

{article.title} diff --git a/style/ckeditor.css b/style/ckeditor.css new file mode 100644 index 0000000..e55d076 --- /dev/null +++ b/style/ckeditor.css @@ -0,0 +1,215 @@ +/* CKEditor Custom Styling */ + +/* Main editor container */ +.ck.ck-editor { + border-radius: 6px; + overflow: hidden; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +/* Toolbar styling */ +.ck.ck-toolbar { + background: #f8fafc; + border: 1px solid #d1d5db; + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 8px; +} + +.ck.ck-toolbar .ck-toolbar__items { + gap: 4px; +} + +/* Main editable area */ +.ck.ck-editor__editable { + background: #ffffff; + border: 1px solid #d1d5db; + border-top: none; + border-radius: 0 0 6px 6px; + padding: 1.5em 2em !important; + min-height: 400px; + line-height: 1.6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #333; +} + +/* Focus state */ +.ck.ck-editor__editable.ck-focused { + border-color: #1a9aef; + box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2); + outline: none; +} + +/* Content styling */ +.ck.ck-editor__editable .ck-content { + padding: 0; +} + +/* Typography improvements */ +.ck.ck-editor__editable p { + margin: 0.5em 0; + line-height: 1.6; +} + +.ck.ck-editor__editable h1, +.ck.ck-editor__editable h2, +.ck.ck-editor__editable h3, +.ck.ck-editor__editable h4, +.ck.ck-editor__editable h5, +.ck.ck-editor__editable h6 { + margin: 1em 0 0.5em 0; + font-weight: 600; + line-height: 1.4; +} + +.ck.ck-editor__editable h1 { font-size: 1.75em; } +.ck.ck-editor__editable h2 { font-size: 1.5em; } +.ck.ck-editor__editable h3 { font-size: 1.25em; } +.ck.ck-editor__editable h4 { font-size: 1.1em; } +.ck.ck-editor__editable h5 { font-size: 1em; } +.ck.ck-editor__editable h6 { font-size: 0.9em; } + +/* Lists */ +.ck.ck-editor__editable ul, +.ck.ck-editor__editable ol { + margin: 0.5em 0; + padding-left: 2em; +} + +.ck.ck-editor__editable li { + margin: 0.25em 0; + line-height: 1.6; +} + +/* Blockquotes */ +.ck.ck-editor__editable blockquote { + margin: 1em 0; + padding: 0.75em 1em; + border-left: 4px solid #1a9aef; + background-color: #f8fafc; + border-radius: 0 4px 4px 0; + font-style: italic; + color: #4b5563; +} + +/* Tables */ +.ck.ck-editor__editable table { + border-collapse: collapse; + width: 100%; + margin: 1em 0; +} + +.ck.ck-editor__editable table td, +.ck.ck-editor__editable table th { + border: 1px solid #d1d5db; + padding: 0.5em 0.75em; + text-align: left; +} + +.ck.ck-editor__editable table th { + background-color: #f8fafc; + font-weight: 600; +} + +/* Links */ +.ck.ck-editor__editable a { + color: #1a9aef; + text-decoration: underline; +} + +.ck.ck-editor__editable a:hover { + color: #0d7cd6; +} + +/* Code blocks */ +.ck.ck-editor__editable pre { + background-color: #f8fafc; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 1em; + margin: 1em 0; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.4; +} + +.ck.ck-editor__editable code { + background-color: #f1f5f9; + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9em; +} + +/* Images */ +.ck.ck-editor__editable img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 0.5em 0; +} + +/* Horizontal rule */ +.ck.ck-editor__editable hr { + border: none; + border-top: 1px solid #d1d5db; + margin: 2em 0; +} + +/* Placeholder text */ +.ck.ck-editor__editable.ck-blurred:empty::before { + content: attr(data-placeholder); + color: #9ca3af; + font-style: italic; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .ck.ck-editor__editable { + padding: 1em 1.5em !important; + font-size: 16px; /* Better for mobile */ + } + + .ck.ck-toolbar { + padding: 6px; + } + + .ck.ck-toolbar .ck-toolbar__items { + gap: 2px; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .ck.ck-editor__editable { + background: #1f2937; + color: #f9fafb; + border-color: #4b5563; + } + + .ck.ck-editor__editable h1, + .ck.ck-editor__editable h2, + .ck.ck-editor__editable h3, + .ck.ck-editor__editable h4, + .ck.ck-editor__editable h5, + .ck.ck-editor__editable h6 { + color: #f9fafb; + } + + .ck.ck-editor__editable blockquote { + background-color: #374151; + border-left-color: #1a9aef; + color: #d1d5db; + } + + .ck.ck-editor__editable pre { + background-color: #374151; + border-color: #4b5563; + } + + .ck.ck-editor__editable code { + background-color: #4b5563; + } +} diff --git a/style/global.css b/style/global.css new file mode 100644 index 0000000..5a22a31 --- /dev/null +++ b/style/global.css @@ -0,0 +1,2 @@ +/* @import url("https://fonts.googleapis.com/css2?family=Bytesized&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); */ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");