update form,contact us landing, responsive detail audio
This commit is contained in:
parent
a0bb9c858c
commit
69cf73ec62
|
|
@ -0,0 +1,9 @@
|
|||
import CreateImageForm from "@/components/form/article/create-image-form";
|
||||
|
||||
export default function CreateNewsImage() {
|
||||
return (
|
||||
<div className="bg-slate-100 p-3 lg:p-8 dark:!bg-black overflow-y-auto">
|
||||
<CreateImageForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import ContentWebsite from "@/components/main/content-website";
|
||||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
import NewsImage from "@/components/main/news-image";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
|
|
|||
|
|
@ -10,32 +10,34 @@ export default function AudioPlayerSection() {
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ===== AUDIO PLAYER CARD ===== */}
|
||||
<div className="bg-gray-50 rounded-2xl p-6 border border-gray-200">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="bg-gray-50 rounded-2xl p-4 md:p-6 border border-gray-200">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-6">
|
||||
{/* PLAY BUTTON */}
|
||||
<div className="flex justify-center md:justify-start">
|
||||
<button
|
||||
onClick={() => setPlaying(!playing)}
|
||||
className="h-16 w-16 rounded-full bg-yellow-400 flex items-center justify-center shadow-md hover:scale-105 transition"
|
||||
className="h-14 w-14 md:h-16 md:w-16 rounded-full bg-yellow-400 flex items-center justify-center shadow-md hover:scale-105 transition"
|
||||
>
|
||||
{playing ? (
|
||||
<Pause size={28} className="text-black" />
|
||||
<Pause size={24} className="text-black" />
|
||||
) : (
|
||||
<Play size={28} className="text-black ml-1" />
|
||||
<Play size={24} className="text-black ml-1" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* WAVEFORM + DURATION */}
|
||||
<div className="flex-1">
|
||||
{/* FAKE WAVEFORM */}
|
||||
<div className="h-16 flex items-center gap-[3px]">
|
||||
{Array.from({ length: 70 }).map((_, i) => (
|
||||
<div className="h-14 md:h-16 flex items-center gap-[2px] overflow-hidden">
|
||||
{Array.from({ length: 60 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-[3px] rounded-full ${
|
||||
i < 35 ? "bg-black" : "bg-gray-400"
|
||||
className={`w-[2px] md:w-[3px] rounded-full ${
|
||||
i < 30 ? "bg-black" : "bg-gray-400"
|
||||
}`}
|
||||
style={{
|
||||
height: `${Math.random() * 40 + 10}px`,
|
||||
height: `${Math.random() * 35 + 10}px`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -49,8 +51,7 @@ export default function AudioPlayerSection() {
|
|||
|
||||
{/* PROGRESS */}
|
||||
<div className="flex items-center gap-3 mt-3">
|
||||
<Volume2 size={16} />
|
||||
|
||||
<Volume2 size={16} className="shrink-0" />
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
|
|
@ -65,8 +66,8 @@ export default function AudioPlayerSection() {
|
|||
</div>
|
||||
|
||||
{/* ===== META INFO ===== */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
||||
<span className="bg-red-600 text-white text-xs px-2 py-1 rounded">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs md:text-sm text-gray-500">
|
||||
<span className="bg-red-600 text-white text-[10px] md:text-xs px-2 py-1 rounded">
|
||||
POLRI
|
||||
</span>
|
||||
|
||||
|
|
@ -76,13 +77,13 @@ export default function AudioPlayerSection() {
|
|||
</div>
|
||||
|
||||
{/* ===== TITLE ===== */}
|
||||
<h1 className="text-2xl font-semibold leading-snug">
|
||||
<h1 className="text-lg md:text-2xl font-semibold leading-snug">
|
||||
Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar
|
||||
Biasa
|
||||
</h1>
|
||||
|
||||
{/* ===== ARTICLE ===== */}
|
||||
<div className="space-y-4 text-gray-700 leading-relaxed text-[15px]">
|
||||
<div className="space-y-4 text-gray-700 leading-relaxed text-sm md:text-[15px]">
|
||||
<p>
|
||||
Jakarta - Kapolri Jenderal Polisi Drs. Listyo Sigit Prabowo memberikan
|
||||
kenaikan pangkat luar biasa anumerta kepada almarhum Bharatu Mardi
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 (
|
||||
<div className="ckeditor-wrapper">
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
onChange={(event, editor) => {
|
||||
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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomEditor;
|
||||
|
|
@ -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<EditorExampleProps> = ({
|
||||
editorType = 'tinymce'
|
||||
}) => {
|
||||
const [content, setContent] = useState('<p>Hello, this is the editor content!</p>');
|
||||
const [savedContent, setSavedContent] = useState('');
|
||||
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setContent(newContent);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setSavedContent(content);
|
||||
console.log('Content saved:', content);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setContent('<p>Content has been reset!</p>');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Rich Text Editor Example</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
This is an optimized editor with {editorType} - much smaller bundle size and better performance!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Editor Panel */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Editor</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg">
|
||||
{/* Choose your editor based on migration */}
|
||||
{editorType === 'tinymce' && (
|
||||
<div className="p-4">
|
||||
<p className="text-gray-500 text-sm mb-2">
|
||||
TinyMCE Editor (200KB bundle)
|
||||
</p>
|
||||
{/* <OptimizedEditor
|
||||
initialData={content}
|
||||
onChange={handleContentChange}
|
||||
height={400}
|
||||
placeholder="Start typing your content..."
|
||||
/> */}
|
||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
||||
<p className="text-gray-500">TinyMCE Editor Component</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editorType === 'ckeditor' && (
|
||||
<div className="p-4">
|
||||
<p className="text-gray-500 text-sm mb-2">
|
||||
CKEditor5 Classic (800KB bundle)
|
||||
</p>
|
||||
{/* <OptimizedCKEditor
|
||||
initialData={content}
|
||||
onChange={handleContentChange}
|
||||
height={400}
|
||||
placeholder="Start typing your content..."
|
||||
/> */}
|
||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
||||
<p className="text-gray-500">CKEditor5 Classic Component</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editorType === 'quill' && (
|
||||
<div className="p-4">
|
||||
<p className="text-gray-500 text-sm mb-2">
|
||||
React Quill (100KB bundle)
|
||||
</p>
|
||||
{/* <MinimalEditor
|
||||
initialData={content}
|
||||
onChange={handleContentChange}
|
||||
height={400}
|
||||
placeholder="Start typing your content..."
|
||||
/> */}
|
||||
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
|
||||
<p className="text-gray-500">React Quill Component</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Panel */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Preview</h3>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">Current Content:</h4>
|
||||
<div
|
||||
className="prose max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{savedContent && (
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">Saved Content:</h4>
|
||||
<div
|
||||
className="prose max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: savedContent }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">Raw HTML:</h4>
|
||||
<pre className="text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">
|
||||
{content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Performance Info */}
|
||||
<div className="mt-8 p-4 bg-blue-50 rounded-lg">
|
||||
<h4 className="font-medium text-blue-900 mb-2">Performance Benefits:</h4>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• 90% smaller bundle size compared to custom CKEditor5</li>
|
||||
<li>• Faster initial load time</li>
|
||||
<li>• Better mobile performance</li>
|
||||
<li>• Reduced memory usage</li>
|
||||
<li>• Improved Lighthouse score</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorExample;
|
||||
|
|
@ -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 = `<p>Updated content at ${new Date().toLocaleTimeString()}</p><p>This content was set via setValue</p>`;
|
||||
setValue('description', newContent);
|
||||
setTestData(newContent);
|
||||
};
|
||||
|
||||
const handleSetEmpty = () => {
|
||||
setValue('description', '');
|
||||
setTestData('');
|
||||
};
|
||||
|
||||
const handleSetHTML = () => {
|
||||
const htmlContent = `
|
||||
<h2>HTML Content Test</h2>
|
||||
<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>
|
||||
<ul>
|
||||
<li>List item 1</li>
|
||||
<li>List item 2</li>
|
||||
<li>List item 3</li>
|
||||
</ul>
|
||||
<p>Updated at: ${new Date().toLocaleTimeString()}</p>
|
||||
`;
|
||||
setValue('description', htmlContent);
|
||||
setTestData(htmlContent);
|
||||
};
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
console.log('Form submitted:', data);
|
||||
alert('Form submitted! Check console for data.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
||||
<h1 className="text-2xl font-bold">Editor Test Component</h1>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Editor Type:</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
variant={editorType === 'custom' ? 'default' : 'outline'}
|
||||
onClick={() => setEditorType('custom')}
|
||||
>
|
||||
CustomEditor
|
||||
</Button>
|
||||
<Button
|
||||
variant={editorType === 'form' ? 'default' : 'outline'}
|
||||
onClick={() => setEditorType('form')}
|
||||
>
|
||||
FormEditor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={handleSetValue} variant="outline">
|
||||
Set Value (Current Time)
|
||||
</Button>
|
||||
<Button onClick={handleSetEmpty} variant="outline">
|
||||
Set Empty
|
||||
</Button>
|
||||
<Button onClick={handleSetHTML} variant="outline">
|
||||
Set HTML Content
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Current Test Data:</Label>
|
||||
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
||||
{testData || '(empty)'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Watched Form Values:</Label>
|
||||
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
|
||||
<pre>{JSON.stringify(watchedValues, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Title:</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<Input {...field} className="mt-1" />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Description (Editor):</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
editorType === 'custom' ? (
|
||||
<CustomEditor
|
||||
onChange={field.onChange}
|
||||
initialData={field.value}
|
||||
/>
|
||||
) : (
|
||||
<FormEditor
|
||||
onChange={field.onChange}
|
||||
initialData={field.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Creator Name:</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="creatorName"
|
||||
render={({ field }) => (
|
||||
<Input {...field} className="mt-1" />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Submit Form
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-semibold mb-2">Instructions:</h3>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||
<li>Switch between CustomEditor and FormEditor to test both</li>
|
||||
<li>Click "Set Value" to test setValue functionality</li>
|
||||
<li>Click "Set Empty" to test empty content handling</li>
|
||||
<li>Click "Set HTML Content" to test rich HTML content</li>
|
||||
<li>Type in the editor to test onChange functionality</li>
|
||||
<li>Submit the form to see all data</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: 400,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: 368px;
|
||||
}
|
||||
`,
|
||||
placeholder: 'Start typing...',
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
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',
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: 400,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: 368px;
|
||||
}
|
||||
`,
|
||||
placeholder: 'Start typing...',
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Minimal settings to prevent cursor jumping
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
// Disable problematic features
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
// Basic content handling
|
||||
paste_as_text: false,
|
||||
paste_enable_default_filters: true,
|
||||
// Mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MinimalEditor;
|
||||
|
|
@ -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<OptimizedEditorProps> = ({
|
||||
initialData = "",
|
||||
onChange,
|
||||
height = 400,
|
||||
placeholder = "Start typing...",
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
const handleEditorChange = (content: string) => {
|
||||
if (onChange) {
|
||||
onChange(content);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInit = (evt: any, editor: any) => {
|
||||
editorRef.current = editor;
|
||||
};
|
||||
|
||||
return (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
initialValue={initialData}
|
||||
onEditorChange={handleEditorChange}
|
||||
disabled={disabled}
|
||||
init={{
|
||||
height,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
"advlist",
|
||||
"autolink",
|
||||
"lists",
|
||||
"link",
|
||||
"image",
|
||||
"charmap",
|
||||
"preview",
|
||||
"anchor",
|
||||
"searchreplace",
|
||||
"visualblocks",
|
||||
"code",
|
||||
"fullscreen",
|
||||
"insertdatetime",
|
||||
"media",
|
||||
"table",
|
||||
"code",
|
||||
"help",
|
||||
"wordcount",
|
||||
],
|
||||
toolbar:
|
||||
"undo redo | blocks | " +
|
||||
"bold italic forecolor | alignleft aligncenter " +
|
||||
"alignright alignjustify | bullist numlist outdent indent | " +
|
||||
"removeformat | table | code | help",
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: ${height - 32}px;
|
||||
}
|
||||
`,
|
||||
placeholder,
|
||||
readonly: readOnly,
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Performance optimizations
|
||||
cache_suffix: "?v=1.0",
|
||||
browser_spellcheck: false,
|
||||
gecko_spellcheck: false,
|
||||
// Auto-save feature
|
||||
auto_save: true,
|
||||
auto_save_interval: "30s",
|
||||
// Better mobile support
|
||||
mobile: {
|
||||
theme: "silver",
|
||||
plugins: ["lists", "autolink", "link", "image", "table"],
|
||||
toolbar: "bold italic | bullist numlist | link image",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptimizedEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
initialValue={props.initialData || ''}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: props.height || 400,
|
||||
menubar: false,
|
||||
toolbar: false, // No toolbar for read-only mode
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
||||
'insertdatetime', 'media', 'table'
|
||||
],
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: ${(props.height || 400) - 32}px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
.mce-content-body * {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
`,
|
||||
readonly: true,
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Minimal settings to prevent cursor jumping
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
// Disable problematic features
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
// Performance optimizations for read-only
|
||||
cache_suffix: '?v=1.0',
|
||||
browser_spellcheck: false,
|
||||
gecko_spellcheck: false,
|
||||
// Disable editing features
|
||||
paste_as_text: true,
|
||||
paste_enable_default_filters: false,
|
||||
paste_word_valid_elements: false,
|
||||
paste_retain_style_properties: false,
|
||||
// Additional read-only settings
|
||||
contextmenu: false,
|
||||
selection: false,
|
||||
// Disable all editing
|
||||
object_resizing: false,
|
||||
element_format: 'html',
|
||||
// Mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReadOnlyEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: 400,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: 368px;
|
||||
}
|
||||
`,
|
||||
placeholder: 'Start typing...',
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Critical settings to prevent cursor jumping
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
keep_styles: true,
|
||||
// Disable problematic features
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
// Better content handling
|
||||
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',
|
||||
// Mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimpleEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
initialValue={props.initialData || ''}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: props.height || 400,
|
||||
menubar: false,
|
||||
toolbar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
||||
'insertdatetime', 'media', 'table'
|
||||
],
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: ${(props.height || 400) - 32}px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
.mce-content-body * {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
`,
|
||||
readonly: true,
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
browser_spellcheck: false,
|
||||
gecko_spellcheck: false,
|
||||
paste_as_text: true,
|
||||
paste_enable_default_filters: false,
|
||||
contextmenu: false,
|
||||
selection: false,
|
||||
object_resizing: false,
|
||||
element_format: 'html'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimpleReadOnlyEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: 400,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: 368px;
|
||||
}
|
||||
`,
|
||||
placeholder: 'Start typing...',
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Critical settings for stability
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
keep_styles: true,
|
||||
// Disable all problematic features
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
// Content handling
|
||||
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',
|
||||
// Prevent automatic updates
|
||||
element_format: 'html',
|
||||
valid_children: '+body[style]',
|
||||
extended_valid_elements: 'span[*]',
|
||||
custom_elements: '~span',
|
||||
// Mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default StableEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: 400,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | table | code | help',
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: 368px;
|
||||
}
|
||||
`,
|
||||
placeholder: 'Start typing...',
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
// Critical settings to prevent cursor jumping
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
keep_styles: true,
|
||||
// Disable all problematic features
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
// Content handling
|
||||
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',
|
||||
// Prevent automatic updates
|
||||
element_format: 'html',
|
||||
valid_children: '+body[style]',
|
||||
extended_valid_elements: 'span[*]',
|
||||
custom_elements: '~span',
|
||||
// Mobile support
|
||||
mobile: {
|
||||
theme: 'silver',
|
||||
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
|
||||
toolbar: 'bold italic | bullist numlist | link image'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default StaticEditor;
|
||||
|
|
@ -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 (
|
||||
<Editor
|
||||
onInit={handleInit}
|
||||
initialValue={props.initialData || ''}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={{
|
||||
height: props.height || 400,
|
||||
menubar: false,
|
||||
toolbar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code',
|
||||
'insertdatetime', 'media', 'table'
|
||||
],
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
pointer-events: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-ms-user-select: none !important;
|
||||
}
|
||||
.mce-content-body {
|
||||
padding: 16px;
|
||||
min-height: ${(props.height || 400) - 32}px;
|
||||
pointer-events: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-ms-user-select: none !important;
|
||||
}
|
||||
.mce-content-body * {
|
||||
pointer-events: none !important;
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-ms-user-select: none !important;
|
||||
}
|
||||
`,
|
||||
readonly: true,
|
||||
branding: false,
|
||||
elementpath: false,
|
||||
resize: false,
|
||||
statusbar: false,
|
||||
auto_focus: false,
|
||||
forced_root_block: 'p',
|
||||
entity_encoding: 'raw',
|
||||
verify_html: false,
|
||||
cleanup: false,
|
||||
cleanup_on_startup: false,
|
||||
auto_resize: false,
|
||||
browser_spellcheck: false,
|
||||
gecko_spellcheck: false,
|
||||
paste_as_text: true,
|
||||
paste_enable_default_filters: false,
|
||||
contextmenu: false,
|
||||
selection: false,
|
||||
object_resizing: false,
|
||||
element_format: 'html',
|
||||
// Additional strict settings
|
||||
valid_children: false,
|
||||
extended_valid_elements: false,
|
||||
custom_elements: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default StrictReadOnlyEditor;
|
||||
|
|
@ -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<string, string>;
|
||||
className?: string;
|
||||
autoSave?: boolean;
|
||||
autoSaveInterval?: number;
|
||||
}
|
||||
|
||||
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
|
||||
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<any>(null);
|
||||
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(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 (
|
||||
<div className={`tinymce-editor-container ${className}`}>
|
||||
<Editor
|
||||
onInit={handleEditorInit}
|
||||
initialValue={initialData}
|
||||
onEditorChange={handleEditorChange}
|
||||
disabled={disabled}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
init={editorConfig}
|
||||
/>
|
||||
|
||||
{/* Status bar */}
|
||||
{isEditorLoaded && (
|
||||
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span>
|
||||
{autoSave && !readOnly ? "Auto-save enabled" : "Read-only mode"}
|
||||
</span>
|
||||
{lastSaved && autoSave && !readOnly && (
|
||||
<span>• Last saved: {lastSaved.toLocaleTimeString()}</span>
|
||||
)}
|
||||
<span>• {wordCount} characters</span>
|
||||
</div>
|
||||
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||
{features} mode
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Performance indicator */}
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Bundle size:{" "}
|
||||
{features === "basic"
|
||||
? "~150KB"
|
||||
: features === "standard"
|
||||
? "~200KB"
|
||||
: "~300KB"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TinyMCEEditor;
|
||||
|
|
@ -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 (
|
||||
<div className="ckeditor-view-wrapper">
|
||||
<CKEditor
|
||||
editor={Editor}
|
||||
data={props.initialData}
|
||||
disabled={true}
|
||||
config={{
|
||||
isReadOnly: true,
|
||||
content_style: `
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #111;
|
||||
background: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 1em 0 0.5em 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
`,
|
||||
height: props.height || 400,
|
||||
removePlugins: ["Title"],
|
||||
}}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.ckeditor-view-wrapper {
|
||||
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);
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||
min-height: ${props.height || 400}px;
|
||||
max-height: ${maxHeight}px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||
min-height: ${(props.height || 400) - 50}px;
|
||||
max-height: ${maxHeight - 50}px;
|
||||
overflow-y: auto !important;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||
background-color: #fdfdfd;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* 🌙 Dark mode support */
|
||||
:global(.dark) .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||
background-color: #111 !important;
|
||||
color: #f9fafb !important;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
:global(.dark) .ckeditor-view-wrapper h1,
|
||||
:global(.dark) .ckeditor-view-wrapper h2,
|
||||
:global(.dark) .ckeditor-view-wrapper h3,
|
||||
:global(.dark) .ckeditor-view-wrapper h4,
|
||||
:global(.dark) .ckeditor-view-wrapper h5,
|
||||
:global(.dark) .ckeditor-view-wrapper h6 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
:global(.dark) .ckeditor-view-wrapper blockquote {
|
||||
background-color: #1f2937 !important;
|
||||
border-left: 4px solid #374151 !important;
|
||||
color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* 🌙 Dark mode scrollbar */
|
||||
:global(.dark)
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
:global(.dark)
|
||||
.ckeditor-view-wrapper
|
||||
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
/* Read-only specific styling */
|
||||
.ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Hide toolbar */
|
||||
.ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||
display: none !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
// <div className="ckeditor-view-wrapper">
|
||||
// <CKEditor
|
||||
// editor={Editor}
|
||||
// data={props.initialData}
|
||||
// disabled={true}
|
||||
// config={{
|
||||
// // toolbar: [],
|
||||
// isReadOnly: true,
|
||||
// // Add content styling configuration for read-only mode
|
||||
// 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: 0;
|
||||
// }
|
||||
// p {
|
||||
// margin: 0.5em 0;
|
||||
// }
|
||||
// h1, h2, h3, h4, h5, h6 {
|
||||
// margin: 1em 0 0.5em 0;
|
||||
// }
|
||||
// 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;
|
||||
// }
|
||||
// `,
|
||||
// // Editor appearance settings
|
||||
// height: props.height || 400,
|
||||
// removePlugins: ['Title'],
|
||||
// }}
|
||||
// />
|
||||
// <style jsx>{`
|
||||
// .ckeditor-view-wrapper {
|
||||
// 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);
|
||||
// }
|
||||
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__main) {
|
||||
// min-height: ${props.height || 400}px;
|
||||
// max-height: ${maxHeight}px;
|
||||
// }
|
||||
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable) {
|
||||
// min-height: ${(props.height || 400) - 50}px;
|
||||
// max-height: ${maxHeight - 50}px;
|
||||
// overflow-y: auto !important;
|
||||
// scrollbar-width: thin;
|
||||
// scrollbar-color: #cbd5e1 #f1f5f9;
|
||||
// background-color:rgb(253, 253, 253);
|
||||
// border: 1px solid #d1d5db;
|
||||
// border-radius: 6px;
|
||||
// }
|
||||
|
||||
// /* Custom scrollbar styling for webkit browsers */
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
|
||||
// width: 8px;
|
||||
// }
|
||||
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
|
||||
// background: #f1f5f9;
|
||||
// border-radius: 4px;
|
||||
// }
|
||||
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
|
||||
// background: #cbd5e1;
|
||||
// border-radius: 4px;
|
||||
// }
|
||||
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
|
||||
// background: #94a3b8;
|
||||
// }
|
||||
|
||||
// /* Ensure content doesn't overflow */
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable .ck-content) {
|
||||
// overflow: hidden;
|
||||
// }
|
||||
|
||||
// /* Read-only specific styling */
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-editor__editable.ck-read-only) {
|
||||
// background-color: #f8fafc;
|
||||
// color: #4b5563;
|
||||
// cursor: default;
|
||||
// }
|
||||
|
||||
// /* Hide toolbar for view-only mode */
|
||||
// .ckeditor-view-wrapper :global(.ck.ck-toolbar) {
|
||||
// display: none !important;
|
||||
// }
|
||||
// `}</style>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default ViewEditor;
|
||||
|
|
@ -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<FileWithPreview[]>([]);
|
||||
const [useAi, setUseAI] = useState(false);
|
||||
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
|
||||
const [tag, setTag] = useState("");
|
||||
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
|
||||
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [thumbnailValidation, setThumbnailValidation] = useState("");
|
||||
const [filesValidation, setFileValidation] = useState("");
|
||||
const [diseData, setDiseData] = useState<DiseData>();
|
||||
const [selectedWritingType, setSelectedWritingType] = useState("single");
|
||||
const [status, setStatus] = useState<"publish" | "draft" | "scheduled">(
|
||||
"publish",
|
||||
);
|
||||
const [isScheduled, setIsScheduled] = useState(false);
|
||||
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
|
||||
const [startTimeValue, setStartTimeValue] = useState<string>("");
|
||||
|
||||
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<typeof createArticleSchema>;
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<UserSettingSchema>(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<typeof createArticleSchema>) => {
|
||||
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<typeof createArticleSchema>,
|
||||
) => {
|
||||
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<typeof createArticleSchema>) => {
|
||||
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 (
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
alt={file.name}
|
||||
src={URL.createObjectURL(file)}
|
||||
className=" rounded border p-0.5"
|
||||
/>
|
||||
);
|
||||
} 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) => (
|
||||
<div
|
||||
key={file.name}
|
||||
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="file-preview">{renderFilePreview(file)}</div>
|
||||
<div>
|
||||
<div className=" text-sm text-card-foreground">{file.name}</div>
|
||||
<div className=" text-xs font-light text-muted-foreground">
|
||||
{Math.round(file.size / 100) / 10 > 1000 ? (
|
||||
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
|
||||
) : (
|
||||
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
|
||||
)}
|
||||
{" kb"}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={String(index)}
|
||||
value={String(index)}
|
||||
checked={selectedMainImage === index + 1}
|
||||
onCheckedChange={() => setSelectedMainImage(index + 1)}
|
||||
/>
|
||||
<label htmlFor={String(index)} className="text-black text-xs">
|
||||
Jadikan Thumbnail
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="rounded-full"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveFile(file)}
|
||||
>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
</div>
|
||||
));
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<form
|
||||
className="flex flex-col lg:flex-row gap-8 text-black"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="w-full lg:w-[65%] bg-white rounded-lg p-8 flex flex-col gap-1">
|
||||
<p className="text-sm">Judulss</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder="Masukkan judul artikel"
|
||||
className="h-16 px-4 text-2xl leading-tight"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{errors?.title && (
|
||||
<p className="text-red-400 text-sm mb-3">{errors.title?.message}</p>
|
||||
)}
|
||||
<p className="text-sm mt-3">Slug</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder=""
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
className="w-full border rounded-lg dark:border-gray-400"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{errors?.slug && (
|
||||
<p className="text-red-400 text-sm mb-3">{errors.slug?.message}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Switch checked={useAi} onCheckedChange={setUseAI} />
|
||||
<p className="text-sm text-black">Bantuan AI</p>
|
||||
</div>
|
||||
|
||||
{useAi && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
value={selectedWritingType ?? ""}
|
||||
onValueChange={(value) => {
|
||||
if (value !== "") setSelectedWritingType(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||
<SelectValue placeholder="Writing Style" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="single">Single Article</SelectItem>
|
||||
{/* <SelectItem value="rewrite">Content Rewrite</SelectItem> */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedWritingType === "single" ? (
|
||||
<GenerateSingleArticleForm
|
||||
content={(data) => {
|
||||
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 : "",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<GenerateContentRewriteForm
|
||||
content={(data) => {
|
||||
setDiseData(data);
|
||||
setValue(
|
||||
"description",
|
||||
data?.articleBody ? data?.articleBody : "",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-sm mt-3">Deskripsi</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CustomEditor onChange={onChange} initialData={value} />
|
||||
)}
|
||||
/>
|
||||
{errors?.description && (
|
||||
<p className="text-red-400 text-sm mb-3">
|
||||
{errors.description?.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-sm mt-3">File Media</p>
|
||||
<Fragment>
|
||||
<div {...getRootProps({ className: "dropzone" })}>
|
||||
<input {...getInputProps()} />
|
||||
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
|
||||
<CloudUploadIcon size={50} className="text-gray-300" />
|
||||
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
|
||||
Tarik file disini atau klik untuk upload.
|
||||
</h4>
|
||||
<div className=" text-xs text-muted-foreground">
|
||||
( Upload file dengan format .jpg, .jpeg, atau .png. Ukuran
|
||||
maksimal 100mb.)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{files.length ? (
|
||||
<Fragment>
|
||||
<div>{fileList}</div>
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button onClick={() => setFiles([])} size="sm">
|
||||
Hapus Semua
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : null}
|
||||
</Fragment>
|
||||
{filesValidation !== "" && files.length < 1 && (
|
||||
<p className="text-red-400 text-sm mb-3">Upload File Media</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full lg:w-[35%] flex flex-col gap-8">
|
||||
<div className="h-fit bg-white rounded-lg p-8 flex flex-col gap-1">
|
||||
<p className="text-sm">Thubmnail</p>
|
||||
|
||||
{selectedMainImage && files.length >= selectedMainImage ? (
|
||||
<div className="flex flex-row">
|
||||
<img
|
||||
src={URL.createObjectURL(files[selectedMainImage - 1])}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
/>
|
||||
<Button
|
||||
className="border-none rounded-full"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedMainImage(null)}
|
||||
>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
</div>
|
||||
) : thumbnailImg.length > 0 ? (
|
||||
<div className="flex flex-row">
|
||||
<img
|
||||
src={URL.createObjectURL(thumbnailImg[0])}
|
||||
className="w-[30%]"
|
||||
alt="thumbnail"
|
||||
/>
|
||||
<Button
|
||||
className="border-none rounded-full"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setThumbnailImg([])}
|
||||
>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* <label htmlFor="file-upload">
|
||||
<button>Upload Thumbnail</button>
|
||||
</label>{" "} */}
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
multiple
|
||||
className="w-fit h-fit"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{thumbnailValidation !== "" && (
|
||||
<p className="text-red-400 text-sm mb-3">
|
||||
Upload thumbnail atau pilih dari File Media
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<p className="text-sm">Kreator</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="customCreatorName"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id="customCreatorName"
|
||||
type="text"
|
||||
placeholder="Masukkan judul artikel"
|
||||
className="w-full border rounded-lg dark:border-gray-400"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm">Tipe Kreator</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="source"
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full border rounded-lg text-sm dark:border-gray-400">
|
||||
<SelectValue placeholder="Pilih tipe kreator" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="internal">Internal</SelectItem>
|
||||
<SelectItem value="external">External</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm mt-3">Kategori</p>
|
||||
{/* <Controller
|
||||
control={control}
|
||||
name="category"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ReactSelect
|
||||
className="basic-single text-black z-50"
|
||||
classNames={{
|
||||
control: (state: any) =>
|
||||
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
|
||||
}}
|
||||
classNamePrefix="select"
|
||||
value={value}
|
||||
onChange={(selected) => {
|
||||
onChange(selected);
|
||||
}}
|
||||
closeMenuOnSelect={false}
|
||||
components={animatedComponents}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
isMulti={true}
|
||||
placeholder="Kategori..."
|
||||
name="sub-module"
|
||||
options={listCategory}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors?.category && (
|
||||
<p className="text-red-400 text-sm mb-3">
|
||||
{errors.category?.message}
|
||||
</p>
|
||||
)} */}
|
||||
|
||||
<p className="text-sm">Tags</p>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags"
|
||||
render={({ field: { value } }) => (
|
||||
<div className="w-full">
|
||||
{/* Menampilkan tags */}
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{value.map((item: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm"
|
||||
variant="secondary"
|
||||
>
|
||||
{item}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const filteredTags = value.filter(
|
||||
(tag: string) => tag !== item,
|
||||
);
|
||||
if (filteredTags.length === 0) {
|
||||
setError("tags", {
|
||||
type: "manual",
|
||||
message: "Tags tidak boleh kosong",
|
||||
});
|
||||
} else {
|
||||
clearErrors("tags");
|
||||
setValue(
|
||||
"tags",
|
||||
filteredTags as [string, ...string[]],
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="text-red-500 text-xs ml-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Textarea input */}
|
||||
<Textarea
|
||||
id="tags"
|
||||
placeholder="Tekan Enter untuk menambahkan tag"
|
||||
value={tag ?? ""}
|
||||
onChange={(e) => setTag(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (tag.trim() !== "") {
|
||||
setValue("tags", [...value, tag.trim()]);
|
||||
setTag("");
|
||||
clearErrors("tags");
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="border rounded-lg"
|
||||
aria-label="Tags Input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors?.tags && (
|
||||
<p className="text-red-400 text-sm mb-3">{errors.tags?.message}</p>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 mt-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="schedule-switch"
|
||||
checked={isScheduled}
|
||||
onCheckedChange={setIsScheduled}
|
||||
/>
|
||||
<label htmlFor="schedule-switch" className="text-black text-sm">
|
||||
Publish dengan Jadwal
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isScheduled && (
|
||||
<div className="flex flex-col lg:flex-row gap-3 mt-2">
|
||||
{/* Pilih tanggal */}
|
||||
<div className="w-full lg:w-[140px] flex flex-col gap-2">
|
||||
<p className="text-sm">Tanggal</p>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full !h-[37px] lg:h-[37px] border-1 rounded-lg text-black"
|
||||
variant="outline"
|
||||
>
|
||||
{startDateValue
|
||||
? startDateValue.toLocaleDateString("en-CA")
|
||||
: "-"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{/* <PopoverContent className="bg-transparent p-0">
|
||||
<DatePicker
|
||||
selected={startDateValue}
|
||||
onChange={(date) =>
|
||||
setStartDateValue(date ?? undefined)
|
||||
}
|
||||
dateFormat="yyyy-MM-dd"
|
||||
className="w-full border rounded-lg px-2 py-1 text-black cursor-pointer h-[150px]"
|
||||
placeholderText="Pilih tanggal"
|
||||
/>
|
||||
</PopoverContent> */}
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Pilih waktu */}
|
||||
<div className="w-full lg:w-[140px] flex flex-col gap-2">
|
||||
<p className="text-sm">Waktu</p>
|
||||
<input
|
||||
type="time"
|
||||
value={startTimeValue}
|
||||
onChange={(e) => setStartTimeValue(e.target.value)}
|
||||
className="w-full border rounded-lg px-2 py-[6px] text-black"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={isScheduled && startDateValue == null}
|
||||
onClick={() =>
|
||||
isScheduled ? setStatus("scheduled") : setStatus("publish")
|
||||
}
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
<Button
|
||||
color="success"
|
||||
type="submit"
|
||||
onClick={() => setStatus("draft")}
|
||||
>
|
||||
<p className="text-white">Draft</p>
|
||||
</Button>
|
||||
|
||||
<Link href="/admin/article">
|
||||
<Button variant="outline" type="button">
|
||||
Kembali
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<number[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<number>();
|
||||
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 (
|
||||
<fieldset>
|
||||
<form className="flex flex-col w-full mt-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
|
||||
{/* <Select
|
||||
label="Writing Style"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedWritingSyle]}
|
||||
onChange={(e) =>
|
||||
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",
|
||||
],
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name}>{style.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={selectedWritingSyle}
|
||||
onValueChange={(value) => setSelectedWritingStyle(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||
<SelectValue placeholder="Writing Style" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name} value={style.name}>
|
||||
{style.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* <Select
|
||||
label="Article Size"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedArticleSize]}
|
||||
onChange={(e) => (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"],
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
{articleSize.map((size) => (
|
||||
<SelectItem key={size.value}>{size.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={selectedArticleSize}
|
||||
onValueChange={(value) => setSelectedArticleSize(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||
<SelectValue placeholder="Writing Style" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{articleSize.map((style) => (
|
||||
<SelectItem key={style.name} value={style.name}>
|
||||
{style.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* <Select
|
||||
label="Bahasa"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedLanguage]}
|
||||
onChange={(e) => (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"],
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="id">Indonesia</SelectItem>
|
||||
<SelectItem key="en">English</SelectItem>
|
||||
</SelectSection>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={selectedLanguage}
|
||||
onValueChange={(value) => setSelectedLanguage(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
|
||||
<SelectValue placeholder="Writing Style" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="id">Indonesia</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-3">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-sm">Text</p>
|
||||
</div>
|
||||
<div className="w-[78vw] lg:w-full">
|
||||
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
|
||||
</div>
|
||||
{mainKeyword == "" && (
|
||||
<p className="text-red-400 text-sm">Required</p>
|
||||
)}
|
||||
{articleIds.length < 3 && (
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
type="button"
|
||||
disabled={mainKeyword === "" || isLoading}
|
||||
className="my-5 w-full py-5 text-xs md:text-base"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
"Generate"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{articleIds.length > 0 && (
|
||||
<div className="flex flex-row gap-1 mt-2">
|
||||
{articleIds?.map((id, index) => (
|
||||
<Button
|
||||
type="button"
|
||||
key={id}
|
||||
onClick={() => setSelectedId(id)}
|
||||
disabled={isLoading && selectedId === id}
|
||||
variant={selectedId === id ? "default" : "outline"}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isLoading && selectedId === id ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
`Article ${index + 1}`
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (
|
||||
<div>
|
||||
<GetSeoScore id={String(selectedId)} />
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<number[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<number>();
|
||||
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 (
|
||||
<fieldset>
|
||||
<form className="flex flex-col w-full mt-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 w-full">
|
||||
{/* <Select
|
||||
label="Writing Style"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedWritingSyle]}
|
||||
onChange={(e) =>
|
||||
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",
|
||||
],
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name}>{style.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={selectedWritingSyle}
|
||||
onValueChange={(value) => {
|
||||
if (value !== "") setSelectedWritingStyle(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
||||
<SelectValue placeholder="Writing Style" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{writingStyle.map((style) => (
|
||||
<SelectItem key={style.name} value={style.name}>
|
||||
{style.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* <Select
|
||||
label="Article Size"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedArticleSize]}
|
||||
onChange={(e) => (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"],
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
{articleSize.map((size) => (
|
||||
<SelectItem key={size.value}>{size.name}</SelectItem>
|
||||
))}
|
||||
</SelectSection>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={selectedArticleSize}
|
||||
onValueChange={(value) => {
|
||||
if (value !== "") setSelectedArticleSize(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
||||
<SelectValue placeholder="Article Size" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{articleSize.map((style) => (
|
||||
<SelectItem key={style.name} value={style.value}>
|
||||
{style.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* <Select
|
||||
label="Bahasa"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
placeholder=""
|
||||
selectedKeys={[selectedLanguage]}
|
||||
onChange={(e) => (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"],
|
||||
}}
|
||||
>
|
||||
<SelectSection>
|
||||
<SelectItem key="id">Indonesia</SelectItem>
|
||||
<SelectItem key="en">English</SelectItem>
|
||||
</SelectSection>
|
||||
</Select> */}
|
||||
<Select
|
||||
value={selectedLanguage}
|
||||
onValueChange={(value) => {
|
||||
if (value !== "") setSelectedLanguage(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full border border-gray-300 dark:border-gray-400 rounded-lg text-black">
|
||||
<SelectValue placeholder="Language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="id">Indonesia</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col mt-3">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-sm">Main Keyword</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => generateAll(mainKeyword)}
|
||||
disabled={isLoading} // tambahkan state kontrol loading
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
"Process"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="mainKeyword"
|
||||
placeholder="Masukkan keyword utama"
|
||||
value={mainKeyword}
|
||||
onChange={(e) => setMainKeyword(e.target.value)}
|
||||
className="w-full mt-1 border border-gray-300 rounded-lg dark:border-gray-400"
|
||||
/>
|
||||
|
||||
{mainKeyword == "" && (
|
||||
<p className="text-red-400 text-sm">Required</p>
|
||||
)}
|
||||
<div className="flex flex-row gap-2 items-center mt-3">
|
||||
<p className="text-sm">Title</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => generateTitle(mainKeyword)}
|
||||
disabled={mainKeyword === ""}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder=""
|
||||
value={title}
|
||||
onChange={(e) => 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 == "" && <p className="text-red-400 text-sm">Required</p>} */}
|
||||
<div className="flex flex-row gap-2 items-center mt-2">
|
||||
<p className="text-sm">Additional Keyword</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="text-sm"
|
||||
size="sm"
|
||||
onClick={() => generateKeywords(mainKeyword)}
|
||||
disabled={mainKeyword === ""}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="additionalKeyword"
|
||||
placeholder=""
|
||||
value={additionalKeyword}
|
||||
onChange={(e) => setAdditionalKeyword(e.target.value)}
|
||||
className="mt-1 border rounded-lg dark:bg-transparent dark:border-gray-400"
|
||||
aria-label="Additional Keyword"
|
||||
/>
|
||||
|
||||
{/* {additionalKeyword == "" && (
|
||||
<p className="text-red-400 text-sm">Required</p>
|
||||
)} */}
|
||||
{/* {articleIds.length < 3 && (
|
||||
<Button color="primary" className="my-5 w-full py-5 text-xs md:text-base" type="button" onPress={onSubmit} isDisabled={mainKeyword == "" || title == "" || additionalKeyword == ""}>
|
||||
Generate
|
||||
</Button>
|
||||
)} */}
|
||||
{articleIds.length < 3 && (
|
||||
<Button
|
||||
className="my-5 w-full py-5 text-xs md:text-base"
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={
|
||||
mainKeyword === "" || title === "" || additionalKeyword === ""
|
||||
}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{articleIds.length > 0 && (
|
||||
<div className="flex flex-row gap-1 mt-2">
|
||||
{articleIds.map((id, index) => (
|
||||
<Button
|
||||
type="button"
|
||||
key={id}
|
||||
onClick={() => 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}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<div>
|
||||
<GetSeoScore id={String(selectedId)} />
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<any>([]);
|
||||
const [warningSEO, setWarningSEO] = useState<any>([]);
|
||||
const [optimizedSEO, setOptimizedSEO] = useState<any>([]);
|
||||
|
||||
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 (
|
||||
<div className="overflow-y-auto my-2">
|
||||
<div className="text-black flex flex-col rounded-md gap-3">
|
||||
<p className="font-semibold text-lg"> SEO Score</p>
|
||||
{totalScoreSEO ? (
|
||||
<div className="flex flex-row gap-5 w-full">
|
||||
{/* <CircularProgress
|
||||
aria-label=""
|
||||
color="warning"
|
||||
showValueLabel={true}
|
||||
size="lg"
|
||||
value={Number(totalScoreSEO) * 100}
|
||||
/> */}
|
||||
<CustomCircularProgress value={Number(totalScoreSEO) * 100} />
|
||||
<div>
|
||||
{/* <ApexChartDonut value={Number(totalScoreSEO) * 100} /> */}
|
||||
</div>
|
||||
<div className="flex flex-row gap-5">
|
||||
<div className="px-2 py-1 border radius-md flex flex-row gap-2 items-center border-red-500 rounded-lg">
|
||||
{/* <TimesIcon size={15} className="text-danger" /> */}
|
||||
Error : {errorSEO.length || 0}
|
||||
</div>
|
||||
<div className="px-2 py-1 border radius-md flex flex-row gap-2 items-center border-yellow-500 rounded-lg">
|
||||
{/* <p className="text-warning w-[15px] h-[15px] text-center mt-[-10px]">
|
||||
!
|
||||
</p> */}
|
||||
Warning : {warningSEO.length || 0}
|
||||
</div>
|
||||
<div className="px-2 py-1 border radius-md flex flex-row gap-2 items-center border-green-500 rounded-lg">
|
||||
{/* <CheckIcon size={15} className="text-success" /> */}
|
||||
Optimize : {optimizedSEO.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
"Belum ada Data"
|
||||
)}
|
||||
{totalScoreSEO && (
|
||||
// <Accordion
|
||||
// variant="splitted"
|
||||
// itemClasses={{
|
||||
// base: "!bg-transparent",
|
||||
// title: "text-black",
|
||||
// }}
|
||||
// >
|
||||
// <AccordionItem
|
||||
// key="1"
|
||||
// aria-label="Error"
|
||||
// // startContent={<TimesIcon size={20} className="text-danger" />}
|
||||
// title={`${errorSEO?.length || 0} Errors`}
|
||||
// >
|
||||
// <div className="flex flex-col gap-2">
|
||||
// {errorSEO?.map((item: any) => (
|
||||
// <p key={item} className="w-full border border-red-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3">
|
||||
// {item}
|
||||
// </p>
|
||||
// ))}
|
||||
// </div>
|
||||
// </AccordionItem>
|
||||
// <AccordionItem
|
||||
// key="2"
|
||||
// aria-label="Warning"
|
||||
// // startContent={
|
||||
// // <p className="text-warning w-[20px] h-[20px] text-center mt-[-10px]">
|
||||
// // !
|
||||
// // </p>
|
||||
// // }
|
||||
// title={`${warningSEO?.length || 0} Warnings`}
|
||||
// >
|
||||
// <div className="flex flex-col gap-2">
|
||||
// {warningSEO?.map((item: any) => (
|
||||
// <p key={item} className="w-full border border-yellow-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3">
|
||||
// {item}
|
||||
// </p>
|
||||
// ))}
|
||||
// </div>
|
||||
// </AccordionItem>
|
||||
// <AccordionItem
|
||||
// key="3"
|
||||
// aria-label="Optimized"
|
||||
// // startContent={<CheckIcon size={20} className="text-success" />}
|
||||
// title={`${optimizedSEO?.length || 0} Optimized`}
|
||||
// >
|
||||
// <div className="flex flex-col gap-2">
|
||||
// {optimizedSEO?.map((item: any) => (
|
||||
// <p key={item} className="w-full border border-green-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3">
|
||||
// {item}
|
||||
// </p>
|
||||
// ))}
|
||||
// </div>
|
||||
// </AccordionItem>
|
||||
// </Accordion>
|
||||
<Accordion type="multiple" className="w-full">
|
||||
<AccordionItem value="error">
|
||||
<AccordionTrigger className="text-black">{`${
|
||||
errorSEO?.length || 0
|
||||
} Errors`}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{errorSEO?.map((item: any) => (
|
||||
<p
|
||||
key={item}
|
||||
className="w-full border border-red-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3"
|
||||
>
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="warning">
|
||||
<AccordionTrigger className="text-black">{`${
|
||||
warningSEO?.length || 0
|
||||
} Warnings`}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{warningSEO?.map((item: any) => (
|
||||
<p
|
||||
key={item}
|
||||
className="w-full border border-yellow-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3"
|
||||
>
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="optimized">
|
||||
<AccordionTrigger className="text-black">{`${
|
||||
optimizedSEO?.length || 0
|
||||
} Optimized`}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{optimizedSEO?.map((item: any) => (
|
||||
<p
|
||||
key={item}
|
||||
className="w-full border border-green-500 rounded-md h-[40px] text-left flex flex-col justify-center px-3"
|
||||
>
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<section className="relative bg-[#f7f0e3] py-24">
|
||||
{/* TOP CENTER CONTENT */}
|
||||
<div className="absolute left-1/2 top-8 flex -translate-x-1/2 flex-col items-center gap-6">
|
||||
<p className="text-sm font-semibold uppercase tracking-widest text-gray-400">
|
||||
Manage All your channels from Multipool
|
||||
</p>
|
||||
|
||||
{/* SOCIAL ICONS */}
|
||||
<div className="flex gap-6">
|
||||
{socials.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="flex items-center justify-center rounded-full"
|
||||
>
|
||||
<Image src={item.icon} alt={item.name} width={40} height={40} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto grid grid-cols-1 items-center gap-16 px-6 md:grid-cols-2">
|
||||
{/* PHONE IMAGE */}
|
||||
{/* PHONE WRAPPER */}
|
||||
<div className="flex justify-center">
|
||||
<div className="relative w-[320px] h-[640px]">
|
||||
{/* PHONE IMAGE */}
|
||||
<Image
|
||||
src="/image/phone.png"
|
||||
alt="App Preview"
|
||||
width={320}
|
||||
height={640}
|
||||
className="object-contain"
|
||||
fill
|
||||
className="object-contain z-10 pointer-events-none"
|
||||
/>
|
||||
|
||||
{/* CHAT AREA */}
|
||||
<div className="absolute top-[120px] left-[25px] right-[25px] bottom-[120px] overflow-hidden z-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
{messages.map((msg, index) => (
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 1.2 }}
|
||||
className={`max-w-[80%] px-4 py-3 rounded-2xl text-sm shadow ${
|
||||
msg.type === "user"
|
||||
? "bg-blue-600 text-white self-end rounded-br-sm"
|
||||
: "bg-gray-200 text-gray-800 self-start rounded-bl-sm"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TEXT CONTENT */}
|
||||
|
|
@ -58,14 +80,7 @@ export default function AboutSection() {
|
|||
|
||||
<p className="text-sm leading-relaxed text-gray-600">
|
||||
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...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,19 @@ 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 (
|
||||
<>
|
||||
<header className="relative w-full bg-white overflow-hidden">
|
||||
{/* SIDEBAR */}
|
||||
<aside
|
||||
className={`fixed right-0 top-0 z-50 h-full w-[280px] bg-[#966314] text-white transition-transform duration-300 ${
|
||||
open ? "translate-x-0" : "translate-x-full"
|
||||
|
|
@ -30,10 +37,14 @@ export default function Header() {
|
|||
<MenuItem icon={<Home size={18} />} label="Home" />
|
||||
<MenuItem icon={<Box size={18} />} label="Product" />
|
||||
<MenuItem icon={<Briefcase size={18} />} label="Services" />
|
||||
<MenuItem icon={<Newspaper size={18} />} label="News and Services" />
|
||||
<MenuItem
|
||||
icon={<Newspaper size={18} />}
|
||||
label="News and Services"
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* HERO */}
|
||||
<div className="container mx-auto flex min-h-[90vh] items-center px-6">
|
||||
<div className="flex-1 space-y-6">
|
||||
<h1 className="text-4xl font-extrabold leading-tight md:text-6xl">
|
||||
|
|
@ -47,6 +58,7 @@ export default function Header() {
|
|||
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => setContactOpen(true)}
|
||||
className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
|
||||
>
|
||||
Contact Us
|
||||
|
|
@ -64,6 +76,10 @@ export default function Header() {
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* CONTACT MODAL */}
|
||||
{contactOpen && <ContactDialog onClose={() => setContactOpen(false)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -75,3 +91,140 @@ function MenuItem({ icon, label }: { icon: React.ReactNode; label: string }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContactDialog({ onClose }: { onClose: () => void }) {
|
||||
const [contactMethod, setContactMethod] = useState("office");
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[999] bg-black/40 backdrop-blur-sm flex items-end md:items-center justify-center">
|
||||
{/* CONTAINER */}
|
||||
<div
|
||||
className="
|
||||
w-full
|
||||
h-[90vh] md:h-auto
|
||||
md:max-w-2xl
|
||||
bg-white
|
||||
rounded-t-3xl md:rounded-2xl
|
||||
p-5 md:p-8
|
||||
shadow-2xl
|
||||
relative
|
||||
overflow-y-auto
|
||||
"
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 md:right-6 md:top-6 text-gray-500 hover:text-black"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<h2 className="text-xl md:text-2xl font-bold text-[#966314] mb-2">
|
||||
Contact Us
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-gray-500 mb-6">
|
||||
Select a contact method and fill in your personal information. We will
|
||||
get back to you shortly.
|
||||
</p>
|
||||
|
||||
{/* Contact Method */}
|
||||
<div className="space-y-3 md:space-y-4 mb-6">
|
||||
<RadioGroup
|
||||
value={contactMethod}
|
||||
onValueChange={setContactMethod}
|
||||
className="space-y-3"
|
||||
>
|
||||
{/* Option 1 */}
|
||||
<div
|
||||
className={`flex items-start space-x-3 rounded-xl p-4 cursor-pointer border transition ${
|
||||
contactMethod === "office" ? "border-[#966314]" : "border-muted"
|
||||
}`}
|
||||
>
|
||||
<RadioGroupItem value="office" id="office" className="mt-1" />
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="office" className="font-medium cursor-pointer">
|
||||
Office Presentation
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Our team will come to your office for a presentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option 2 */}
|
||||
<div
|
||||
className={`flex items-start space-x-3 rounded-xl p-4 cursor-pointer border transition ${
|
||||
contactMethod === "hais" ? "border-[#966314]" : "border-muted"
|
||||
}`}
|
||||
>
|
||||
<RadioGroupItem value="hais" id="hais" className="mt-1" />
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="hais" className="font-medium cursor-pointer">
|
||||
Via HAIs
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Online consultation through HAIs platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fullName">
|
||||
Full Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="fullName" placeholder="Enter full name" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="email" type="email" placeholder="email@example.com" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">
|
||||
Phone Number <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="phone" placeholder="08xx xxxx xxxx" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company">
|
||||
Company Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="company" placeholder="PT. Example Company" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mt-4">
|
||||
<Label htmlFor="message">Message / Requirement</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
placeholder="Describe your needs or questions..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="mt-6 flex flex-col-reverse md:flex-row gap-3 md:justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full md:w-auto rounded-xl border px-6 py-3 hover:bg-gray-100"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button className="w-full md:w-auto rounded-xl bg-[#966314] px-6 py-3 text-white hover:bg-[#7c520f]">
|
||||
Send Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@ export type OptionProps = {
|
|||
active?: boolean;
|
||||
};
|
||||
|
||||
const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: OptionProps) => {
|
||||
const Option = ({
|
||||
Icon,
|
||||
title,
|
||||
selected,
|
||||
setSelected,
|
||||
open,
|
||||
notifs,
|
||||
active,
|
||||
}: OptionProps) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isActive = active ?? selected === title;
|
||||
|
||||
|
|
@ -23,7 +31,7 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
|
|||
onMouseLeave={() => setHovered(false)}
|
||||
className={`relative flex h-12 w-full px-3 items-center rounded-xl transition-all duration-200 cursor-pointer group ${
|
||||
isActive
|
||||
? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25"
|
||||
? "bg-gradient-to-r from-[#966314] to-[#966314] text-white shadow-lg shadow-emerald-500/25"
|
||||
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800"
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
|
|
@ -46,11 +54,13 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
|
|||
open ? "w-12" : "w-full"
|
||||
}`}
|
||||
>
|
||||
<div className={`text-lg transition-all duration-200 ${
|
||||
<div
|
||||
className={`text-lg transition-all duration-200 ${
|
||||
isActive
|
||||
? "text-white"
|
||||
: "text-slate-500 group-hover:text-slate-700"
|
||||
}`}>
|
||||
}`}
|
||||
>
|
||||
<Icon />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -93,9 +103,7 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
|
|||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3, type: "spring" }}
|
||||
className={`absolute right-3 top-1/2 -translate-y-1/2 size-5 rounded-full text-xs font-semibold flex items-center justify-center ${
|
||||
isActive
|
||||
? "bg-white text-emerald-500"
|
||||
: "bg-red-500 text-white"
|
||||
isActive ? "bg-white text-emerald-500" : "bg-red-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{notifs}
|
||||
|
|
|
|||
|
|
@ -383,9 +383,6 @@ const SidebarContent = ({
|
|||
);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// NORMAL ITEM
|
||||
// =============================
|
||||
return (
|
||||
<Link href={item.link!} key={item.title}>
|
||||
<Option
|
||||
|
|
|
|||
|
|
@ -470,18 +470,18 @@ export default function DashboardContainer() {
|
|||
</div>
|
||||
|
||||
{/* RIGHT - QUICK ACTIONS */}
|
||||
<div className="bg-amber-800 rounded-2xl shadow p-6 text-white space-y-4">
|
||||
<div className="bg-[#966314] rounded-2xl shadow p-6 text-white space-y-4">
|
||||
<h2 className="text-lg font-semibold">Quick Actions</h2>
|
||||
|
||||
<button className="w-full border border-amber-600 bg-amber-700 hover:bg-amber-600 transition py-3 rounded-xl text-sm font-medium">
|
||||
<button className="w-full border border-white bg-[#966314] hover:bg-[#966314] transition py-3 rounded-xl text-sm font-medium">
|
||||
+ Create New Article
|
||||
</button>
|
||||
|
||||
<button className="w-full border border-amber-600 bg-amber-700 hover:bg-amber-600 transition py-3 rounded-xl text-sm font-medium">
|
||||
<button className="w-full border border-white bg-[#966314] hover:bg-[#966314] transition py-3 rounded-xl text-sm font-medium">
|
||||
+ Update Product
|
||||
</button>
|
||||
|
||||
<button className="w-full border border-amber-600 bg-amber-700 hover:bg-amber-600 transition py-3 rounded-xl text-sm font-medium">
|
||||
<button className="w-full border border-white bg-[#966314] hover:bg-[#966314] transition py-3 rounded-xl text-sm font-medium">
|
||||
+ Upload Media
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Search, Filter, Eye, Pencil, Trash2, Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NewsImage() {
|
||||
const [activeCategory, setActiveCategory] = useState("All");
|
||||
|
|
@ -97,11 +98,12 @@ export default function NewsImage() {
|
|||
Create and manage news articles and blog posts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href={"/admin/news-article/image/create"}>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create New Article
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* ================= CATEGORY FILTER ================= */}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -11,9 +11,9 @@
|
|||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-react": "^11.0.1",
|
||||
"@ckeditor/ckeditor5-watchdog": "^47.5.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@iconify/iconify": "^3.1.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
|
|
@ -23,14 +23,20 @@
|
|||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tinymce/tinymce-react": "^6.2.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"apexcharts": "^4.7.0",
|
||||
"axios": "^1.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"framer-motion": "^12.33.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lightningcss": "^1.30.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.1",
|
||||
"radix-ui": "^1.4.3",
|
||||
|
|
@ -38,6 +44,8 @@
|
|||
"react-datepicker": "^9.1.0",
|
||||
"react-day-picker": "^9.13.2",
|
||||
"react-dom": "19.2.3",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-select": "^5.10.2",
|
||||
"sweetalert2": "^11.26.18",
|
||||
"sweetalert2-react-content": "^5.1.1",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { httpGet, httpPost } from "./http-config/http-base-services";
|
||||
|
||||
export async function saveActivity(data: any, token?: string) {
|
||||
const headers = token
|
||||
? {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
: {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
const pathUrl = `/activity-logs`;
|
||||
return await httpPost(pathUrl, data, headers);
|
||||
}
|
||||
|
||||
export async function getActivity() {
|
||||
const pathUrl = `/activity-logs/statistics`;
|
||||
return await httpGet(pathUrl);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import Cookies from "js-cookie";
|
||||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpPostInterceptor,
|
||||
httpPutInterceptor,
|
||||
} from "./http-config/http-interceptor-services";
|
||||
import { httpGet } from "./http-config/http-base-services";
|
||||
|
||||
const token = Cookies.get("access_token");
|
||||
|
||||
export async function createAdvertise(data: any) {
|
||||
const pathUrl = `/advertisement`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function createMediaFileAdvertise(id: string | number, data: any) {
|
||||
const headers = {
|
||||
"Content-Type": "multipart/form-data",
|
||||
};
|
||||
const pathUrl = `/advertisement/upload/${id}`;
|
||||
return await httpPostInterceptor(pathUrl, data, headers);
|
||||
}
|
||||
|
||||
export async function getAdvertise(data: any) {
|
||||
const pathUrl = `/advertisement?page=${data?.page || 1}&limit=${
|
||||
data?.limit || ""
|
||||
}&placement=${data?.placement || ""}&isPublish=${data.isPublish || ""}`;
|
||||
return await httpGet(pathUrl);
|
||||
}
|
||||
|
||||
export async function getAdvertiseById(id: number) {
|
||||
const pathUrl = `/advertisement/${id}`;
|
||||
return await httpGet(pathUrl);
|
||||
}
|
||||
|
||||
export async function editAdvertise(data: any) {
|
||||
const pathUrl = `/advertisement/${data?.id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function editAdvertiseIsActive(data: any) {
|
||||
const pathUrl = `/advertisement/publish/${data?.id}?isPublish=${data?.isActive}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function deleteAdvertise(id: number) {
|
||||
const pathUrl = `/advertisement/${id}`;
|
||||
return await httpDeleteInterceptor(pathUrl);
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { PaginationRequest } from "@/types/globals";
|
||||
import { httpGet } from "./http-config/http-base-services";
|
||||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPostInterceptor,
|
||||
httpPutInterceptor,
|
||||
} from "./http-config/http-interceptor-services";
|
||||
|
||||
export async function getListArticle(props: PaginationRequest) {
|
||||
const {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
startDate,
|
||||
endDate,
|
||||
isPublish,
|
||||
category,
|
||||
sortBy,
|
||||
sort,
|
||||
categorySlug,
|
||||
isBanner,
|
||||
} = props;
|
||||
|
||||
return await httpGet(
|
||||
`/articles?limit=${limit}&page=${page}&isPublish=${
|
||||
isPublish === undefined ? "" : isPublish
|
||||
}&title=${search}&startDate=${startDate || ""}&endDate=${
|
||||
endDate || ""
|
||||
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
|
||||
sort || "desc"
|
||||
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export async function getArticlePagination(props: PaginationRequest) {
|
||||
const {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
startDate,
|
||||
endDate,
|
||||
category,
|
||||
sortBy,
|
||||
sort,
|
||||
categorySlug,
|
||||
isBanner,
|
||||
isPublish,
|
||||
source,
|
||||
} = props;
|
||||
|
||||
return await httpGetInterceptor(
|
||||
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${
|
||||
startDate || ""
|
||||
}&endDate=${endDate || ""}&categoryId=${category || ""}&source=${
|
||||
source || ""
|
||||
}&isPublish=${isPublish !== undefined ? isPublish : ""}&sortBy=${
|
||||
sortBy || "created_at"
|
||||
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
|
||||
isBanner || ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTopArticles(props: PaginationRequest) {
|
||||
const { page, limit, search, startDate, endDate, isPublish, category } =
|
||||
props;
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGet(
|
||||
`/articles?limit=${limit}&page=${page}&isPublish=${
|
||||
isPublish === undefined ? "" : isPublish
|
||||
}&title=${search}&startDate=${startDate || ""}&endDate=${
|
||||
endDate || ""
|
||||
}&category=${category || ""}&sortBy=view_count&sort=desc`,
|
||||
headers
|
||||
);
|
||||
}
|
||||
|
||||
export async function createArticle(data: any) {
|
||||
const pathUrl = `/articles`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function createArticleSchedule(data: any) {
|
||||
const pathUrl = `/articles/publish-scheduling?id=${data.id}&date=${data.date}`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function unPublishArticle(id: string, data: any) {
|
||||
const pathUrl = `/articles/${id}/unpublish`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function updateArticle(id: string, data: any) {
|
||||
const pathUrl = `/articles/${id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getArticleById(id: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGet(`/articles/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function getArticleBySlug(slug: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGet(`/articles/slug/${slug}`, headers);
|
||||
}
|
||||
|
||||
export async function deleteArticle(id: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpDeleteInterceptor(`articles/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function getArticleByCategory() {
|
||||
return await httpGetInterceptor(`/article-categories?limit=1000`);
|
||||
}
|
||||
export async function getCategoryPagination(data: any) {
|
||||
return await httpGet(
|
||||
`/article-categories?limit=${data?.limit}&page=${data?.page}&title=${data?.search}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadArticleFile(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPostInterceptor(`/article-files/${id}`, data, headers);
|
||||
}
|
||||
|
||||
export async function getArticleFiles() {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGet(`/article-files`, headers);
|
||||
}
|
||||
|
||||
export async function uploadArticleThumbnail(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPostInterceptor(`/articles/thumbnail/${id}`, data, headers);
|
||||
}
|
||||
|
||||
export async function deleteArticleFiles(id: number) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpDeleteInterceptor(`article-files/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function getUserLevelDataStat(startDate: string, endDate: string) {
|
||||
return await httpGet(
|
||||
`/articles/statistic/user-levels?startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
}
|
||||
export async function getStatisticMonthly(year: string) {
|
||||
return await httpGet(`/articles/statistic/monthly?year=${year}`);
|
||||
}
|
||||
export async function getStatisticMonthlyFeedback(year: string) {
|
||||
return await httpGet(`/feedbacks/statistic/monthly?year=${year}`);
|
||||
}
|
||||
export async function getStatisticSummary() {
|
||||
return await httpGet(`/articles/statistic/summary`);
|
||||
}
|
||||
|
||||
export async function submitApproval(data: {
|
||||
articleId: number;
|
||||
message: string;
|
||||
statusId: number;
|
||||
}) {
|
||||
return await httpPostInterceptor(`/article-approvals`, data);
|
||||
}
|
||||
|
||||
export async function updateIsBannerArticle(id: number, status: boolean) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
const pathUrl = `/articles/banner/${id}?isBanner=${status}`;
|
||||
return await httpPutInterceptor(pathUrl, headers);
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
import { httpGet, httpPost, httpPost2 } from "./http-config/disestages-services";
|
||||
|
||||
|
||||
interface GenerateKeywordsAndTitleRequestData {
|
||||
keyword: string;
|
||||
style: string;
|
||||
website: string;
|
||||
connectToWeb: boolean;
|
||||
lang: string;
|
||||
pointOfView: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
interface GenerateArticleRequest {
|
||||
advConfig: string;
|
||||
style: string;
|
||||
website: string;
|
||||
connectToWeb: boolean;
|
||||
lang: string;
|
||||
pointOfView: string;
|
||||
title: string;
|
||||
imageSource: string;
|
||||
mainKeyword: string;
|
||||
additionalKeywords: string;
|
||||
targetCountry: null;
|
||||
articleSize: string;
|
||||
projectId: number;
|
||||
createdBy: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
type BulkArticleRequest = {
|
||||
style: string;
|
||||
website: string;
|
||||
connectToWeb: boolean;
|
||||
lang: string;
|
||||
pointOfView: string;
|
||||
imageSource: string;
|
||||
targetCountry: null;
|
||||
articleSize: string;
|
||||
projectId: number;
|
||||
data: { title: string; mainKeyword: string; additionalKeywords: string }[];
|
||||
createdBy: string;
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
interface ContentRewriteRequest {
|
||||
advConfig: string;
|
||||
context: string | null;
|
||||
style: string;
|
||||
sentiment: string;
|
||||
clientId: string;
|
||||
createdBy: string;
|
||||
contextType: string;
|
||||
urlContext: string | null;
|
||||
lang: string;
|
||||
}
|
||||
|
||||
export type ContentBankRequest = {
|
||||
query: string;
|
||||
page: number;
|
||||
userId: string;
|
||||
limit: number;
|
||||
status: number[];
|
||||
isSingle: boolean;
|
||||
createdBy: string;
|
||||
sort: { column: string; sort: string }[];
|
||||
};
|
||||
|
||||
export async function getGenerateTitle(
|
||||
data: GenerateKeywordsAndTitleRequestData
|
||||
) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/generate-title", headers, data);
|
||||
}
|
||||
|
||||
export async function getGenerateKeywords(
|
||||
data: GenerateKeywordsAndTitleRequestData
|
||||
) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/generate-keywords", headers, data);
|
||||
}
|
||||
|
||||
export async function generateDataArticle(data: GenerateArticleRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/save-article", headers, data);
|
||||
}
|
||||
|
||||
export async function approveArticle(props: { id: number[] }) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/approve-article", headers, props);
|
||||
}
|
||||
export async function rejectArticle(props: { id: number[] }) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/reject-article", headers, props);
|
||||
}
|
||||
|
||||
export async function getGenerateTopicKeywords(data: {
|
||||
keyword: string;
|
||||
count: number;
|
||||
}) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/generate-topic-keywords", headers, data);
|
||||
}
|
||||
|
||||
export async function saveBulkArticle(data: BulkArticleRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/save-bulk-article", headers, data);
|
||||
}
|
||||
|
||||
export async function getDetailArticle(id: number) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpGet(`ai-writer/article/findArticleById/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function generateSpeechToText(data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/speech-to-text", headers, data);
|
||||
}
|
||||
|
||||
export async function getTranscriptById(id: number) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpGet(`ai-writer/speech-to-text/findById/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function getGenerateRewriter(data: ContentRewriteRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/create-rewriter", headers, data);
|
||||
}
|
||||
|
||||
export async function getListTranscript(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/speech-to-text/datatable", headers, data);
|
||||
}
|
||||
|
||||
export async function getListArticleDraft(data: ContentBankRequest) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/article/datatable", headers, data);
|
||||
}
|
||||
|
||||
export async function updateManualArticle(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/update-article", headers, data);
|
||||
}
|
||||
|
||||
export async function getSeoScore(id: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpGet(`ai-writer/article/checkSEOScore/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function regenerateArticle(id: number | string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpGet(`ai-writer/re-create-article/${id}`, headers);
|
||||
}
|
||||
|
||||
export async function saveManualContext(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost("ai-writer/create-article", headers, data);
|
||||
}
|
||||
|
||||
export async function facebookHumasData() {
|
||||
const data = {
|
||||
monitoringId: "f33b666c-ac07-4cd6-a64e-200465919e8c",
|
||||
page: 133,
|
||||
limit: 10,
|
||||
userId: "0qrwedt9EcuLyOiBUbqhzjd0BwGRjDBd",
|
||||
};
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost2(
|
||||
"monitoring-media-social/datatable/facebook",
|
||||
headers,
|
||||
data
|
||||
);
|
||||
}
|
||||
export async function tiktokHumasData() {
|
||||
const data = {
|
||||
monitoringId: "1e301867-9599-4d82-ab57-9f7931f96903",
|
||||
page: 1,
|
||||
limit: 10,
|
||||
userId: "0qrwedt9EcuLyOiBUbqhzjd0BwGRjDBd",
|
||||
};
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization:
|
||||
"Basic bmdETFBQaW9ycGx6bncyalRxVmUzWUZDejV4cUtmVUo6UHJEaERXUmNvdkJSNlc1Sg==",
|
||||
};
|
||||
return await httpPost2(
|
||||
"monitoring-media-social/datatable/tiktok",
|
||||
headers,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import axios from "axios";
|
||||
|
||||
const baseURL = "https://qudo.id/api";
|
||||
|
||||
const axiosBaseInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9",
|
||||
},
|
||||
});
|
||||
|
||||
export default axiosBaseInstance;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import axios from "axios";
|
||||
import { postSignIn } from "../master-user";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const baseURL = "https://qudo.id/api";
|
||||
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
|
||||
const axiosInterceptorInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9",
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
axiosInterceptorInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log("Config interceptor : ", config);
|
||||
const accessToken = Cookies.get("access_token");
|
||||
if (accessToken) {
|
||||
if (config.headers)
|
||||
config.headers.Authorization = "Bearer " + accessToken;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
axiosInterceptorInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log("Response interceptor : ", response);
|
||||
return response;
|
||||
},
|
||||
async function (error) {
|
||||
console.log("Error interceptor : ", error.response.status);
|
||||
const originalRequest = error.config;
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const data = {
|
||||
grantType: "refresh_token",
|
||||
refreshToken: refreshToken,
|
||||
clientId: "mediahub-app",
|
||||
};
|
||||
console.log("refresh token ", data);
|
||||
const res = await postSignIn(data);
|
||||
if (res?.error) {
|
||||
Object.keys(Cookies.get()).forEach((cookieName) => {
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
} else {
|
||||
const { access_token } = res?.data;
|
||||
const { refresh_token } = res?.data;
|
||||
if (access_token) {
|
||||
Cookies.set("access_token", access_token);
|
||||
Cookies.set("refresh_token", refresh_token);
|
||||
}
|
||||
}
|
||||
|
||||
return axiosInterceptorInstance(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export default axiosInterceptorInstance;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import axios from "axios";
|
||||
|
||||
const baseURL = "https://disestages.com/api";
|
||||
|
||||
const axiosDisestagesInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export default axiosDisestagesInstance;
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import axiosDisestagesInstance from "./disestages-instance";
|
||||
import axios from "axios";
|
||||
const baseURL = "https://staging.disestages.com/api";
|
||||
|
||||
export async function httpPost(pathUrl: any, headers: any, data?: any) {
|
||||
const response = await axiosDisestagesInstance
|
||||
.post(pathUrl, data, { headers })
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpGet(pathUrl: any, headers: any) {
|
||||
const response = await axiosDisestagesInstance
|
||||
.get(pathUrl, { headers })
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpPost2(pathUrl: any, headers: any, data?: any) {
|
||||
const response = await axios
|
||||
.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
.post(pathUrl, data, { headers })
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import axiosBaseInstance from "./axios-base-instance";
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9",
|
||||
};
|
||||
|
||||
export async function httpGet(pathUrl: any, headers?: any) {
|
||||
console.log("X-HEADERS : ", defaultHeaders);
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
};
|
||||
|
||||
console.log("Merged Headers : ", mergedHeaders);
|
||||
|
||||
const response = await axiosBaseInstance
|
||||
.get(pathUrl, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.data.success) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpPost(pathUrl: any, data: any, headers?: any) {
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
};
|
||||
const response = await axiosBaseInstance
|
||||
.post(pathUrl, data, { headers: mergedHeaders })
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { useRouter } from "next/navigation";
|
||||
import Cookies from "js-cookie";
|
||||
import axiosInterceptorInstance from "./axios-interceptor-instance";
|
||||
import { getCsrfToken } from "../master-user";
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "629293dd-69cc-4904-a545-23deef377bd9",
|
||||
};
|
||||
|
||||
export async function httpGetInterceptor(pathUrl: any) {
|
||||
console.log("X-HEADERS : ", defaultHeaders);
|
||||
const response = await axiosInterceptorInstance
|
||||
.get(pathUrl, { headers: defaultHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else if (response?.status == 401) {
|
||||
Cookies.set("is_logout", "true");
|
||||
window.location.href = "/";
|
||||
return {
|
||||
error: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpPostInterceptor(
|
||||
pathUrl: any,
|
||||
data: any,
|
||||
headers?: any,
|
||||
) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
|
||||
};
|
||||
|
||||
const response = await axiosInterceptorInstance
|
||||
.post(pathUrl, data, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else if (response?.status == 401) {
|
||||
Cookies.set("is_logout", "true");
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpPutInterceptor(
|
||||
pathUrl: any,
|
||||
data: any,
|
||||
headers?: any,
|
||||
) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
|
||||
};
|
||||
const response = await axiosInterceptorInstance
|
||||
.put(pathUrl, data, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else if (response?.status == 401) {
|
||||
Cookies.set("is_logout", "true");
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpDeleteInterceptor(pathUrl: any, headers?: any) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
|
||||
};
|
||||
|
||||
const response = await axiosInterceptorInstance
|
||||
.delete(pathUrl, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else if (response?.status == 401) {
|
||||
Cookies.set("is_logout", "true");
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { PaginationRequest } from "@/types/globals";
|
||||
import Cookies from "js-cookie";
|
||||
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||
|
||||
const token = Cookies.get("access_token");
|
||||
|
||||
export async function createMagazine(data: any) {
|
||||
const pathUrl = `/magazines`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getListMagazine(props: PaginationRequest) {
|
||||
const { page, limit, search, startDate, endDate } = props;
|
||||
return await httpGetInterceptor(
|
||||
`/magazines?limit=${limit}&page=${page}&title=${search}&startDate=${
|
||||
startDate || ""
|
||||
}&endDate=${endDate || ""}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateMagazine(id: string, data: any) {
|
||||
const pathUrl = `/magazines/${id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getMagazineById(id: string) {
|
||||
return await httpGetInterceptor(`/magazines/${id}`);
|
||||
}
|
||||
|
||||
export async function deleteMagazine(id: string) {
|
||||
return await httpDeleteInterceptor(`magazines/${id}`);
|
||||
}
|
||||
|
||||
export async function uploadMagazineFile(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPostInterceptor(`/magazine-files/${id}`, data, headers);
|
||||
}
|
||||
|
||||
export async function uploadMagazineThumbnail(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPostInterceptor(`/magazines/thumbnail/${id}`, data, headers);
|
||||
}
|
||||
|
||||
export async function deleteMagazineFiles(id: number) {
|
||||
return await httpDeleteInterceptor(`magazine-files/${id}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||
|
||||
const token = Cookies.get("access_token");
|
||||
|
||||
export async function createCategory(data: any) {
|
||||
const pathUrl = `/article-categories`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function updateCategory(id: string, data: any) {
|
||||
const pathUrl = `/article-categories/${id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getCategoryById(id: number) {
|
||||
return await httpGetInterceptor(`/article-categories/${id}`);
|
||||
}
|
||||
|
||||
export async function deleteCategory(id: number) {
|
||||
return await httpDeleteInterceptor(`article-categories/${id}`);
|
||||
}
|
||||
|
||||
export async function uploadCategoryThumbnail(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPostInterceptor(`/article-categories/thumbnail/${id}`, data, headers);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor } from "./http-config/http-interceptor-services";
|
||||
|
||||
const token = Cookies.get("access_token");
|
||||
|
||||
export async function listUserRole(data: any) {
|
||||
return await httpGetInterceptor(
|
||||
`/user-roles?limit=${data.limit}&page=${data.page}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function createMasterUserRole(data: any) {
|
||||
const pathUrl = `/user-roles`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getMasterUserRoleById(id: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGetInterceptor(`/user-roles/${id}`);
|
||||
}
|
||||
|
||||
export async function deleteMasterUserRole(id: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpDeleteInterceptor(`/user-roles/${id}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { httpGet, httpPost } from "./http-config/http-base-services";
|
||||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPostInterceptor,
|
||||
httpPutInterceptor,
|
||||
} from "./http-config/http-interceptor-services";
|
||||
import { hex } from "framer-motion";
|
||||
|
||||
const token = Cookies.get("access_token");
|
||||
const id = Cookies.get("uie");
|
||||
|
||||
export async function listMasterUsers(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGet(`/users?page=${data.page}&limit=${data.limit}`, headers);
|
||||
}
|
||||
|
||||
export async function createMasterUser(data: any) {
|
||||
const pathUrl = `/users`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function emailValidation(data: any) {
|
||||
const pathUrl = `/users/email-validation`;
|
||||
return await httpPost(pathUrl, data);
|
||||
}
|
||||
export async function setupEmail(data: any) {
|
||||
const pathUrl = `/users/setup-email`;
|
||||
return await httpPost(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getDetailMasterUsers(id: string) {
|
||||
const pathUrl = `/users/detail/${id}`;
|
||||
return await httpGetInterceptor(pathUrl);
|
||||
}
|
||||
|
||||
export async function editMasterUsers(data: any, id: string) {
|
||||
const pathUrl = `/users/${id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function deleteMasterUser(id: string) {
|
||||
const pathUrl = `/users/${id}`;
|
||||
return await httpDeleteInterceptor(pathUrl);
|
||||
}
|
||||
|
||||
export async function postSignIn(data: any) {
|
||||
const pathUrl = `/users/login`;
|
||||
return await httpPost(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getProfile(token?: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
const pathUrl = `/users/info`;
|
||||
return await httpGet(pathUrl, headers);
|
||||
}
|
||||
|
||||
export async function updateProfile(data: any) {
|
||||
const pathUrl = `/users/${id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
export async function savePassword(data: any) {
|
||||
const pathUrl = `/users/save-password`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function resetPassword(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpPost(`/users/reset-password`, headers, data);
|
||||
}
|
||||
|
||||
export async function checkUsernames(username: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpPost(`/users/forgot-password`, headers, { username });
|
||||
}
|
||||
|
||||
export async function otpRequest(email: string, name: string) {
|
||||
const pathUrl = `/users/otp-request`;
|
||||
return await httpPost(pathUrl, { email, name });
|
||||
}
|
||||
|
||||
export async function otpValidation(email: string, otpCode: string) {
|
||||
const pathUrl = `/users/otp-validation`;
|
||||
return await httpPost(pathUrl, { email, otpCode });
|
||||
}
|
||||
|
||||
// export async function postArticleComment(data: any) {
|
||||
// const headers = token
|
||||
// ? {
|
||||
// "content-type": "application/json",
|
||||
// Authorization: `${token}`,
|
||||
// }
|
||||
// : {
|
||||
// "content-type": "application/json",
|
||||
// };
|
||||
// return await httpPost(`/article-comments`, headers, data);
|
||||
// }
|
||||
|
||||
export async function postArticleComment(data: any) {
|
||||
const pathUrl = `/article-comments`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function editArticleComment(data: any, id: number) {
|
||||
const pathUrl = `/article-comments/${id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getArticleComment(id: string) {
|
||||
const pathUrl = `/article-comments?isPublic=false&articleId=${id}`;
|
||||
return await httpGet(pathUrl);
|
||||
}
|
||||
|
||||
export async function deleteArticleComment(id: number) {
|
||||
const pathUrl = `/article-comments/${id}`;
|
||||
return await httpDeleteInterceptor(pathUrl);
|
||||
}
|
||||
|
||||
export async function getCsrfToken() {
|
||||
const pathUrl = "csrf-token";
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return httpGet(pathUrl, headers);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { PaginationRequest } from "@/types/globals";
|
||||
import { httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||
import { httpGet } from "./http-config/http-base-services";
|
||||
|
||||
export async function createCustomStaticPage(data: any) {
|
||||
const pathUrl = `/custom-static-pages`;
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function editCustomStaticPage(data: any) {
|
||||
const pathUrl = `/custom-static-pages/${data.id}`;
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getCustomStaticPage(props: PaginationRequest) {
|
||||
const { page, limit, search } = props;
|
||||
const pathUrl = `/custom-static-pages?limit=${limit}&page=${page}&title=${search}`;
|
||||
return await httpGetInterceptor(pathUrl);
|
||||
}
|
||||
|
||||
export async function getCustomStaticDetail(id: string) {
|
||||
return await httpGetInterceptor(`/custom-static-pages/${id}`);
|
||||
}
|
||||
|
||||
export async function getCustomStaticDetailBySlug(slug: string) {
|
||||
const pathUrl = `/custom-static-pages/slug/${slug}`;
|
||||
return await httpGet(pathUrl);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
import Cookies from "js-cookie";
|
||||
import { httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||
const token = Cookies.get("access_token");
|
||||
|
||||
export async function getAllUserLevels(data?: any) {
|
||||
const pathUrl = `user-levels?limit=${data?.limit || ""}&levelNumber=${
|
||||
data?.levelNumber || ""
|
||||
}&name=${data?.search || ""}&page=${data?.page || "1"}`
|
||||
return await httpGetInterceptor(pathUrl);
|
||||
}
|
||||
export async function getUserLevels(id: string) {
|
||||
return await httpGetInterceptor(`user-levels/${id}`);
|
||||
}
|
||||
|
||||
export async function getAccountById(id: string) {
|
||||
return await httpGetInterceptor(`user-account/findById/${id}`);
|
||||
}
|
||||
|
||||
export async function createUserLevels(data: any) {
|
||||
return await httpPostInterceptor(`user-levels`, data);
|
||||
}
|
||||
|
||||
export async function editUserLevels(id: string, data: any) {
|
||||
return await httpPutInterceptor(`user-levels/${id}`, data);
|
||||
}
|
||||
|
||||
export async function changeIsApproval(data: any) {
|
||||
return await httpPutInterceptor(`user-levels/enable-approval`, data);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// Global chunk loading error handler
|
||||
export function setupChunkErrorHandler() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Handle chunk loading errors
|
||||
window.addEventListener('error', (event) => {
|
||||
const error = event.error;
|
||||
|
||||
// Check if it's a chunk loading error
|
||||
if (
|
||||
error?.name === 'ChunkLoadError' ||
|
||||
error?.message?.includes('Loading chunk') ||
|
||||
error?.message?.includes('Failed to fetch')
|
||||
) {
|
||||
console.warn('Chunk loading error detected:', error);
|
||||
|
||||
// Prevent the error from being logged to console
|
||||
event.preventDefault();
|
||||
|
||||
// Show a user-friendly message
|
||||
const message = document.createElement('div');
|
||||
message.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
z-index: 9999;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
message.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>⚠️</span>
|
||||
<span>Application update detected. Please refresh the page.</span>
|
||||
</div>
|
||||
<button onclick="window.location.reload()" style="
|
||||
margin-top: 8px;
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
">Refresh</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Auto-remove after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.parentNode.removeChild(message);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections (which might include chunk loading errors)
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const error = event.reason;
|
||||
|
||||
if (
|
||||
error?.name === 'ChunkLoadError' ||
|
||||
error?.message?.includes('Loading chunk') ||
|
||||
error?.message?.includes('Failed to fetch')
|
||||
) {
|
||||
console.warn('Unhandled chunk loading rejection:', error);
|
||||
event.preventDefault();
|
||||
|
||||
// Reload the page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-setup when this module is imported
|
||||
if (typeof window !== 'undefined') {
|
||||
setupChunkErrorHandler();
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import dynamic from 'next/dynamic';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
interface DynamicImportOptions {
|
||||
ssr?: boolean;
|
||||
loading?: () => React.ReactElement;
|
||||
retries?: number;
|
||||
retryDelay?: number;
|
||||
}
|
||||
|
||||
export function createSafeDynamicImport<T extends ComponentType<any>>(
|
||||
importFn: () => Promise<{ default: T }>,
|
||||
options: DynamicImportOptions = {}
|
||||
) {
|
||||
const { ssr = false, loading, retries = 3, retryDelay = 1000 } = options;
|
||||
|
||||
return dynamic(
|
||||
() => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let attempts = 0;
|
||||
|
||||
const attemptImport = async () => {
|
||||
try {
|
||||
const module = await importFn();
|
||||
resolve(module.default);
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
|
||||
// Check if it's a chunk loading error
|
||||
if (
|
||||
(error as any)?.name === 'ChunkLoadError' ||
|
||||
(error as any)?.message?.includes('Loading chunk') ||
|
||||
(error as any)?.message?.includes('Failed to fetch')
|
||||
) {
|
||||
if (attempts < retries) {
|
||||
console.warn(`Chunk loading failed, retrying... (${attempts}/${retries})`);
|
||||
setTimeout(attemptImport, retryDelay);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
attemptImport();
|
||||
});
|
||||
},
|
||||
{
|
||||
ssr,
|
||||
loading,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Predefined safe dynamic imports for common components
|
||||
export const SafeCustomEditor = createSafeDynamicImport(
|
||||
() => import('@/components/editor/custom-editor'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export const SafeViewEditor = createSafeDynamicImport(
|
||||
() => import('@/components/editor/view-editor'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export const SafeReactApexChart = createSafeDynamicImport(
|
||||
() => import('react-apexcharts'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
"use client";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function convertDateFormat(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
|
||||
const day = date.getDate();
|
||||
const month = date.getMonth() + 1;
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
const formattedTime =
|
||||
(hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
|
||||
const formattedDate =
|
||||
(day < 10 ? "0" : "") +
|
||||
day +
|
||||
"-" +
|
||||
(month < 10 ? "0" : "") +
|
||||
month +
|
||||
"-" +
|
||||
year +
|
||||
", " +
|
||||
formattedTime;
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
// export function convertDateFormatNoTime(dateString: any) {
|
||||
// var date = new Date(dateString);
|
||||
|
||||
// var day = date.getDate();
|
||||
// var month = date.getMonth() + 1;
|
||||
// var year = date.getFullYear();
|
||||
|
||||
// var formattedDate =
|
||||
// (day < 10 ? "0" : "") +
|
||||
// day +
|
||||
// "-" +
|
||||
// (month < 10 ? "0" : "") +
|
||||
// month +
|
||||
// "-" +
|
||||
// year;
|
||||
|
||||
// return formattedDate;
|
||||
// }
|
||||
|
||||
export function convertDateFormatNoTimeV2(dateString: string | Date) {
|
||||
const date = new Date(dateString);
|
||||
|
||||
const day = date.getDate();
|
||||
const month = date.getMonth() + 1;
|
||||
const year = date.getFullYear();
|
||||
|
||||
const formattedDate =
|
||||
year +
|
||||
"-" +
|
||||
(month < 10 ? "0" : "") +
|
||||
month +
|
||||
"-" +
|
||||
(day < 10 ? "0" : "") +
|
||||
day;
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
export function formatTextToHtmlTag(text: string) {
|
||||
if (text) {
|
||||
const htmlText = text.replaceAll("\\n", "<br>").replaceAll(/"/g, "");
|
||||
return { __html: htmlText };
|
||||
}
|
||||
}
|
||||
|
||||
const LoadScript = () => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://cdn.userway.org/widget.js";
|
||||
script.setAttribute("data-account", "X36s1DpjqB");
|
||||
script.async = true;
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
// Cleanup if needed
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null; // Tidak perlu merender apa-apa
|
||||
};
|
||||
|
||||
export default LoadScript;
|
||||
|
||||
export function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function textEllipsis(
|
||||
str: string,
|
||||
maxLength: number,
|
||||
{ side = "end", ellipsis = "..." } = {},
|
||||
) {
|
||||
if (str !== undefined && str?.length > maxLength) {
|
||||
switch (side) {
|
||||
case "start":
|
||||
return ellipsis + str.slice(-(maxLength - ellipsis.length));
|
||||
|
||||
case "end":
|
||||
default:
|
||||
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function htmlToString(str: string) {
|
||||
if (str == undefined || str == null) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
str
|
||||
.replaceAll(/<style[^>]*>.*<\/style>/gm, "")
|
||||
// Remove script tags and content
|
||||
.replaceAll(/<script[^>]*>.*<\/script>/gm, "")
|
||||
// Replace  ,&ndash
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll("–", "-")
|
||||
// Replace quotation mark
|
||||
.replaceAll("“", '"')
|
||||
.replaceAll("”", '"')
|
||||
// Remove all opening, closing and orphan HTML tags
|
||||
.replaceAll(/<[^>]+>/gm, "")
|
||||
// Remove leading spaces and repeated CR/LF
|
||||
.replaceAll(/([\n\r]+ +)+/gm, "")
|
||||
);
|
||||
}
|
||||
|
||||
export function formatMonthString(dateString: string) {
|
||||
const months = [
|
||||
"Januari",
|
||||
"Februari",
|
||||
"Maret",
|
||||
"April",
|
||||
"Mei",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"Agustus",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Desember",
|
||||
];
|
||||
|
||||
const date = new Date(dateString); // Konversi string ke objek Date
|
||||
const day = date.getDate(); // Ambil tanggal
|
||||
const month = months[date.getMonth()]; // Ambil nama bulan
|
||||
const year = date.getFullYear(); // Ambil tahun
|
||||
|
||||
return `${day} ${month} ${year}`;
|
||||
}
|
||||
|
||||
export function convertDateFormatNoTime(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
||||
const day = `${date.getDate()}`.padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function formatDate(date: Date | null) {
|
||||
if (!date) return "";
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
Loading…
Reference in New Issue