This commit is contained in:
Anang Yusman 2025-09-29 00:46:28 +08:00
parent d3b37d510e
commit 670f86a92c
33 changed files with 2467 additions and 435 deletions

19
app/detail/[id]/page.tsx Normal file
View File

@ -0,0 +1,19 @@
import DetailContent from "@/components/details/details-content";
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
import Image from "next/image";
export default function Home() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-[#F2F4F3] max-w-7xl mx-auto">
<Navbar />
<div className="flex-1">
<DetailContent />
</div>
<Footer />
</div>
</div>
);
}

View File

@ -56,6 +56,8 @@ export default function DetailContent() {
null null
); );
const [selectedIndex, setSelectedIndex] = useState(0);
const [tabArticles, setTabArticles] = useState<Article[]>([]); const [tabArticles, setTabArticles] = useState<Article[]>([]);
const [activeTab, setActiveTab] = useState<TabKey>("trending"); const [activeTab, setActiveTab] = useState<TabKey>("trending");
@ -206,21 +208,40 @@ export default function DetailContent() {
</div> </div>
<div className="w-full h-auto mb-6"> <div className="w-full h-auto mb-6">
{articleDetail?.files?.[0]?.fileUrl ? ( <div className="w-full">
<Image <Image
src={articleDetail.files[0].fileUrl} src={articleDetail?.files[selectedIndex].fileUrl}
alt="Berita" alt={articleDetail?.files[selectedIndex].fileAlt || "Berita"}
width={800} width={800}
height={400} height={400}
className="rounded-lg w-full object-cover" className="rounded-lg w-full object-cover"
/> />
) : ( </div>
<div className="w-full h-[400px] bg-gray-100 flex items-center justify-center rounded-lg">
<p className="text-gray-400 text-sm">Gambar tidak tersedia</p>
</div>
)}
<div className=" flex flex-row w-fit rounded overflow-hidden mr-5 gap-3"> {/* Thumbnail */}
<div className="flex gap-2 mt-3 overflow-x-auto">
{articleDetail?.files.map((file: any, index: number) => (
<button
key={file.id || index}
onClick={() => setSelectedIndex(index)}
className={`border-2 rounded-lg overflow-hidden ${
selectedIndex === index
? "border-red-500"
: "border-transparent"
}`}
>
<Image
src={file.fileUrl}
alt={file.fileAlt || "Thumbnail"}
width={100}
height={80}
className="object-cover"
/>
</button>
))}
</div>
<div className=" flex flex-row w-fit rounded overflow-hidden mr-5 gap-3 mt-4">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<p className="text-red-500 font-semibold">0</p> <p className="text-red-500 font-semibold">0</p>
<p className="text-red-500 font-semibold">SHARES</p> <p className="text-red-500 font-semibold">SHARES</p>
@ -338,13 +359,13 @@ export default function DetailContent() {
</div> </div>
<div className="flex relative"> <div className="flex relative">
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<p className="text-gray-700 leading-relaxed text-justify"> <div className="text-gray-700 leading-relaxed text-justify">
{/* <span className="text-black font-bold text-md"> <div
Mikulnews.com - dangerouslySetInnerHTML={{
</span> */} __html: articleDetail?.htmlDescription || "",
}}
{articleDetail?.description} />
</p> </div>
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<span className="font-semibold text-sm text-gray-700"> <span className="font-semibold text-sm text-gray-700">

Binary file not shown.

View File

@ -5,36 +5,166 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "@/vendor/ckeditor5/build/ckeditor"; import Editor from "@/vendor/ckeditor5/build/ckeditor";
function CustomEditor(props) { function CustomEditor(props) {
const maxHeight = props.maxHeight || 600;
return ( return (
<CKEditor <div className="ckeditor-wrapper">
editor={Editor} <CKEditor
data={props.initialData} editor={Editor}
onChange={(event, editor) => { data={props.initialData}
const data = editor.getData(); onChange={(event, editor) => {
console.log({ event, editor, data }); const data = editor.getData();
props.onChange(data); console.log({ event, editor, data });
}} props.onChange(data);
config={{ }}
toolbar: [ config={{
"heading", toolbar: [
"fontsize", "heading",
"bold", "fontsize",
"italic", "bold",
"link", "italic",
"numberedList", "link",
"bulletedList", "numberedList",
"undo", "bulletedList",
"redo", "undo",
"alignment", "redo",
"outdent", "alignment",
"indent", "outdent",
"blockQuote", "indent",
"insertTable", "blockQuote",
"codeBlock", "insertTable",
"sourceEditing", "codeBlock",
], "sourceEditing",
}} ],
/> content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #111 !important;
background: #fff !important;
margin: 0;
padding: 1rem;
}
p {
margin: 0.5em 0;
}
h1, h2, h3, h4, h5, h6 {
margin: 1em 0 0.5em 0;
color: inherit !important;
}
ul, ol {
margin: 0.5em 0;
padding-left: 2em;
}
blockquote {
margin: 1em 0;
padding: 0.5em 1em;
border-left: 4px solid #d1d5db;
background-color: #f9fafb;
color: inherit !important;
}
`,
height: props.height || 400,
removePlugins: ["Title"],
mobile: {
theme: "silver",
},
}}
/>
<style jsx>{`
.ckeditor-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-wrapper :global(.ck.ck-editor__main) {
min-height: ${props.height || 400}px;
max-height: ${maxHeight}px;
}
.ckeditor-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: #fff !important;
color: #111 !important;
}
/* Dark mode support */
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable) {
background: #111 !important;
color: #f9fafb !important;
}
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h1),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h2),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h3),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h4),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h5),
:global(.dark) .ckeditor-wrapper :global(.ck.ck-editor__editable h6) {
color: #f9fafb !important;
}
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable blockquote) {
background-color: #1f2937 !important;
border-left-color: #374151 !important;
color: #f3f4f6 !important;
}
/* Custom scrollbar styling for webkit browsers */
.ckeditor-wrapper :global(.ck.ck-editor__editable::-webkit-scrollbar) {
width: 8px;
}
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
background: #f1f5f9;
border-radius: 4px;
}
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
background: #cbd5e1;
border-radius: 4px;
}
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
background: #94a3b8;
}
/* Dark mode scrollbar */
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-track) {
background: #1f2937;
}
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb) {
background: #4b5563;
}
:global(.dark)
.ckeditor-wrapper
:global(.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover) {
background: #6b7280;
}
/* Ensure content doesn't overflow */
.ckeditor-wrapper :global(.ck.ck-editor__editable .ck-content) {
overflow: hidden;
}
`}</style>
</div>
); );
} }

View File

@ -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;

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,105 @@
"use client";
import React, { useEffect, useRef } from "react";
import { Editor } from "@tinymce/tinymce-react";
interface OptimizedEditorProps {
initialData?: string;
onChange?: (data: string) => void;
height?: number;
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
}
const OptimizedEditor: React.FC<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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,309 @@
"use client";
import React, { useRef, useState, useEffect } from "react";
import { Editor } from "@tinymce/tinymce-react";
interface TinyMCEEditorProps {
initialData?: string;
onChange?: (data: string) => void;
onReady?: (editor: any) => void;
height?: number;
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
features?: "basic" | "standard" | "full";
toolbar?: string;
language?: string;
uploadUrl?: string;
uploadHeaders?: Record<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,
// readonly: readOnly,
// disabled,
branding: false,
elementpath: false,
resize: false,
statusbar: !readOnly,
// Performance optimizations
cache_suffix: "?v=1.0",
browser_spellcheck: false,
gecko_spellcheck: false,
// Content styling
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
margin: 0;
padding: 16px;
}
.mce-content-body {
min-height: ${height - 32}px;
}
.mce-content-body:focus {
outline: none;
}
`,
// Image upload configuration
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
automatic_uploads: !!uploadUrl,
file_picker_types: "image",
// Better mobile support
mobile: {
theme: "silver",
plugins: ["lists", "autolink", "link", "image", "table"],
toolbar: "bold italic | bullist numlist | link image",
},
// Paste configuration
paste_as_text: false,
paste_enable_default_filters: true,
paste_word_valid_elements: "b,strong,i,em,h1,h2,h3,h4,h5,h6",
paste_retain_style_properties:
"color background-color font-size font-weight",
// Table configuration
table_default_styles: {
width: "100%",
},
table_default_attributes: {
border: "1",
},
// Code configuration
codesample_languages: [
{ text: "HTML/XML", value: "markup" },
{ text: "JavaScript", value: "javascript" },
{ text: "CSS", value: "css" },
{ text: "PHP", value: "php" },
{ text: "Python", value: "python" },
{ text: "Java", value: "java" },
{ text: "C", value: "c" },
{ text: "C++", value: "cpp" },
],
// ...feature config
...featureConfig,
// Custom toolbar if provided
...(toolbar && { toolbar }),
};
return (
<div className={`tinymce-editor-container ${className}`}>
<Editor
onInit={handleEditorInit}
initialValue={initialData}
onEditorChange={handleEditorChange}
disabled={disabled || readOnly}
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;

View File

@ -3,17 +3,261 @@ import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "@/vendor/ckeditor5/build/ckeditor"; import Editor from "@/vendor/ckeditor5/build/ckeditor";
function ViewEditor(props) { function ViewEditor(props) {
const maxHeight = props.maxHeight || 600; // Default max height 600px
return ( return (
<CKEditor <div className="ckeditor-view-wrapper">
editor={Editor} <CKEditor
data={props.initialData} editor={Editor}
disabled={true} data={props.initialData}
config={{ disabled={true}
// toolbar: [], config={{
isReadOnly: true, 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; 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;

View File

@ -339,8 +339,8 @@ export default function CreateArticleForm() {
function successSubmit(redirect: string, id: number, slug: string) { function successSubmit(redirect: string, id: number, slug: string) {
const url = const url =
`${window.location.protocol}//${window.location.host}` + `${window.location.protocol}//${window.location.host}` +
"/news/detail/" + "/detail/" +
`${id}-${slug}`; `${id}`;
MySwal.fire({ MySwal.fire({
title: "Sukses", title: "Sukses",
icon: "success", icon: "success",
@ -524,13 +524,21 @@ export default function CreateArticleForm() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="single">Single Article</SelectItem> <SelectItem value="single">Single Article</SelectItem>
<SelectItem value="rewrite">Content Rewrite</SelectItem> {/* <SelectItem value="rewrite">Content Rewrite</SelectItem> */}
</SelectContent> </SelectContent>
</Select> </Select>
{selectedWritingType === "single" ? ( {selectedWritingType === "single" ? (
<GenerateSingleArticleForm <GenerateSingleArticleForm
content={(data) => { content={(data) => {
setDiseData(data); setDiseData(data);
// setValue("title", data?.title ?? "", {
// shouldValidate: true,
// shouldDirty: true,
// });
// setValue("slug", generateSlug(data?.title ?? ""), {
// shouldValidate: true,
// shouldDirty: true,
// });
setValue( setValue(
"description", "description",
data?.articleBody ? data?.articleBody : "" data?.articleBody ? data?.articleBody : ""
@ -664,7 +672,10 @@ export default function CreateArticleForm() {
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500", "!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
}} }}
classNamePrefix="select" classNamePrefix="select"
onChange={onChange} value={value}
onChange={(selected) => {
onChange(selected);
}}
closeMenuOnSelect={false} closeMenuOnSelect={false}
components={animatedComponents} components={animatedComponents}
isClearable={true} isClearable={true}
@ -683,60 +694,7 @@ export default function CreateArticleForm() {
)} )}
<p className="text-sm">Tags</p> <p className="text-sm">Tags</p>
{/* <Controller
control={control}
name="tags"
render={({ field: { onChange, value } }) => (
<Textarea
type="text"
id="tags"
placeholder=""
label=""
value={tag}
onValueChange={setTag}
startContent={
<div className="flex flex-wrap gap-1">
{value.map((item, index) => (
<Chip
color="primary"
key={index}
className=""
onClose={() => {
const filteredTags = value.filter((tag) => tag !== item);
if (filteredTags.length === 0) {
setError("tags", {
type: "manual",
message: "Tags tidak boleh kosong",
});
} else {
clearErrors("tags");
setValue("tags", filteredTags as [string, ...string[]]);
}
}}
>
{item}
</Chip>
))}
</div>
}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
e.preventDefault();
}
}
}}
labelPlacement="outside"
className="w-full h-fit"
classNames={{
inputWrapper: ["border-1 rounded-lg", "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400"],
}}
variant="bordered"
/>
)}
/> */}
<Controller <Controller
control={control} control={control}
name="tags" name="tags"

View File

@ -549,7 +549,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
)} )}
<p className="text-sm mt-3">Deskripsi</p> <p className="text-sm mt-3">Deskripsi</p>
<Controller {/* <Controller
control={control} control={control}
name="description" name="description"
render={({ field: { onChange, value } }) => render={({ field: { onChange, value } }) =>
@ -572,8 +572,17 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
<p className="text-red-400 text-sm mb-3"> <p className="text-red-400 text-sm mb-3">
{errors.description?.message} {errors.description?.message}
</p> </p>
)} */}
<Controller
control={control}
name="description"
render={({ field }) => (
<CustomEditor onChange={field.onChange} initialData={field.value} />
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">{errors.description.message}</p>
)} )}
<p className="text-sm mt-3">File Media</p> <p className="text-sm mt-3">File Media</p>
{!isDetail && ( {!isDetail && (
<Fragment> <Fragment>
@ -672,6 +681,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
</div> </div>
<Button <Button
type="button"
className=" border-none rounded-full" className=" border-none rounded-full"
variant="outline" variant="outline"
color="danger" color="danger"
@ -943,14 +953,11 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
Simpan Simpan
</Button> </Button>
)} )}
{isDetail && {detailData?.isPublish === false && (
detailData?.isPublish === false && <Button type="button" color="primary" onClick={doPublish}>
detailData?.statusId !== 1 && Publish
Number(userId) === detailData?.createdById && ( </Button>
<Button type="button" color="primary" onClick={doPublish}> )}
Publish
</Button>
)}
{/* {!isDetail && ( {/* {!isDetail && (
<Button color="success" type="button"> <Button color="success" type="button">
<p className="text-white">Draft</p> <p className="text-white">Draft</p>

View File

@ -1,11 +1,20 @@
"use client"; "use client";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { close, error, loading } from "@/config/swal"; import { close, error, loading } from "@/config/swal";
import { delay } from "@/utils/global"; import { delay } from "@/utils/global";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getDetailArticle, getGenerateRewriter } from "@/service/generate-article"; import {
getDetailArticle,
getGenerateRewriter,
} from "@/service/generate-article";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import GetSeoScore from "./get-seo-score-form"; import GetSeoScore from "./get-seo-score-form";
@ -69,8 +78,11 @@ interface DiseData {
additionalKeywords: string; additionalKeywords: string;
} }
export default function GenerateContentRewriteForm(props: { content: (data: DiseData) => void }) { export default function GenerateContentRewriteForm(props: {
const [selectedWritingSyle, setSelectedWritingStyle] = useState("Informational"); content: (data: DiseData) => void;
}) {
const [selectedWritingSyle, setSelectedWritingStyle] =
useState("Informational");
const [selectedArticleSize, setSelectedArticleSize] = useState("News"); const [selectedArticleSize, setSelectedArticleSize] = useState("News");
const [selectedLanguage, setSelectedLanguage] = useState("id"); const [selectedLanguage, setSelectedLanguage] = useState("id");
const [mainKeyword, setMainKeyword] = useState(""); const [mainKeyword, setMainKeyword] = useState("");
@ -166,7 +178,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
))} ))}
</SelectSection> </SelectSection>
</Select> */} </Select> */}
<Select value={selectedWritingSyle} onValueChange={(value) => setSelectedWritingStyle(value)}> <Select
value={selectedWritingSyle}
onValueChange={(value) => setSelectedWritingStyle(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400"> <SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Writing Style" />
</SelectTrigger> </SelectTrigger>
@ -198,7 +213,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
))} ))}
</SelectSection> </SelectSection>
</Select> */} </Select> */}
<Select value={selectedArticleSize} onValueChange={(value) => setSelectedArticleSize(value)}> <Select
value={selectedArticleSize}
onValueChange={(value) => setSelectedArticleSize(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400"> <SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Writing Style" />
</SelectTrigger> </SelectTrigger>
@ -229,7 +247,10 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
<SelectItem key="en">English</SelectItem> <SelectItem key="en">English</SelectItem>
</SelectSection> </SelectSection>
</Select> */} </Select> */}
<Select value={selectedLanguage} onValueChange={(value) => setSelectedLanguage(value)}> <Select
value={selectedLanguage}
onValueChange={(value) => setSelectedLanguage(value)}
>
<SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400"> <SelectTrigger className="w-full border rounded-lg text-black dark:border-gray-400">
<SelectValue placeholder="Writing Style" /> <SelectValue placeholder="Writing Style" />
</SelectTrigger> </SelectTrigger>
@ -239,6 +260,7 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="flex flex-col mt-3"> <div className="flex flex-col mt-3">
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<p className="text-sm">Text</p> <p className="text-sm">Text</p>
@ -246,9 +268,16 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
<div className="w-[78vw] lg:w-full"> <div className="w-[78vw] lg:w-full">
<CustomEditor onChange={setMainKeyword} initialData={mainKeyword} /> <CustomEditor onChange={setMainKeyword} initialData={mainKeyword} />
</div> </div>
{mainKeyword == "" && <p className="text-red-400 text-sm">Required</p>} {mainKeyword == "" && (
<p className="text-red-400 text-sm">Required</p>
)}
{articleIds.length < 3 && ( {articleIds.length < 3 && (
<Button onClick={onSubmit} type="button" disabled={mainKeyword === "" || isLoading} className="my-5 w-full py-5 text-xs md:text-base"> <Button
onClick={onSubmit}
type="button"
disabled={mainKeyword === "" || isLoading}
className="my-5 w-full py-5 text-xs md:text-base"
>
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -263,7 +292,14 @@ export default function GenerateContentRewriteForm(props: { content: (data: Dise
{articleIds.length > 0 && ( {articleIds.length > 0 && (
<div className="flex flex-row gap-1 mt-2"> <div className="flex flex-row gap-1 mt-2">
{articleIds?.map((id, index) => ( {articleIds?.map((id, index) => (
<Button key={id} onClick={() => setSelectedId(id)} disabled={isLoading && selectedId === id} variant={selectedId === id ? "default" : "outline"} className="flex items-center gap-2"> <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 ? ( {isLoading && selectedId === id ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />

View File

@ -85,7 +85,7 @@ export default function GenerateSingleArticleForm(props: {
const [additionalKeyword, setAdditionalKeyword] = useState(""); const [additionalKeyword, setAdditionalKeyword] = useState("");
const [articleIds, setArticleIds] = useState<number[]>([]); const [articleIds, setArticleIds] = useState<number[]>([]);
const [selectedId, setSelectedId] = useState<number>(); const [selectedId, setSelectedId] = useState<number>();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(false);
const generateAll = async (keyword: string | undefined) => { const generateAll = async (keyword: string | undefined) => {
if (keyword) { if (keyword) {
@ -319,6 +319,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<p className="text-sm">Main Keyword</p> <p className="text-sm">Main Keyword</p>
<Button <Button
type="button"
variant="default" variant="default"
size="sm" size="sm"
onClick={() => generateAll(mainKeyword)} onClick={() => generateAll(mainKeyword)}
@ -350,6 +351,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-2 items-center mt-3"> <div className="flex flex-row gap-2 items-center mt-3">
<p className="text-sm">Title</p> <p className="text-sm">Title</p>
<Button <Button
type="button"
variant="default" variant="default"
size="sm" size="sm"
onClick={() => generateTitle(mainKeyword)} onClick={() => generateTitle(mainKeyword)}
@ -373,6 +375,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-2 items-center mt-2"> <div className="flex flex-row gap-2 items-center mt-2">
<p className="text-sm">Additional Keyword</p> <p className="text-sm">Additional Keyword</p>
<Button <Button
type="button"
className="text-sm" className="text-sm"
size="sm" size="sm"
onClick={() => generateKeywords(mainKeyword)} onClick={() => generateKeywords(mainKeyword)}
@ -417,6 +420,7 @@ export default function GenerateSingleArticleForm(props: {
<div className="flex flex-row gap-1 mt-2"> <div className="flex flex-row gap-1 mt-2">
{articleIds.map((id, index) => ( {articleIds.map((id, index) => (
<Button <Button
type="button"
key={id} key={id}
onClick={() => setSelectedId(id)} onClick={() => setSelectedId(id)}
disabled={isLoading && selectedId === id} disabled={isLoading && selectedId === id}

View File

@ -71,35 +71,37 @@ export default function HeaderGuard() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.map((article) => ( {articles.map((article) => (
<div key={article.id}> <div key={article.id}>
<Image <Link href={`/detail/${article.id}`}>
src={article.thumbnailUrl || "/default.jpg"} <Image
alt={article.title} src={article.thumbnailUrl || "/default.jpg"}
width={600} alt={article.title}
height={400} width={600}
className="w-full h-[250px] object-cover rounded-md" height={400}
/> className="w-full h-[250px] object-cover rounded-md"
<p className="text-xs font-semibold text-yellow-600 mt-4 uppercase"> />
{article.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 mt-4 uppercase">
article.categoryName || {article.categories?.[0]?.title ||
"BERITA"} article.categoryName ||
</p> "BERITA"}
<h3 className="text-lg font-bold mt-1">{article.title}</h3> </p>
<p className="text-sm text-gray-600 mt-2 line-clamp-3"> <h3 className="text-lg font-bold mt-1">{article.title}</h3>
{article.description} <p className="text-sm text-gray-600 mt-2 line-clamp-3">
</p> {article.description}
<p className="text-sm text-gray-700 mt-4"> </p>
By <strong>{article.createdByName}</strong> {" "} <p className="text-sm text-gray-700 mt-4">
{new Date(article.createdAt).toLocaleDateString("en-US", { By <strong>{article.createdByName}</strong> {" "}
month: "long", {new Date(article.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
<Link </p>
href={`/news/${article.id}`} <Link
className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline" href={`/detail/${article.id}`}
> className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline"
READ MORE &gt; >
READ MORE &gt;
</Link>
</Link> </Link>
</div> </div>
))} ))}
@ -125,34 +127,36 @@ export default function HeaderGuard() {
<div className="space-y-6 sticky top-28 h-fit"> <div className="space-y-6 sticky top-28 h-fit">
<h4 className="text-xl font-bold">Recent Posts</h4> <h4 className="text-xl font-bold">Recent Posts</h4>
{recentPosts.map((post, index) => ( {recentPosts.map((post, index) => (
<div key={post.id} className="flex gap-3"> <div key={post.id}>
<Image <Link className="flex gap-3" href={`/detail/${post.id}`}>
src={post.thumbnailUrl || "/default.jpg"} <Image
alt={post.title} src={post.thumbnailUrl || "/default.jpg"}
width={80} alt={post.title}
height={80} width={80}
className="w-20 h-20 object-cover rounded-md" height={80}
/> className="w-20 h-20 object-cover rounded-md"
<div className="flex-1"> />
<p className="text-xs font-semibold text-yellow-600 uppercase"> <div className="flex-1">
{post.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 uppercase">
post.categoryName || {post.categories?.[0]?.title ||
"BERITA"} post.categoryName ||
</p> "BERITA"}
<h5 className="text-sm font-semibold leading-snug"> </p>
{post.title} <h5 className="text-sm font-semibold leading-snug">
</h5> {post.title}
<p className="text-xs text-gray-500 mt-1"> </h5>
{new Date(post.createdAt).toLocaleDateString("en-US", { <p className="text-xs text-gray-500 mt-1">
month: "long", {new Date(post.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
</div> </p>
<div className="text-3xl font-bold text-yellow-300 leading-none"> </div>
{String(index + 1).padStart(2, "0")} <div className="text-3xl font-bold text-yellow-300 leading-none">
</div> {String(index + 1).padStart(2, "0")}
</div>
</Link>
</div> </div>
))} ))}
<div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded"> <div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded">

View File

@ -72,35 +72,37 @@ export default function HeaderLatest() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.map((article) => ( {articles.map((article) => (
<div key={article.id}> <div key={article.id}>
<Image <Link href={`/detail/${article.id}`}>
src={article.thumbnailUrl || "/default.jpg"} <Image
alt={article.title} src={article.thumbnailUrl || "/default.jpg"}
width={600} alt={article.title}
height={400} width={600}
className="w-full h-[250px] object-cover rounded-md" height={400}
/> className="w-full h-[250px] object-cover rounded-md"
<p className="text-xs font-semibold text-yellow-600 mt-4 uppercase"> />
{article.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 mt-4 uppercase">
article.categoryName || {article.categories?.[0]?.title ||
"BERITA"} article.categoryName ||
</p> "BERITA"}
<h3 className="text-lg font-bold mt-1">{article.title}</h3> </p>
<p className="text-sm text-gray-600 mt-2 line-clamp-3"> <h3 className="text-lg font-bold mt-1">{article.title}</h3>
{article.description} <p className="text-sm text-gray-600 mt-2 line-clamp-3">
</p> {article.description}
<p className="text-sm text-gray-700 mt-4"> </p>
By <strong>{article.createdByName}</strong> {" "} <p className="text-sm text-gray-700 mt-4">
{new Date(article.createdAt).toLocaleDateString("en-US", { By <strong>{article.createdByName}</strong> {" "}
month: "long", {new Date(article.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
<Link </p>
href={`/news/${article.id}`} <Link
className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline" href={`/detail/${article.id}`}
> className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline"
READ MORE &gt; >
READ MORE &gt;
</Link>
</Link> </Link>
</div> </div>
))} ))}
@ -126,34 +128,36 @@ export default function HeaderLatest() {
<div className="space-y-6 sticky top-28 h-fit"> <div className="space-y-6 sticky top-28 h-fit">
<h4 className="text-xl font-bold">Recent Posts</h4> <h4 className="text-xl font-bold">Recent Posts</h4>
{recentPosts.map((post, index) => ( {recentPosts.map((post, index) => (
<div key={post.id} className="flex gap-3"> <div key={post.id}>
<Image <Link className="flex gap-3" href={`/detail/${post.id}`}>
src={post.thumbnailUrl || "/default.jpg"} <Image
alt={post.title} src={post.thumbnailUrl || "/default.jpg"}
width={80} alt={post.title}
height={80} width={80}
className="w-20 h-20 object-cover rounded-md" height={80}
/> className="w-20 h-20 object-cover rounded-md"
<div className="flex-1"> />
<p className="text-xs font-semibold text-yellow-600 uppercase"> <div className="flex-1">
{post.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 uppercase">
post.categoryName || {post.categories?.[0]?.title ||
"BERITA"} post.categoryName ||
</p> "BERITA"}
<h5 className="text-sm font-semibold leading-snug"> </p>
{post.title} <h5 className="text-sm font-semibold leading-snug">
</h5> {post.title}
<p className="text-xs text-gray-500 mt-1"> </h5>
{new Date(post.createdAt).toLocaleDateString("en-US", { <p className="text-xs text-gray-500 mt-1">
month: "long", {new Date(post.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
</div> </p>
<div className="text-3xl font-bold text-yellow-300 leading-none"> </div>
{String(index + 1).padStart(2, "0")} <div className="text-3xl font-bold text-yellow-300 leading-none">
</div> {String(index + 1).padStart(2, "0")}
</div>
</Link>
</div> </div>
))} ))}
<div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded"> <div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded">

View File

@ -71,35 +71,37 @@ export default function HeaderOpinion() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.map((article) => ( {articles.map((article) => (
<div key={article.id}> <div key={article.id}>
<Image <Link href={`/detail/${article.id}`}>
src={article.thumbnailUrl || "/default.jpg"} <Image
alt={article.title} src={article.thumbnailUrl || "/default.jpg"}
width={600} alt={article.title}
height={400} width={600}
className="w-full h-[250px] object-cover rounded-md" height={400}
/> className="w-full h-[250px] object-cover rounded-md"
<p className="text-xs font-semibold text-yellow-600 mt-4 uppercase"> />
{article.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 mt-4 uppercase">
article.categoryName || {article.categories?.[0]?.title ||
"BERITA"} article.categoryName ||
</p> "BERITA"}
<h3 className="text-lg font-bold mt-1">{article.title}</h3> </p>
<p className="text-sm text-gray-600 mt-2 line-clamp-3"> <h3 className="text-lg font-bold mt-1">{article.title}</h3>
{article.description} <p className="text-sm text-gray-600 mt-2 line-clamp-3">
</p> {article.description}
<p className="text-sm text-gray-700 mt-4"> </p>
By <strong>{article.createdByName}</strong> {" "} <p className="text-sm text-gray-700 mt-4">
{new Date(article.createdAt).toLocaleDateString("en-US", { By <strong>{article.createdByName}</strong> {" "}
month: "long", {new Date(article.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
<Link </p>
href={`/news/${article.id}`} <Link
className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline" href={`/detail/${article.id}`}
> className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline"
READ MORE &gt; >
READ MORE &gt;
</Link>
</Link> </Link>
</div> </div>
))} ))}
@ -125,34 +127,36 @@ export default function HeaderOpinion() {
<div className="space-y-6 sticky top-28 h-fit"> <div className="space-y-6 sticky top-28 h-fit">
<h4 className="text-xl font-bold">Recent Posts</h4> <h4 className="text-xl font-bold">Recent Posts</h4>
{recentPosts.map((post, index) => ( {recentPosts.map((post, index) => (
<div key={post.id} className="flex gap-3"> <div key={post.id}>
<Image <Link className="flex gap-3" href={`/detail/${post.id}`}>
src={post.thumbnailUrl || "/default.jpg"} <Image
alt={post.title} src={post.thumbnailUrl || "/default.jpg"}
width={80} alt={post.title}
height={80} width={80}
className="w-20 h-20 object-cover rounded-md" height={80}
/> className="w-20 h-20 object-cover rounded-md"
<div className="flex-1"> />
<p className="text-xs font-semibold text-yellow-600 uppercase"> <div className="flex-1">
{post.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 uppercase">
post.categoryName || {post.categories?.[0]?.title ||
"BERITA"} post.categoryName ||
</p> "BERITA"}
<h5 className="text-sm font-semibold leading-snug"> </p>
{post.title} <h5 className="text-sm font-semibold leading-snug">
</h5> {post.title}
<p className="text-xs text-gray-500 mt-1"> </h5>
{new Date(post.createdAt).toLocaleDateString("en-US", { <p className="text-xs text-gray-500 mt-1">
month: "long", {new Date(post.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
</div> </p>
<div className="text-3xl font-bold text-yellow-300 leading-none"> </div>
{String(index + 1).padStart(2, "0")} <div className="text-3xl font-bold text-yellow-300 leading-none">
</div> {String(index + 1).padStart(2, "0")}
</div>
</Link>
</div> </div>
))} ))}
<div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded"> <div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded">

View File

@ -71,35 +71,37 @@ export default function HeaderPeace() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.map((article) => ( {articles.map((article) => (
<div key={article.id}> <div key={article.id}>
<Image <Link href={`/detail/${article.id}`}>
src={article.thumbnailUrl || "/default.jpg"} <Image
alt={article.title} src={article.thumbnailUrl || "/default.jpg"}
width={600} alt={article.title}
height={400} width={600}
className="w-full h-[250px] object-cover rounded-md" height={400}
/> className="w-full h-[250px] object-cover rounded-md"
<p className="text-xs font-semibold text-yellow-600 mt-4 uppercase"> />
{article.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 mt-4 uppercase">
article.categoryName || {article.categories?.[0]?.title ||
"BERITA"} article.categoryName ||
</p> "BERITA"}
<h3 className="text-lg font-bold mt-1">{article.title}</h3> </p>
<p className="text-sm text-gray-600 mt-2 line-clamp-3"> <h3 className="text-lg font-bold mt-1">{article.title}</h3>
{article.description} <p className="text-sm text-gray-600 mt-2 line-clamp-3">
</p> {article.description}
<p className="text-sm text-gray-700 mt-4"> </p>
By <strong>{article.createdByName}</strong> {" "} <p className="text-sm text-gray-700 mt-4">
{new Date(article.createdAt).toLocaleDateString("en-US", { By <strong>{article.createdByName}</strong> {" "}
month: "long", {new Date(article.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
<Link </p>
href={`/news/${article.id}`} <Link
className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline" href={`/detail/${article.id}`}
> className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline"
READ MORE &gt; >
READ MORE &gt;
</Link>
</Link> </Link>
</div> </div>
))} ))}
@ -125,34 +127,36 @@ export default function HeaderPeace() {
<div className="space-y-6 sticky top-28 h-fit"> <div className="space-y-6 sticky top-28 h-fit">
<h4 className="text-xl font-bold">Recent Posts</h4> <h4 className="text-xl font-bold">Recent Posts</h4>
{recentPosts.map((post, index) => ( {recentPosts.map((post, index) => (
<div key={post.id} className="flex gap-3"> <div key={post.id}>
<Image <Link className="flex gap-3" href={`/detail/${post.id}`}>
src={post.thumbnailUrl || "/default.jpg"} <Image
alt={post.title} src={post.thumbnailUrl || "/default.jpg"}
width={80} alt={post.title}
height={80} width={80}
className="w-20 h-20 object-cover rounded-md" height={80}
/> className="w-20 h-20 object-cover rounded-md"
<div className="flex-1"> />
<p className="text-xs font-semibold text-yellow-600 uppercase"> <div className="flex-1">
{post.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 uppercase">
post.categoryName || {post.categories?.[0]?.title ||
"BERITA"} post.categoryName ||
</p> "BERITA"}
<h5 className="text-sm font-semibold leading-snug"> </p>
{post.title} <h5 className="text-sm font-semibold leading-snug">
</h5> {post.title}
<p className="text-xs text-gray-500 mt-1"> </h5>
{new Date(post.createdAt).toLocaleDateString("en-US", { <p className="text-xs text-gray-500 mt-1">
month: "long", {new Date(post.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
</div> </p>
<div className="text-3xl font-bold text-yellow-300 leading-none"> </div>
{String(index + 1).padStart(2, "0")} <div className="text-3xl font-bold text-yellow-300 leading-none">
</div> {String(index + 1).padStart(2, "0")}
</div>
</Link>
</div> </div>
))} ))}
<div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded"> <div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded">

View File

@ -71,35 +71,37 @@ export default function HeaderPopular() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{articles.map((article) => ( {articles.map((article) => (
<div key={article.id}> <div key={article.id}>
<Image <Link href={`/detail/${article.id}`}>
src={article.thumbnailUrl || "/default.jpg"} <Image
alt={article.title} src={article.thumbnailUrl || "/default.jpg"}
width={600} alt={article.title}
height={400} width={600}
className="w-full h-[250px] object-cover rounded-md" height={400}
/> className="w-full h-[250px] object-cover rounded-md"
<p className="text-xs font-semibold text-yellow-600 mt-4 uppercase"> />
{article.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 mt-4 uppercase">
article.categoryName || {article.categories?.[0]?.title ||
"BERITA"} article.categoryName ||
</p> "BERITA"}
<h3 className="text-lg font-bold mt-1">{article.title}</h3> </p>
<p className="text-sm text-gray-600 mt-2 line-clamp-3"> <h3 className="text-lg font-bold mt-1">{article.title}</h3>
{article.description} <p className="text-sm text-gray-600 mt-2 line-clamp-3">
</p> {article.description}
<p className="text-sm text-gray-700 mt-4"> </p>
By <strong>{article.createdByName}</strong> {" "} <p className="text-sm text-gray-700 mt-4">
{new Date(article.createdAt).toLocaleDateString("en-US", { By <strong>{article.createdByName}</strong> {" "}
month: "long", {new Date(article.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
<Link </p>
href={`/news/${article.id}`} <Link
className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline" href={`/detail/${article.id}`}
> className="mt-2 inline-block text-sm text-yellow-600 font-semibold hover:underline"
READ MORE &gt; >
READ MORE &gt;
</Link>
</Link> </Link>
</div> </div>
))} ))}
@ -125,34 +127,36 @@ export default function HeaderPopular() {
<div className="space-y-6 sticky top-28 h-fit"> <div className="space-y-6 sticky top-28 h-fit">
<h4 className="text-xl font-bold">Recent Posts</h4> <h4 className="text-xl font-bold">Recent Posts</h4>
{recentPosts.map((post, index) => ( {recentPosts.map((post, index) => (
<div key={post.id} className="flex gap-3"> <div key={post.id}>
<Image <Link className="flex gap-3" href={`/detail/${post.id}`}>
src={post.thumbnailUrl || "/default.jpg"} <Image
alt={post.title} src={post.thumbnailUrl || "/default.jpg"}
width={80} alt={post.title}
height={80} width={80}
className="w-20 h-20 object-cover rounded-md" height={80}
/> className="w-20 h-20 object-cover rounded-md"
<div className="flex-1"> />
<p className="text-xs font-semibold text-yellow-600 uppercase"> <div className="flex-1">
{post.categories?.[0]?.title || <p className="text-xs font-semibold text-yellow-600 uppercase">
post.categoryName || {post.categories?.[0]?.title ||
"BERITA"} post.categoryName ||
</p> "BERITA"}
<h5 className="text-sm font-semibold leading-snug"> </p>
{post.title} <h5 className="text-sm font-semibold leading-snug">
</h5> {post.title}
<p className="text-xs text-gray-500 mt-1"> </h5>
{new Date(post.createdAt).toLocaleDateString("en-US", { <p className="text-xs text-gray-500 mt-1">
month: "long", {new Date(post.createdAt).toLocaleDateString("en-US", {
day: "numeric", month: "long",
year: "numeric", day: "numeric",
})} year: "numeric",
</p> })}
</div> </p>
<div className="text-3xl font-bold text-yellow-300 leading-none"> </div>
{String(index + 1).padStart(2, "0")} <div className="text-3xl font-bold text-yellow-300 leading-none">
</div> {String(index + 1).padStart(2, "0")}
</div>
</Link>
</div> </div>
))} ))}
<div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded"> <div className="relative max-w-full h-[300px] overflow-hidden flex items-center mx-auto my-6 rounded">

View File

@ -67,7 +67,7 @@ export default function Beranda() {
{highlightArticles.map((item) => ( {highlightArticles.map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="relative rounded-xl overflow-hidden group" className="relative rounded-xl overflow-hidden group"
> >
<Image <Image
@ -102,7 +102,7 @@ export default function Beranda() {
{otherArticles.slice(1, 3).map((item) => ( {otherArticles.slice(1, 3).map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="flex gap-4 rounded-sm overflow-hidden transition" className="flex gap-4 rounded-sm overflow-hidden transition"
> >
<Image <Image

View File

@ -70,7 +70,7 @@ export default function Lifestyle() {
{articles.slice(0, visibleCount).map((item) => ( {articles.slice(0, visibleCount).map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="group block rounded-lg overflow-hidden" className="group block rounded-lg overflow-hidden"
> >
<Image <Image
@ -164,7 +164,7 @@ export default function Lifestyle() {
{/* {articles.slice(0, visibleCount).map((item) => ( {/* {articles.slice(0, visibleCount).map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="group block rounded-lg overflow-hidden" className="group block rounded-lg overflow-hidden"
> >
<Image <Image
@ -259,7 +259,7 @@ export default function Lifestyle() {
{articles.slice(0, visibleCount).map((item) => ( {articles.slice(0, visibleCount).map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="group block rounded-lg overflow-hidden" className="group block rounded-lg overflow-hidden"
> >
<Image <Image
@ -368,7 +368,7 @@ export default function Lifestyle() {
{articles.slice(0, visibleCount).map((item) => ( {articles.slice(0, visibleCount).map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="group block rounded-lg overflow-hidden" className="group block rounded-lg overflow-hidden"
> >
<Image <Image
@ -435,7 +435,7 @@ export default function Lifestyle() {
{articles.slice(0, visibleCount).map((item) => ( {articles.slice(0, visibleCount).map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="flex flex-col md:flex-row gap-5 group" className="flex flex-col md:flex-row gap-5 group"
> >
<Image <Image
@ -503,7 +503,7 @@ export default function Lifestyle() {
{articles.map((post, idx) => ( {articles.map((post, idx) => (
<Link <Link
key={post.id} key={post.id}
href={`/news/${post.id}`} href={`/detail/${post.id}`}
className="flex items-start gap-3 group" className="flex items-start gap-3 group"
> >
{/* Thumbnail */} {/* Thumbnail */}

View File

@ -74,7 +74,7 @@ export default function OnTheSpot() {
{articles.map((item) => ( {articles.map((item) => (
<Link <Link
key={item.id} key={item.id}
href={`/news/${item.id}`} href={`/detail/${item.id}`}
className="flex gap-4 rounded-sm overflow-hidden transition" className="flex gap-4 rounded-sm overflow-hidden transition"
> >
<Image <Image

View File

@ -173,11 +173,11 @@ export default function ArticleTable() {
initState(); initState();
}; };
const copyUrlArticle = async (id: number, slug: string) => { const copyUrlArticle = async (id: number) => {
const url = const url =
`${window.location.protocol}//${window.location.host}` + `${window.location.protocol}//${window.location.host}` +
"/news/detail/" + "/detail/" +
`${id}-${slug}`; `${id}`;
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
successToast("Success", "Article Copy to Clipboard"); successToast("Success", "Article Copy to Clipboard");
@ -228,9 +228,7 @@ export default function ArticleTable() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56"> <DropdownMenuContent className="w-56">
<DropdownMenuItem <DropdownMenuItem onClick={() => copyUrlArticle(article.id)}>
onClick={() => copyUrlArticle(article.id, article.slug)}
>
<CopyIcon className="mr-2 h-4 w-4" /> <CopyIcon className="mr-2 h-4 w-4" />
Copy Url Article Copy Url Article
</DropdownMenuItem> </DropdownMenuItem>

22
package-lock.json generated
View File

@ -24,6 +24,7 @@
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@tinymce/tinymce-react": "^6.3.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"apexcharts": "^5.3.4", "apexcharts": "^5.3.4",
"axios": "^1.10.0", "axios": "^1.10.0",
@ -2438,6 +2439,24 @@
"tailwindcss": "4.1.12" "tailwindcss": "4.1.12"
} }
}, },
"node_modules/@tinymce/tinymce-react": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz",
"integrity": "sha512-E++xnn0XzDzpKr40jno2Kj7umfAE6XfINZULEBBeNjTMvbACWzA6CjiR6V8eTDc9yVmdVhIPqVzV4PqD5TZ/4g==",
"dependencies": {
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
"tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.1"
},
"peerDependenciesMeta": {
"tinymce": {
"optional": true
}
}
},
"node_modules/@types/color-convert": { "node_modules/@types/color-convert": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz",
@ -2503,7 +2522,6 @@
"version": "19.1.12", "version": "19.1.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz",
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"dev": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -2512,7 +2530,7 @@
"version": "19.1.9", "version": "19.1.9",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
"dev": true, "devOptional": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
} }

View File

@ -24,6 +24,7 @@
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@tinymce/tinymce-react": "^6.3.0",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"apexcharts": "^5.3.4", "apexcharts": "^5.3.4",
"axios": "^1.10.0", "axios": "^1.10.0",