Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo memberikan
kenaikan pangkat luar biasa anumerta kepada almarhum Bharatu Mardi
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
new file mode 100644
index 0000000..6599d8e
--- /dev/null
+++ b/components/editor/custom-editor.js
@@ -0,0 +1,77 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+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",
+ ],
+ 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 !important;
+ }
+ 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",
+ },
+ }}
+ />
+
+ );
+}
+
+export default CustomEditor;
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
+
+
+ Save
+
+
+ Reset
+
+
+
+
+
+ {/* 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
+
+
+
+ {savedContent && (
+
+ )}
+
+
+
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
+
+
+
+
+
Editor Type:
+
+ setEditorType('custom')}
+ >
+ CustomEditor
+
+ setEditorType('form')}
+ >
+ FormEditor
+
+
+
+
+
+
+ Set Value (Current Time)
+
+
+ Set Empty
+
+
+ Set HTML Content
+
+
+
+
+
+
Current Test Data:
+
+ {testData || '(empty)'}
+
+
+
+
Watched Form Values:
+
+
{JSON.stringify(watchedValues, null, 2)}
+
+
+
+
+
+
+
+
+
+ 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..d9d7bd6
--- /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?: any;
+}
+
+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..866c1c4
--- /dev/null
+++ b/components/editor/tinymce-editor.tsx
@@ -0,0 +1,303 @@
+"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,
+ branding: false,
+ elementpath: false,
+ resize: false,
+ statusbar: !readOnly,
+ // Performance optimizations
+ cache_suffix: "?v=1.0",
+ browser_spellcheck: false,
+ gecko_spellcheck: false,
+ 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;
+ }
+ `,
+ images_upload_handler: uploadUrl ? handleImageUpload : undefined,
+ automatic_uploads: !!uploadUrl,
+ file_picker_types: "image",
+ mobile: {
+ theme: "silver",
+ plugins: ["lists", "autolink", "link", "image", "table"],
+ toolbar: "bold italic | bullist numlist | link image",
+ },
+ 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_default_styles: { width: "100%" },
+ table_default_attributes: { border: "1" },
+ 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" },
+ ],
+ ...featureConfig,
+ ...(toolbar && { toolbar }),
+ setup: (editor: any) => {
+ // ⬅️ Set readOnly di sini
+ editor.on("init", () => {
+ if (readOnly) {
+ editor.mode.set("readonly");
+ }
+ });
+ },
+ };
+
+ 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
new file mode 100644
index 0000000..1e579b0
--- /dev/null
+++ b/components/editor/view-editor.js
@@ -0,0 +1,264 @@
+import React from "react";
+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-image-form.tsx b/components/form/article/create-image-form.tsx
new file mode 100644
index 0000000..328f946
--- /dev/null
+++ b/components/form/article/create-image-form.tsx
@@ -0,0 +1,918 @@
+"use client";
+import { Fragment, useEffect, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import Swal from "sweetalert2";
+import withReactContent from "sweetalert2-react-content";
+import dynamic from "next/dynamic";
+import { useDropzone } from "react-dropzone";
+import { CloudUploadIcon, TimesIcon } from "@/components/icons";
+import Image from "next/image";
+import ReactSelect from "react-select";
+import makeAnimated from "react-select/animated";
+import { convertDateFormatNoTime, htmlToString } from "@/utils/global";
+import { close, error, loading, successToast } from "@/config/swal";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import Cookies from "js-cookie";
+import { z } from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+ createArticle,
+ createArticleSchedule,
+ getArticleByCategory,
+ uploadArticleFile,
+ uploadArticleThumbnail,
+} from "@/service/article";
+import {
+ saveManualContext,
+ updateManualArticle,
+} from "@/service/generate-article";
+import { getUserLevels } from "@/service/user-levels-service";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Button } from "@/components/ui/button";
+import { getCategoryById } from "@/service/master-categories";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import GenerateSingleArticleForm from "./generate-ai-single-form";
+import GenerateContentRewriteForm from "./generate-ai-content-rewrite-form";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Calendar } from "@/components/ui/calendar";
+import DatePicker from "react-datepicker";
+
+const CustomEditor = dynamic(
+ () => {
+ return import("@/components/editor/custom-editor");
+ },
+ { ssr: false },
+);
+
+interface FileWithPreview extends File {
+ preview: string;
+}
+
+interface CategoryType {
+ id: number;
+ label: string;
+ value: number;
+}
+const categorySchema = z.object({
+ id: z.number(),
+ label: z.string(),
+ value: z.number(),
+});
+
+interface DiseData {
+ id: number;
+ articleBody: string;
+ title: string;
+ metaTitle: string;
+ description: string;
+ metaDescription: string;
+ mainKeyword: string;
+ additionalKeywords: string;
+}
+
+const createArticleSchema = z.object({
+ title: z.string().min(2, {
+ message: "Judul harus diisi",
+ }),
+ customCreatorName: z.string().min(2, {
+ message: "Judul harus diisi",
+ }),
+ slug: z.string().min(2, {
+ message: "Slug harus diisi",
+ }),
+ description: z.string().min(2, {
+ message: "Deskripsi harus diisi",
+ }),
+ // category: z.array(categorySchema).nonempty({
+ // message: "Kategori harus memiliki setidaknya satu item",
+ // }),
+ tags: z.array(z.string()).nonempty({
+ message: "Minimal 1 tag",
+ }),
+ source: z.enum(["internal", "external"]).optional(),
+});
+
+export default function CreateImageForm() {
+ const userLevel = Cookies.get("ulne");
+ const animatedComponents = makeAnimated();
+ const MySwal = withReactContent(Swal);
+ const router = useRouter();
+ const [files, setFiles] = useState([]);
+ const [useAi, setUseAI] = useState(false);
+ const [listCategory, setListCategory] = useState([]);
+ const [tag, setTag] = useState("");
+ const [thumbnailImg, setThumbnailImg] = useState([]);
+ const [selectedMainImage, setSelectedMainImage] = useState(
+ null,
+ );
+ const [thumbnailValidation, setThumbnailValidation] = useState("");
+ const [filesValidation, setFileValidation] = useState("");
+ const [diseData, setDiseData] = useState();
+ const [selectedWritingType, setSelectedWritingType] = useState("single");
+ const [status, setStatus] = useState<"publish" | "draft" | "scheduled">(
+ "publish",
+ );
+ const [isScheduled, setIsScheduled] = useState(false);
+ const [startDateValue, setStartDateValue] = useState();
+ const [startTimeValue, setStartTimeValue] = useState("");
+
+ const { getRootProps, getInputProps } = useDropzone({
+ onDrop: (acceptedFiles) => {
+ setFiles((prevFiles) => [
+ ...prevFiles,
+ ...acceptedFiles.map((file) => Object.assign(file)),
+ ]);
+ },
+ multiple: true,
+ accept: {
+ "image/*": [],
+ },
+ });
+
+ const formOptions = {
+ resolver: zodResolver(createArticleSchema),
+ defaultValues: { title: "", description: "", category: [], tags: [] },
+ };
+ type UserSettingSchema = z.infer;
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ setValue,
+ getValues,
+ watch,
+ setError,
+ clearErrors,
+ } = useForm(formOptions);
+
+ useEffect(() => {
+ fetchCategory();
+ }, []);
+
+ const fetchCategory = async () => {
+ const res = await getArticleByCategory();
+ if (res?.data?.data) {
+ setupCategory(res?.data?.data);
+ }
+ };
+
+ const setupCategory = (data: any) => {
+ const temp = [];
+ for (const element of data) {
+ temp.push({
+ id: element.id,
+ label: element.title,
+ value: element.id,
+ });
+ }
+ setListCategory(temp);
+ };
+
+ const onSubmit = async (values: z.infer) => {
+ if ((thumbnailImg.length < 1 && !selectedMainImage) || files.length < 1) {
+ if (files.length < 1) {
+ setFileValidation("Required");
+ } else {
+ setFileValidation("");
+ }
+ if (thumbnailImg.length < 1 && !selectedMainImage) {
+ setThumbnailValidation("Required");
+ } else {
+ setThumbnailValidation("");
+ }
+ } else {
+ setThumbnailValidation("");
+ setFileValidation("");
+ MySwal.fire({
+ title: "Simpan Data",
+ text: "",
+ icon: "warning",
+ showCancelButton: true,
+ cancelButtonColor: "#d33",
+ confirmButtonColor: "#3085d6",
+ confirmButtonText: "Simpan",
+ }).then((result) => {
+ if (result.isConfirmed) {
+ save(values);
+ }
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (useAi === false) {
+ setValue("description", "");
+ }
+ }, [useAi]);
+
+ function removeImgTags(htmlString: string) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(String(htmlString), "text/html");
+
+ const images = doc.querySelectorAll("img");
+ images.forEach((img) => img.remove());
+ return doc.body.innerHTML;
+ }
+
+ const saveArticleToDise = async (
+ values: z.infer,
+ ) => {
+ if (useAi) {
+ const request = {
+ id: diseData?.id,
+ title: values.title,
+ customCreatorName: values.customCreatorName,
+ source: values.source,
+ articleBody: removeImgTags(values.description),
+ metaDescription: diseData?.metaDescription,
+ metaTitle: diseData?.metaTitle,
+ mainKeyword: diseData?.mainKeyword,
+ additionalKeywords: diseData?.additionalKeywords,
+ createdBy: "345",
+ style: "Informational",
+ projectId: 2,
+ clientId: "humasClientIdtest",
+ lang: "id",
+ };
+ const res = await updateManualArticle(request);
+ if (res.error) {
+ error(res.message);
+ return false;
+ }
+
+ return diseData?.id;
+ } else {
+ const request = {
+ title: values.title,
+ articleBody: removeImgTags(values.description),
+ metaDescription: values.title,
+ metaTitle: values.title,
+ mainKeyword: values.title,
+ additionalKeywords: values.title,
+ createdBy: "345",
+ style: "Informational",
+ projectId: 2,
+ clientId: "humasClientIdtest",
+ lang: "id",
+ };
+
+ const res = await saveManualContext(request);
+ if (res.error) {
+ res.message;
+ return 0;
+ }
+ return res?.data?.data?.id;
+ }
+ };
+
+ const getUserLevelApprovalStatus = async () => {
+ const res = await getUserLevels(String(userLevel));
+ return res?.data?.data?.isApprovalActive;
+ };
+
+ const save = async (values: z.infer) => {
+ loading();
+
+ const userLevelStatus = await getUserLevelApprovalStatus();
+ const formData = {
+ title: values.title,
+ typeId: 1,
+ slug: values.slug,
+ customCreatorName: values.customCreatorName,
+ source: values.source,
+ categoryIds: "test",
+ tags: values.tags.join(","),
+ description: htmlToString(removeImgTags(values.description)),
+ htmlDescription: removeImgTags(values.description),
+ aiArticleId: await saveArticleToDise(values),
+ // isDraft: userLevelStatus ? true : status === "draft",
+ // isPublish: userLevelStatus ? false : status === "publish",
+ isDraft: status === "draft",
+ isPublish: status === "publish",
+ };
+
+ const response = await createArticle(formData);
+
+ if (response?.error) {
+ error(response.message);
+ return false;
+ }
+ const articleId = response?.data?.data?.id;
+
+ if (files?.length > 0) {
+ const formFiles = new FormData();
+
+ for (const element of files) {
+ formFiles.append("file", element);
+ const resFile = await uploadArticleFile(articleId, formFiles);
+ }
+ }
+ if (thumbnailImg?.length > 0 || files?.length > 0) {
+ if (thumbnailImg?.length > 0) {
+ const formFiles = new FormData();
+
+ formFiles.append("files", thumbnailImg[0]);
+ const resFile = await uploadArticleThumbnail(articleId, formFiles);
+ } else {
+ const formFiles = new FormData();
+
+ if (selectedMainImage) {
+ formFiles.append("files", files[selectedMainImage - 1]);
+
+ const resFile = await uploadArticleThumbnail(articleId, formFiles);
+ }
+ }
+ }
+
+ if (status === "scheduled" && startDateValue) {
+ // ambil waktu, default 00:00 jika belum diisi
+ const [hours, minutes] = startTimeValue
+ ? startTimeValue.split(":").map(Number)
+ : [0, 0];
+
+ // gabungkan tanggal + waktu
+ const combinedDate = new Date(startDateValue);
+ combinedDate.setHours(hours, minutes, 0, 0);
+
+ // format: 2025-10-08 14:30:00
+ const formattedDateTime = `${combinedDate.getFullYear()}-${String(
+ combinedDate.getMonth() + 1,
+ ).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
+ 2,
+ "0",
+ )} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
+ combinedDate.getMinutes(),
+ ).padStart(2, "0")}:00`;
+
+ const request = {
+ id: articleId,
+ date: formattedDateTime,
+ };
+
+ console.log("📤 Sending schedule request:", request);
+ const res = await createArticleSchedule(request);
+ console.log("✅ Schedule response:", res);
+ }
+
+ close();
+ successSubmit("/admin/article", articleId, values.slug);
+ };
+
+ function successSubmit(redirect: string, id: number, slug: string) {
+ const url =
+ `${window.location.protocol}//${window.location.host}` +
+ "/news/detail/" +
+ `${id}-${slug}`;
+ MySwal.fire({
+ title: "Sukses",
+ icon: "success",
+ confirmButtonColor: "#3085d6",
+ confirmButtonText: "OK",
+ }).then((result) => {
+ if (result.isConfirmed) {
+ router.push(redirect);
+ successToast("Article Url", url);
+ } else {
+ router.push(redirect);
+ successToast("Article Url", url);
+ }
+ });
+ }
+
+ const watchTitle = watch("title");
+ const generateSlug = (title: string) => {
+ return title
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, "")
+ .replace(/\s+/g, "-");
+ };
+
+ useEffect(() => {
+ setValue("slug", generateSlug(watchTitle));
+ }, [watchTitle]);
+
+ const renderFilePreview = (file: FileWithPreview) => {
+ if (file.type.startsWith("image")) {
+ return (
+
+ );
+ } else {
+ return "Not Found";
+ }
+ };
+
+ const handleRemoveFile = (file: FileWithPreview) => {
+ const uploadedFiles = files;
+ const filtered = uploadedFiles.filter((i) => i.name !== file.name);
+ setFiles([...filtered]);
+ };
+
+ const fileList = files.map((file, index) => (
+
+
+
{renderFilePreview(file)}
+
+
{file.name}
+
+ {Math.round(file.size / 100) / 10 > 1000 ? (
+ <>{(Math.round(file.size / 100) / 10000).toFixed(1)}>
+ ) : (
+ <>{(Math.round(file.size / 100) / 10).toFixed(1)}>
+ )}
+ {" kb"}
+
+
+ setSelectedMainImage(index + 1)}
+ />
+
+ Jadikan Thumbnail
+
+
+
+
+
+
handleRemoveFile(file)}
+ >
+
+
+
+ ));
+
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const selectedFiles = event.target.files;
+ if (selectedFiles) {
+ setThumbnailImg(Array.from(selectedFiles));
+ }
+ };
+
+ // const selectedCategory = watch("category");
+
+ // useEffect(() => {
+ // getDetailCategory();
+ // }, [selectedCategory]);
+
+ // const getDetailCategory = async () => {
+ // let temp = getValues("tags");
+ // for (const element of selectedCategory) {
+ // const res = await getCategoryById(element?.id);
+ // const tagList = res?.data?.data?.tags;
+ // if (tagList) {
+ // temp = [...temp, ...res?.data?.data?.tags];
+ // }
+ // }
+ // const uniqueArray = temp.filter(
+ // (item, index) => temp.indexOf(item) === index,
+ // );
+
+ // setValue("tags", uniqueArray as [string, ...string[]]);
+ // };
+
+ return (
+
+ );
+}
diff --git a/components/form/article/generate-ai-content-rewrite-form.tsx b/components/form/article/generate-ai-content-rewrite-form.tsx
new file mode 100644
index 0000000..efa630d
--- /dev/null
+++ b/components/form/article/generate-ai-content-rewrite-form.tsx
@@ -0,0 +1,323 @@
+"use client";
+
+import {
+ Select,
+ SelectTrigger,
+ SelectValue,
+ SelectContent,
+ SelectItem,
+} from "@/components/ui/select";
+import { useEffect, useState } from "react";
+import { close, error, loading } from "@/config/swal";
+import { delay } from "@/utils/global";
+import dynamic from "next/dynamic";
+import {
+ getDetailArticle,
+ getGenerateRewriter,
+} from "@/service/generate-article";
+import { Button } from "@/components/ui/button";
+import { Loader2 } from "lucide-react";
+import GetSeoScore from "./get-seo-score-form";
+
+const CustomEditor = dynamic(
+ () => {
+ return import("@/components/editor/custom-editor");
+ },
+ { ssr: false }
+);
+
+const writingStyle = [
+ {
+ id: 1,
+ name: "Friendly",
+ },
+ {
+ id: 1,
+ name: "Professional",
+ },
+ {
+ id: 3,
+ name: "Informational",
+ },
+ {
+ id: 4,
+ name: "Neutral",
+ },
+ {
+ id: 5,
+ name: "Witty",
+ },
+];
+
+const articleSize = [
+ {
+ id: 1,
+ name: "News (300 - 900 words)",
+ value: "News",
+ },
+ {
+ id: 2,
+ name: "Info (900 - 2000 words)",
+ value: "Info",
+ },
+ {
+ id: 3,
+ name: "Detail (2000 - 5000 words)",
+ value: "Detail",
+ },
+];
+
+interface DiseData {
+ id: number;
+ articleBody: string;
+ title: string;
+ metaTitle: string;
+ description: string;
+ metaDescription: string;
+ mainKeyword: string;
+ additionalKeywords: string;
+}
+
+export default function GenerateContentRewriteForm(props: {
+ content: (data: DiseData) => void;
+}) {
+ const [selectedWritingSyle, setSelectedWritingStyle] =
+ useState("Informational");
+ const [selectedArticleSize, setSelectedArticleSize] = useState("News");
+ const [selectedLanguage, setSelectedLanguage] = useState("id");
+ const [mainKeyword, setMainKeyword] = useState("");
+ const [articleIds, setArticleIds] = useState([]);
+ const [selectedId, setSelectedId] = useState();
+ const [isLoading, setIsLoading] = useState(true);
+
+ const onSubmit = async () => {
+ loading();
+ const request = {
+ advConfig: "",
+ context: mainKeyword,
+ style: selectedWritingSyle,
+ sentiment: "Informational",
+ urlContext: null,
+ contextType: "article",
+ lang: selectedLanguage,
+ createdBy: "123123",
+ clientId: "humasClientIdtest",
+ };
+ const res = await getGenerateRewriter(request);
+ close();
+ if (res?.error) {
+ error("Error");
+ }
+ setArticleIds([...articleIds, res?.data?.data?.id]);
+ };
+
+ useEffect(() => {
+ getArticleDetail();
+ }, [selectedId]);
+
+ const checkArticleStatus = async (data: string | null) => {
+ if (data === null) {
+ delay(7000).then(() => {
+ getArticleDetail();
+ });
+ }
+ };
+
+ const getArticleDetail = async () => {
+ if (selectedId) {
+ const res = await getDetailArticle(selectedId);
+ const data = res?.data?.data;
+ checkArticleStatus(data?.articleBody);
+ if (data?.articleBody !== null) {
+ setIsLoading(false);
+ props.content(data);
+ } else {
+ setIsLoading(true);
+ props.content({
+ id: data?.id,
+ articleBody: "",
+ title: "",
+ metaTitle: "",
+ description: "",
+ metaDescription: "",
+ additionalKeywords: "",
+ mainKeyword: "",
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+ {/*
+ e.target.value !== ""
+ ? setSelectedWritingStyle(e.target.value)
+ : ""
+ }
+ className="w-full"
+ classNames={{
+ label: "!text-black",
+ value: "!text-black",
+ trigger: [
+ "border-1 rounded-lg",
+ "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
+ ],
+ }}
+ >
+
+ {writingStyle.map((style) => (
+ {style.name}
+ ))}
+
+ */}
+ setSelectedWritingStyle(value)}
+ >
+
+
+
+
+ {writingStyle.map((style) => (
+
+ {style.name}
+
+ ))}
+
+
+ {/* (e.target.value !== "" ? setSelectedArticleSize(e.target.value) : "")}
+ className="w-full"
+ classNames={{
+ label: "!text-black",
+ value: "!text-black",
+ trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
+ }}
+ >
+
+ {articleSize.map((size) => (
+ {size.name}
+ ))}
+
+ */}
+ setSelectedArticleSize(value)}
+ >
+
+
+
+
+ {articleSize.map((style) => (
+
+ {style.name}
+
+ ))}
+
+
+ {/* (e.target.value !== "" ? setSelectedLanguage(e.target.value) : "")}
+ className="w-full"
+ classNames={{
+ label: "!text-black",
+ value: "!text-black",
+ trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
+ }}
+ >
+
+ Indonesia
+ English
+
+ */}
+ setSelectedLanguage(value)}
+ >
+
+
+
+
+ Indonesia
+ English
+
+
+
+
+
+
+
+
+
+ {mainKeyword == "" && (
+
Required
+ )}
+ {articleIds.length < 3 && (
+
+ {isLoading ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ "Generate"
+ )}
+
+ )}
+
+ {articleIds.length > 0 && (
+
+ {articleIds?.map((id, index) => (
+ setSelectedId(id)}
+ disabled={isLoading && selectedId === id}
+ variant={selectedId === id ? "default" : "outline"}
+ className="flex items-center gap-2"
+ >
+ {isLoading && selectedId === id ? (
+ <>
+
+ Loading...
+ >
+ ) : (
+ `Article ${index + 1}`
+ )}
+
+ ))}
+
+ )}
+ {!isLoading && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/form/article/generate-ai-single-form.tsx b/components/form/article/generate-ai-single-form.tsx
new file mode 100644
index 0000000..f69148a
--- /dev/null
+++ b/components/form/article/generate-ai-single-form.tsx
@@ -0,0 +1,451 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { close, error, loading } from "@/config/swal";
+import { delay } from "@/utils/global";
+import GetSeoScore from "./get-seo-score-form";
+import {
+ generateDataArticle,
+ getDetailArticle,
+ getGenerateKeywords,
+ getGenerateTitle,
+} from "@/service/generate-article";
+import {
+ Select,
+ SelectTrigger,
+ SelectValue,
+ SelectContent,
+ SelectItem,
+} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import { Loader2 } from "lucide-react";
+import { Input } from "@/components/ui/input";
+
+const writingStyle = [
+ {
+ id: 1,
+ name: "Friendly",
+ },
+ {
+ id: 1,
+ name: "Professional",
+ },
+ {
+ id: 3,
+ name: "Informational",
+ },
+ {
+ id: 4,
+ name: "Neutral",
+ },
+ {
+ id: 5,
+ name: "Witty",
+ },
+];
+
+const articleSize = [
+ {
+ id: 1,
+ name: "News (300 - 900 words)",
+ value: "News",
+ },
+ {
+ id: 2,
+ name: "Info (900 - 2000 words)",
+ value: "Info",
+ },
+ {
+ id: 3,
+ name: "Detail (2000 - 5000 words)",
+ value: "Detail",
+ },
+];
+
+interface DiseData {
+ id: number;
+ articleBody: string;
+ title: string;
+ metaTitle: string;
+ description: string;
+ metaDescription: string;
+ mainKeyword: string;
+ additionalKeywords: string;
+}
+
+export default function GenerateSingleArticleForm(props: {
+ content: (data: DiseData) => void;
+}) {
+ const [selectedWritingSyle, setSelectedWritingStyle] = useState("");
+ const [selectedArticleSize, setSelectedArticleSize] = useState("");
+ const [selectedLanguage, setSelectedLanguage] = useState("");
+ const [mainKeyword, setMainKeyword] = useState("");
+ const [title, setTitle] = useState("");
+ const [additionalKeyword, setAdditionalKeyword] = useState("");
+ const [articleIds, setArticleIds] = useState([]);
+ const [selectedId, setSelectedId] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const generateAll = async (keyword: string | undefined) => {
+ if (keyword) {
+ generateTitle(keyword);
+ generateKeywords(keyword);
+ }
+ };
+
+ const generateTitle = async (keyword: string | undefined) => {
+ if (keyword) {
+ loading();
+ const req = {
+ keyword: keyword,
+ style: selectedWritingSyle,
+ website: "None",
+ connectToWeb: true,
+ lang: selectedLanguage,
+ pointOfView: "None",
+ clientId: "",
+ };
+ const res = await getGenerateTitle(req);
+ const data = res?.data?.data;
+ setTitle(data);
+ close();
+ }
+ };
+
+ const generateKeywords = async (keyword: string | undefined) => {
+ if (keyword) {
+ const req = {
+ keyword: keyword,
+ style: selectedWritingSyle,
+ website: "None",
+ connectToWeb: true,
+ lang: selectedLanguage,
+ pointOfView: "0",
+ clientId: "",
+ };
+ loading();
+ const res = await getGenerateKeywords(req);
+ const data = res?.data?.data;
+ setAdditionalKeyword(data);
+ close();
+ }
+ };
+
+ const onSubmit = async () => {
+ loading();
+ const request = {
+ advConfig: "",
+ style: selectedWritingSyle,
+ website: "None",
+ connectToWeb: true,
+ lang: selectedLanguage,
+ pointOfView: "None",
+ title: title,
+ imageSource: "Web",
+ mainKeyword: mainKeyword,
+ additionalKeywords: additionalKeyword,
+ targetCountry: null,
+ articleSize: selectedArticleSize,
+ projectId: 2,
+ createdBy: "123123",
+ clientId: "humasClientIdtest",
+ };
+ const res = await generateDataArticle(request);
+ close();
+ if (res?.error) {
+ error("Error");
+ }
+ setArticleIds([...articleIds, res?.data?.data?.id]);
+ // props.articleId(res?.data?.data?.id);
+ };
+
+ useEffect(() => {
+ getArticleDetail();
+ }, [selectedId]);
+
+ const checkArticleStatus = async (data: string | null) => {
+ if (data === null) {
+ delay(7000).then(() => {
+ getArticleDetail();
+ });
+ }
+ };
+
+ const getArticleDetail = async () => {
+ if (selectedId) {
+ const res = await getDetailArticle(selectedId);
+ const data = res?.data?.data;
+ checkArticleStatus(data?.articleBody);
+ if (data?.articleBody !== null) {
+ setIsLoading(false);
+ props.content(data);
+ } else {
+ setIsLoading(true);
+ props.content({
+ id: data?.id,
+ articleBody: "",
+ title: "",
+ metaTitle: "",
+ description: "",
+ metaDescription: "",
+ additionalKeywords: "",
+ mainKeyword: "",
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+ {/*
+ e.target.value !== ""
+ ? setSelectedWritingStyle(e.target.value)
+ : ""
+ }
+ className="w-full"
+ classNames={{
+ label: "!text-black",
+ value: "!text-black",
+ trigger: [
+ "border-1 rounded-lg",
+ "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
+ ],
+ }}
+ >
+
+ {writingStyle.map((style) => (
+ {style.name}
+ ))}
+
+ */}
+ {
+ if (value !== "") setSelectedWritingStyle(value);
+ }}
+ >
+
+
+
+
+ {writingStyle.map((style) => (
+
+ {style.name}
+
+ ))}
+
+
+ {/* (e.target.value !== "" ? setSelectedArticleSize(e.target.value) : "")}
+ className="w-full"
+ classNames={{
+ label: "!text-black",
+ value: "!text-black",
+ trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
+ }}
+ >
+
+ {articleSize.map((size) => (
+ {size.name}
+ ))}
+
+ */}
+ {
+ if (value !== "") setSelectedArticleSize(value);
+ }}
+ >
+
+
+
+
+ {articleSize.map((style) => (
+
+ {style.name}
+
+ ))}
+
+
+ {/* (e.target.value !== "" ? setSelectedLanguage(e.target.value) : "")}
+ className="w-full"
+ classNames={{
+ label: "!text-black",
+ value: "!text-black",
+ trigger: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
+ }}
+ >
+
+ Indonesia
+ English
+
+ */}
+ {
+ if (value !== "") setSelectedLanguage(value);
+ }}
+ >
+
+
+
+
+ Indonesia
+ English
+
+
+
+
+
+
Main Keyword
+
generateAll(mainKeyword)}
+ disabled={isLoading} // tambahkan state kontrol loading
+ >
+ {isLoading ? (
+ <>
+
+ Processing...
+ >
+ ) : (
+ "Process"
+ )}
+
+
+
+
setMainKeyword(e.target.value)}
+ className="w-full mt-1 border border-gray-300 rounded-lg dark:border-gray-400"
+ />
+
+ {mainKeyword == "" && (
+
Required
+ )}
+
+
Title
+
generateTitle(mainKeyword)}
+ disabled={mainKeyword === ""}
+ >
+ Generate
+
+
+
+
setTitle(e.target.value)}
+ className="w-full mt-1 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" // Custom styling using className
+ aria-label="Title"
+ />
+
+ {/* {title == "" &&
Required
} */}
+
+
Additional Keyword
+
generateKeywords(mainKeyword)}
+ disabled={mainKeyword === ""}
+ >
+ Generate
+
+
+
+
setAdditionalKeyword(e.target.value)}
+ className="mt-1 border rounded-lg dark:bg-transparent dark:border-gray-400"
+ aria-label="Additional Keyword"
+ />
+
+ {/* {additionalKeyword == "" && (
+
Required
+ )} */}
+ {/* {articleIds.length < 3 && (
+
+ Generate
+
+ )} */}
+ {articleIds.length < 3 && (
+
+ Generate
+
+ )}
+
+ {articleIds.length > 0 && (
+
+ {articleIds.map((id, index) => (
+ setSelectedId(id)}
+ disabled={isLoading && selectedId === id}
+ className={`
+ ${
+ selectedId === id
+ ? isLoading
+ ? "bg-yellow-500"
+ : "bg-green-600"
+ : "bg-gray-200"
+ }
+ text-sm px-4 py-2 rounded text-white transition-colors
+ `}
+ >
+ Article {index + 1}
+
+ ))}
+
+ )}
+
+ {!isLoading && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/form/article/get-seo-score-form.tsx b/components/form/article/get-seo-score-form.tsx
new file mode 100644
index 0000000..c05ee80
--- /dev/null
+++ b/components/form/article/get-seo-score-form.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { CustomCircularProgress } from "@/components/layout/costum-circular-progress";
+import { getSeoScore } from "@/service/generate-article";
+import { useEffect, useState } from "react";
+
+export default function GetSeoScore(props: { id: string }) {
+ useEffect(() => {
+ fetchSeoScore();
+ }, [props.id]);
+
+ const [totalScoreSEO, setTotalScoreSEO] = useState();
+ const [errorSEO, setErrorSEO] = useState([]);
+ const [warningSEO, setWarningSEO] = useState([]);
+ const [optimizedSEO, setOptimizedSEO] = useState([]);
+
+ const fetchSeoScore = async () => {
+ const res = await getSeoScore(props?.id);
+ if (res.error) {
+ // error(res.message);
+ return false;
+ }
+ setTotalScoreSEO(res.data.data?.seo_analysis?.score || 0);
+ const errorList: any[] = [
+ ...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.error,
+ ...res.data.data?.seo_analysis?.analysis?.content_quality?.error,
+ ];
+ setErrorSEO(errorList);
+ const warningList: any[] = [
+ ...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.warning,
+ ...res.data.data?.seo_analysis?.analysis?.content_quality?.warning,
+ ];
+ setWarningSEO(warningList);
+ const optimizedList: any[] = [
+ ...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.optimized,
+ ...res.data.data?.seo_analysis?.analysis?.content_quality?.optimized,
+ ];
+ setOptimizedSEO(optimizedList);
+ };
+ return (
+
+
+
SEO Score
+ {totalScoreSEO ? (
+
+ {/*
*/}
+
+
+
+
+ {/* */}
+ Error : {errorSEO.length || 0}
+
+
+ {/*
+ !
+
*/}
+ Warning : {warningSEO.length || 0}
+
+
+ {/* */}
+ Optimize : {optimizedSEO.length || 0}
+
+
+
+ ) : (
+ "Belum ada Data"
+ )}
+ {totalScoreSEO && (
+ //
+ // }
+ // title={`${errorSEO?.length || 0} Errors`}
+ // >
+ //
+ // {errorSEO?.map((item: any) => (
+ //
+ // {item}
+ //
+ // ))}
+ //
+ //
+ //
+ // // !
+ // //
+ // // }
+ // title={`${warningSEO?.length || 0} Warnings`}
+ // >
+ //
+ // {warningSEO?.map((item: any) => (
+ //
+ // {item}
+ //
+ // ))}
+ //
+ //
+ // }
+ // title={`${optimizedSEO?.length || 0} Optimized`}
+ // >
+ //
+ // {optimizedSEO?.map((item: any) => (
+ //
+ // {item}
+ //
+ // ))}
+ //
+ //
+ //
+
+
+ {`${
+ errorSEO?.length || 0
+ } Errors`}
+
+
+ {errorSEO?.map((item: any) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+ {`${
+ warningSEO?.length || 0
+ } Warnings`}
+
+
+ {warningSEO?.map((item: any) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+ {`${
+ optimizedSEO?.length || 0
+ } Optimized`}
+
+
+ {optimizedSEO?.map((item: any) => (
+
+ {item}
+
+ ))}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/landing-page/about.tsx b/components/landing-page/about.tsx
index 29008d6..4ffcad4 100644
--- a/components/landing-page/about.tsx
+++ b/components/landing-page/about.tsx
@@ -1,4 +1,7 @@
+"use client";
+
import Image from "next/image";
+import { motion } from "framer-motion";
export default function AboutSection() {
const socials = [
@@ -9,37 +12,56 @@ export default function AboutSection() {
{ name: "Tiktok", icon: "/image/tt.png" },
];
+ const messages = [
+ { id: 1, text: "Dimana posisi Ayah saya sekarang?", type: "user" },
+ {
+ id: 2,
+ text: "Bapak Ahmad terdeteksi di Tenda Maktab 45, Mina.",
+ type: "bot",
+ },
+ { id: 3, text: "Apakah ada berita cuaca hari ini?", type: "user" },
+ {
+ id: 4,
+ text: "Makkah saat ini cerah, suhu 38°. Kemenag menghimbau jamaah untuk minum air tiap 1 jam.",
+ type: "bot",
+ },
+ ];
+
return (
- {/* TOP CENTER CONTENT */}
-
-
- Manage All your channels from Multipool
-
-
- {/* SOCIAL ICONS */}
-
- {socials.map((item) => (
-
-
-
- ))}
-
-
-
- {/* PHONE IMAGE */}
+ {/* PHONE WRAPPER */}
-
+
+ {/* PHONE IMAGE */}
+
+
+ {/* CHAT AREA */}
+
+
+ {messages.map((msg, index) => (
+
+ {msg.text}
+
+ ))}
+
+
+
{/* TEXT CONTENT */}
@@ -58,14 +80,7 @@ export default function AboutSection() {
PT Qudo Buana Nawakara adalah perusahaan nasional Indonesia yang
- berfokus pada pengembangan aplikasi untuk mendukung kegiatan
- reputasi manajemen institusi, organisasi dan publik figur. Dengan
- dukungan teknologi otomatisasi dan kecerdasan buatan (AI) untuk
- mengoptimalkan proses. Perusahaan didukung oleh team SDM nasional
- yang sudah berpengalaman serta memiliki sertifikasi internasional,
- untuk memastikan produk yang dihasilkan handal dan berkualitas
- tinggi. PT Qudo Buana Nawakara berkantor pusat di Jakarta dengan
- support office di Bandung, Indonesia – India – USA – Oman.
+ berfokus pada pengembangan aplikasi...
diff --git a/components/landing-page/headers.tsx b/components/landing-page/headers.tsx
index 4ce9f61..c63f10e 100644
--- a/components/landing-page/headers.tsx
+++ b/components/landing-page/headers.tsx
@@ -4,66 +4,82 @@ import Image from "next/image";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { Menu, X, Home, Box, Briefcase, Newspaper } from "lucide-react";
+import { Input } from "../ui/input";
+import { Label } from "../ui/label";
+import { Textarea } from "../ui/textarea";
+import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
export default function Header() {
const [open, setOpen] = useState(false);
+ const [contactOpen, setContactOpen] = useState(false);
return (
-