From 670f86a92ceac31dec0d956eb446fe31e00bc3c4 Mon Sep 17 00:00:00 2001 From: Anang Yusman Date: Mon, 29 Sep 2025 00:46:28 +0800 Subject: [PATCH] update --- app/detail/[id]/page.tsx | 19 ++ components/details/details-content.tsx | 53 ++- components/editor/basic-editor.js | Bin 0 -> 64 bytes components/editor/custom-editor.js | 188 +++++++++-- components/editor/editor-example.tsx | 164 ++++++++++ components/editor/editor-test.tsx | 176 ++++++++++ components/editor/fixed-editor.js | Bin 0 -> 9796 bytes components/editor/form-editor.js | 102 ++++++ components/editor/minimal-editor.js | 81 +++++ components/editor/optimized-editor.tsx | 105 ++++++ components/editor/readonly-editor.js | 136 ++++++++ components/editor/simple-editor.js | 95 ++++++ components/editor/simple-readonly-editor.js | 109 ++++++ components/editor/stable-editor.js | 93 ++++++ components/editor/static-editor.js | 93 ++++++ components/editor/strict-readonly-editor.js | 113 +++++++ components/editor/tinymce-editor.tsx | 309 ++++++++++++++++++ components/editor/view-editor.js | 262 ++++++++++++++- .../form/article/create-article-form.tsx | 74 +---- components/form/article/edit-article-form.tsx | 27 +- .../generate-ai-content-rewrite-form.tsx | 56 +++- .../form/article/generate-ai-single-form.tsx | 6 +- components/landing-page/headers-guard.tsx | 118 +++---- components/landing-page/headers-latest.tsx | 118 +++---- components/landing-page/headers-opinion.tsx | 118 +++---- components/landing-page/headers-peace.tsx | 118 +++---- components/landing-page/headers-popular.tsx | 118 +++---- components/landing-page/headers.tsx | 4 +- components/landing-page/lifestyle.tsx | 12 +- components/landing-page/on-the-spot.tsx | 2 +- components/table/article-table.tsx | 10 +- package-lock.json | 22 +- package.json | 1 + 33 files changed, 2467 insertions(+), 435 deletions(-) create mode 100644 app/detail/[id]/page.tsx create mode 100644 components/editor/basic-editor.js create mode 100644 components/editor/editor-example.tsx create mode 100644 components/editor/editor-test.tsx create mode 100644 components/editor/fixed-editor.js create mode 100644 components/editor/form-editor.js create mode 100644 components/editor/minimal-editor.js create mode 100644 components/editor/optimized-editor.tsx create mode 100644 components/editor/readonly-editor.js create mode 100644 components/editor/simple-editor.js create mode 100644 components/editor/simple-readonly-editor.js create mode 100644 components/editor/stable-editor.js create mode 100644 components/editor/static-editor.js create mode 100644 components/editor/strict-readonly-editor.js create mode 100644 components/editor/tinymce-editor.tsx diff --git a/app/detail/[id]/page.tsx b/app/detail/[id]/page.tsx new file mode 100644 index 0000000..6aba50d --- /dev/null +++ b/app/detail/[id]/page.tsx @@ -0,0 +1,19 @@ +import DetailContent from "@/components/details/details-content"; +import Footer from "@/components/landing-page/footer"; +import Navbar from "@/components/landing-page/navbar"; +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ +
+ +
+ +
+
+
+ ); +} diff --git a/components/details/details-content.tsx b/components/details/details-content.tsx index bf0a483..a046e6b 100644 --- a/components/details/details-content.tsx +++ b/components/details/details-content.tsx @@ -56,6 +56,8 @@ export default function DetailContent() { null ); + const [selectedIndex, setSelectedIndex] = useState(0); + const [tabArticles, setTabArticles] = useState([]); const [activeTab, setActiveTab] = useState("trending"); @@ -206,21 +208,40 @@ export default function DetailContent() {
- {articleDetail?.files?.[0]?.fileUrl ? ( +
Berita - ) : ( -
-

Gambar tidak tersedia

-
- )} +
-
+ {/* Thumbnail */} +
+ {articleDetail?.files.map((file: any, index: number) => ( + + ))} +
+ +

0

SHARES

@@ -338,13 +359,13 @@ export default function DetailContent() {
-

- {/* - Mikulnews.com - - */} - - {articleDetail?.description} -

+
+
+
diff --git a/components/editor/basic-editor.js b/components/editor/basic-editor.js new file mode 100644 index 0000000000000000000000000000000000000000..bcf5c6fd841163a84a91824eee888871a5ebc3dd GIT binary patch literal 64 zcmezWPoF`bL4hHeA)g_ap#VtcF{A=<2}3bZJc%I@NM{0Nb%CNOKvoG*MG=D@Ll#h+ Imw}4`0Nxi2$p8QV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..428f234154ce1ec5fe6dca5a80fadc946e559fd6 GIT binary patch literal 9796 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 Crkq^> literal 0 HcmV?d00001 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..82a5508 --- /dev/null +++ b/components/editor/optimized-editor.tsx @@ -0,0 +1,105 @@ +"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; 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..ca67548 --- /dev/null +++ b/components/editor/tinymce-editor.tsx @@ -0,0 +1,309 @@ +"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; 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/create-article-form.tsx b/components/form/article/create-article-form.tsx index 1f6b448..78f8aea 100644 --- a/components/form/article/create-article-form.tsx +++ b/components/form/article/create-article-form.tsx @@ -339,8 +339,8 @@ export default function CreateArticleForm() { function successSubmit(redirect: string, id: number, slug: string) { const url = `${window.location.protocol}//${window.location.host}` + - "/news/detail/" + - `${id}-${slug}`; + "/detail/" + + `${id}`; MySwal.fire({ title: "Sukses", icon: "success", @@ -524,13 +524,21 @@ export default function CreateArticleForm() { Single Article - Content Rewrite + {/* Content Rewrite */} {selectedWritingType === "single" ? ( { setDiseData(data); + // setValue("title", data?.title ?? "", { + // shouldValidate: true, + // shouldDirty: true, + // }); + // setValue("slug", generateSlug(data?.title ?? ""), { + // shouldValidate: true, + // shouldDirty: true, + // }); setValue( "description", data?.articleBody ? data?.articleBody : "" @@ -664,7 +672,10 @@ export default function CreateArticleForm() { "!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500", }} classNamePrefix="select" - onChange={onChange} + value={value} + onChange={(selected) => { + onChange(selected); + }} closeMenuOnSelect={false} components={animatedComponents} isClearable={true} @@ -683,60 +694,7 @@ export default function CreateArticleForm() { )}

Tags

- {/* ( -