Compare commits

...

10 Commits

Author SHA1 Message Date
Anang Yusman d77a862893 fix yml 2026-02-24 01:24:43 +08:00
Anang Yusman ddcee39098 fix zod 2026-02-24 01:01:42 +08:00
Anang Yusman 69cf73ec62 update form,contact us landing, responsive detail audio 2026-02-23 15:47:35 +08:00
Anang Yusman a0bb9c858c install deps 2026-02-19 16:29:34 +08:00
Anang Yusman cf6e994cfb fix 2026-02-19 16:23:12 +08:00
Anang Yusman a0073c692e add ckeeditor 2026-02-19 16:18:35 +08:00
Anang Yusman 43f7c1ff21 update env 2026-02-19 15:56:27 +08:00
Anang Yusman 7bf3c5577a update yml 2026-02-19 15:55:56 +08:00
Anang Yusman 7137412a80 Add new file 2026-02-19 06:25:38 +00:00
Anang Yusman d055d84541 update qudo 2026-02-19 13:35:31 +08:00
10540 changed files with 1814533 additions and 510 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640

27
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,27 @@
build-dev:
stage: build
only:
- main
image: docker:25.0.3-cli
services:
- name: docker:25.0.3-dind
command: ["--insecure-registry=38.47.185.86:8900"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
script:
- docker info
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
- docker build -t 38.47.185.86:8900/medols/web-qudo:dev .
- docker push 38.47.185.86:8900/medols/web-qudo:dev
auto-deploy:
stage: deploy
when: on_success
only:
- main
image: curlimages/curl:latest
services:
- docker:dind
script:
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-qudo/build?token=autodeploymedols

33
Dockerfile Normal file
View File

@ -0,0 +1,33 @@
# Menggunakan image Node.js yang lebih ringan
FROM node:20-alpine
# Mengatur port
ENV PORT 3000
# Install pnpm secara global
RUN npm install -g pnpm
# Membuat direktori aplikasi dan mengatur sebagai working directory
WORKDIR /usr/src/app
# Menyalin file penting terlebih dahulu untuk caching
COPY package.json ./
# Menyalin env
COPY .env .env
# Install dependencies
RUN pnpm install
# RUN pnpm install --frozen-lockfile
# Menyalin source code aplikasi
COPY . .
# Build aplikasi
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
# Expose port untuk server
EXPOSE 3000
# Perintah untuk menjalankan aplikasi
CMD ["pnpm", "run", "start"]

View File

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

View File

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

View File

@ -5,12 +5,15 @@ import ContentLatest from "@/components/landing-page/content-latest";
import ContentPopular from "@/components/landing-page/content-popular";
import ContentCategory from "@/components/landing-page/category-content";
import FloatingMenuNews from "@/components/landing-page/floating-news";
import { Suspense } from "react";
export default function NewsAndServicesPage() {
return (
<div className="relative min-h-screen bg-white">
<FloatingMenuNews />
<NewsAndServicesHeader />
<Suspense fallback={null}>
<NewsAndServicesHeader />
</Suspense>
<ContentLatest />
<ContentPopular />
<ContentCategory />

View File

@ -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 */}
<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"
>
{playing ? (
<Pause size={28} className="text-black" />
) : (
<Play size={28} className="text-black ml-1" />
)}
</button>
<div className="flex justify-center md:justify-start">
<button
onClick={() => setPlaying(!playing)}
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={24} className="text-black" />
) : (
<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.

View File

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

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?: 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;

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">
<Image
src="/image/phone.png"
alt="App Preview"
width={320}
height={640}
className="object-contain"
/>
<div className="relative w-[320px] h-[640px]">
{/* PHONE IMAGE */}
<Image
src="/image/phone.png"
alt="App Preview"
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>

View File

@ -1,5 +1,4 @@
"use client";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { X, ChevronLeft, ChevronRight } from "lucide-react";

View File

@ -4,66 +4,82 @@ import Image from "next/image";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { Menu, X, Home, Box, Briefcase, Newspaper } from "lucide-react";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
export default function Header() {
const [open, setOpen] = useState(false);
const [contactOpen, setContactOpen] = useState(false);
return (
<header className="relative w-full bg-white overflow-hidden">
<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"
}`}
>
<div className="flex items-center justify-between border-b border-white/20 p-6">
<div className="flex rounded-full bg-white text-sm font-semibold text-[#966314]">
<button className="rounded-full bg-white px-3 py-1">ID</button>
<button className="px-3 py-1">EN</button>
<>
<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"
}`}
>
<div className="flex items-center justify-between border-b border-white/20 p-6">
<div className="flex rounded-full bg-white text-sm font-semibold text-[#966314]">
<button className="rounded-full bg-white px-3 py-1">ID</button>
<button className="px-3 py-1">EN</button>
</div>
<button onClick={() => setOpen(false)}>
<X />
</button>
</div>
<button onClick={() => setOpen(false)}>
<X />
</button>
<nav className="flex flex-col gap-6 p-6 text-sm font-medium">
<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"
/>
</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">
<span className="relative inline-block">
<span className="absolute bottom-1 left-0 h-3 w-full bg-[#966314]"></span>
<span className="relative">Beyond Expectations</span>
</span>
<br />
Build <span className="text-[#966314]">Reputation.</span>
</h1>
<Button
size="lg"
onClick={() => setContactOpen(true)}
className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
>
Contact Us
</Button>
</div>
<div className="relative hidden flex-1 justify-end md:flex">
<Image
src="/image/img1.png"
alt="Illustration"
width={520}
height={520}
className="object-contain"
/>
</div>
</div>
</header>
<nav className="flex flex-col gap-6 p-6 text-sm font-medium">
<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" />
</nav>
</aside>
<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">
<span className="relative inline-block">
<span className="absolute bottom-1 left-0 h-3 w-full bg-[#966314]"></span>
<span className="relative">Beyond Expectations</span>
</span>
<br />
Build <span className="text-[#966314]">Reputation.</span>
</h1>
<Button
size="lg"
className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
>
Contact Us
</Button>
</div>
<div className="relative hidden flex-1 justify-end md:flex">
<Image
src="/image/img1.png"
alt="Illustration"
width={520}
height={520}
className="object-contain"
/>
</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>
);
}

View File

@ -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;
@ -22,8 +30,8 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
onMouseEnter={() => setHovered(true)}
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"
isActive
? "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 }}
@ -40,27 +48,29 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
/>
)}
<motion.div
layout
<motion.div
layout
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div className={`text-lg transition-all duration-200 ${
isActive
? "text-white"
: "text-slate-500 group-hover:text-slate-700"
}`}>
<div
className={`text-lg transition-all duration-200 ${
isActive
? "text-white"
: "text-slate-500 group-hover:text-slate-700"
}`}
>
<Icon />
</div>
</motion.div>
{open && (
<motion.span
layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
<motion.span
layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className={`text-sm font-medium transition-colors duration-200 ${
isActive ? "text-white" : "text-slate-700"
}`}
@ -88,14 +98,12 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
{/* Notification badge */}
{notifs && open && (
<motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
<motion.span
initial={{ scale: 0, opacity: 0 }}
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}

View File

@ -383,9 +383,6 @@ const SidebarContent = ({
);
}
// =============================
// NORMAL ITEM
// =============================
return (
<Link href={item.link!} key={item.title}>
<Option

View File

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

View File

@ -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>
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
<Plus className="w-4 h-4 mr-2" />
Create New Article
</Button>
<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 ================= */}

View File

@ -1,7 +1,20 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: [
"mikulnews.com",
"dev.mikulnews.com",
"jaecoocihampelasbdg.com",
"dev.arahnegeri.com",
"qudo.id",
],
},
eslint: {
ignoreDuringBuilds: true,
},
experimental: {
optimizePackageImports: ["@ckeditor/ckeditor5-react", "react-apexcharts"],
},
};
export default nextConfig;

4885
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,24 +9,49 @@
"lint": "eslint"
},
"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-accordion": "^1.2.11",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@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",
"react": "19.2.3",
"react-apexcharts": "^2.0.1",
"react-datepicker": "^9.1.0",
"react-day-picker": "^9.13.2",
"react-dom": "19.2.3",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.71.2",
"react-select": "^5.10.2",
"sweetalert2": "^11.26.18",
"sweetalert2-react-content": "^5.1.1",
"tailwind-merge": "^3.4.0"
"tailwind-merge": "^3.4.0",
"zod": "^3.25.67"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

19
service/activity-log.ts Normal file
View File

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

49
service/advertisement.ts Normal file
View File

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

189
service/article.ts Normal file
View File

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

264
service/generate-article.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

50
service/magazine.tsx Normal file
View File

@ -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}`);
}

View File

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

View File

@ -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}`);
}

135
service/master-user.ts Normal file
View File

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

View File

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

View File

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

View File

@ -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();
}

70
utils/dynamic-import.ts Normal file
View File

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

174
utils/global.tsx Normal file
View File

@ -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 &nbsp,&ndash
.replaceAll("&nbsp;", "")
.replaceAll("&ndash;", "-")
// Replace quotation mark
.replaceAll("&ldquo;", '"')
.replaceAll("&rdquo;", '"')
// 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}`;
}

49
vendor/ckeditor5/LICENSE.md vendored Normal file
View File

@ -0,0 +1,49 @@
Software License Agreement
==========================
Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
Online builder code samples are licensed under the terms of the MIT License (see Appendix A):
http://en.wikipedia.org/wiki/MIT_License
CKEditor 5 collaboration features are only available under a commercial license. [Contact us](https://ckeditor.com/contact/) for more details.
Free 30-days trials of CKEditor 5 collaboration features are available:
* https://ckeditor.com/collaboration/ - Real-time collaboration (with all features).
* https://ckeditor.com/collaboration/comments/ - Inline comments feature (without real-time collaborative editing).
* https://ckeditor.com/collaboration/track-changes/ - Track changes feature (without real-time collaborative editing).
Trademarks
----------
CKEditor is a trademark of CKSource Holding sp. z o.o. All other brand
and product names are trademarks, registered trademarks or service
marks of their respective holders.
---
Appendix A: The MIT License
---------------------------
The MIT License (MIT)
Copyright (c) 2014-2024, CKSource Holding sp. z o.o.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

60
vendor/ckeditor5/README.md vendored Normal file
View File

@ -0,0 +1,60 @@
# CKEditor 5 editor generated with the online builder
This repository presents a CKEditor 5 editor build generated by the [Online builder tool](https://ckeditor.com/ckeditor-5/online-builder)
## Quick start
1. Open the `sample/index.html` page in the browser.
2. Fill the prompt with the license key. If you do not have the license key yet [contact us](https://ckeditor.com/contact/).
## Configuring build
Changes like changing toolbar items, changing order of icons or customizing plugin configurations should be relatively easy to make. Open the `sample/index.html` file and edit the script that initialized the CKEditor 5. Save the file and refresh the browser. That's all.
*Note:* If you have any problems with browser caching use the `Ctrl + R` or `Cmd + R` shortcut depending on your system.
However if you want to remove or add a plugin to the build you need to follow the next step of this guide.
Note that it is also possible to go back to the [Online builder tool](https://ckeditor.com/ckeditor-5/online-builder) and pick other set of plugins. But we encourage you to try the harder way and to learn the principles of Node.js and CKEditor 5 ecosystems that will allow you to do more cool things in the future!
### Installation
In order to rebuild the application you need to install all dependencies first. To do it, open the terminal in the project directory and type:
```
npm install
```
Make sure that you have the `node` and `npm` installed first. If not, then follow the instructions on the [Node.js documentation page](https://nodejs.org/en/).
### Adding or removing plugins
Now you can install additional plugin in the build. Just follow the [Adding a plugin to an editor tutorial](https://ckeditor.com/docs/ckeditor5/latest/installation/plugins/installing-plugins.html#adding-a-plugin-to-an-editor)
### Rebuilding editor
If you have already done the [Installation](#installation) and [Adding or removing plugins](#adding-or-removing-plugins) steps, you're ready to rebuild the editor by running the following command:
```
npm run build
```
This will build the CKEditor 5 to the `build` directory. You can open your browser and you should be able to see the changes you've made in the code. If not, then try to refresh also the browser cache by typing `Ctrl + R` or `Cmd + R` depending on your system.
## What's next?
Follow the guides available on https://ckeditor.com/docs/ckeditor5/latest/framework/index.html and enjoy the document editing.
## FAQ
| Where is the place to report bugs and feature requests?
You can create an issue on https://github.com/ckeditor/ckeditor5/issues including the build id - `yt1k3s5kie6c-kuy63vghjub2`. Make sure that the question / problem is unique, please look for a possibly asked questions in the search box. Duplicates will be closed.
| Where can I learn more about the CKEditor 5 framework?
Here: https://ckeditor.com/docs/ckeditor5/latest/framework/
| Is it possible to use online builder with common frameworks like React, Vue or Angular?
Not yet, but it these integrations will be available at some point in the future.

68
vendor/ckeditor5/build/ckeditor.d.ts vendored Normal file
View File

@ -0,0 +1,68 @@
/**
* @license Copyright (c) 2014-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
import { Alignment } from "@ckeditor/ckeditor5-alignment";
import { Autoformat } from "@ckeditor/ckeditor5-autoformat";
import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles";
import { BlockQuote } from "@ckeditor/ckeditor5-block-quote";
import { CloudServices } from "@ckeditor/ckeditor5-cloud-services";
import { CodeBlock } from "@ckeditor/ckeditor5-code-block";
import type { EditorConfig } from "@ckeditor/ckeditor5-core";
import { Essentials } from "@ckeditor/ckeditor5-essentials";
import { FontSize } from "@ckeditor/ckeditor5-font";
import { Heading } from "@ckeditor/ckeditor5-heading";
import {
Image,
ImageCaption,
ImageInsert,
ImageStyle,
ImageToolbar,
ImageUpload,
} from "@ckeditor/ckeditor5-image";
import { Indent } from "@ckeditor/ckeditor5-indent";
import { Link } from "@ckeditor/ckeditor5-link";
import { List } from "@ckeditor/ckeditor5-list";
import { MediaEmbed } from "@ckeditor/ckeditor5-media-embed";
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
import { PasteFromOffice } from "@ckeditor/ckeditor5-paste-from-office";
import { SourceEditing } from "@ckeditor/ckeditor5-source-editing";
import { Table, TableToolbar } from "@ckeditor/ckeditor5-table";
import { TextTransformation } from "@ckeditor/ckeditor5-typing";
import { Undo } from "@ckeditor/ckeditor5-undo";
import { SimpleUploadAdapter } from "@ckeditor/ckeditor5-upload";
declare class Editor extends ClassicEditor {
static builtinPlugins: (
| typeof Alignment
| typeof Autoformat
| typeof BlockQuote
| typeof Bold
| typeof CloudServices
| typeof CodeBlock
| typeof Essentials
| typeof FontSize
| typeof Heading
| typeof Image
| typeof ImageCaption
| typeof ImageInsert
| typeof ImageStyle
| typeof ImageToolbar
| typeof ImageUpload
| typeof Indent
| typeof Italic
| typeof Link
| typeof List
| typeof MediaEmbed
| typeof Paragraph
| typeof PasteFromOffice
| typeof SimpleUploadAdapter
| typeof SourceEditing
| typeof Table
| typeof TableToolbar
| typeof TextTransformation
| typeof Undo
)[];
static defaultConfig: EditorConfig;
}
export default Editor;

6
vendor/ckeditor5/build/ckeditor.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"ckeditor.js","mappings":";;;;AAAA","sources":["webpack://ClassicEditor/webpack/universalModuleDefinition"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClassicEditor\"] = factory();\n\telse\n\t\troot[\"ClassicEditor\"] = factory();\n})(self, () => {\nreturn "],"names":[],"sourceRoot":""}

View File

@ -0,0 +1 @@
(function(e){const t=e["af"]=e["af"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"%0 van %1",Accept:"",Accessibility:"","Accessibility help":"","Align center":"Belyn in die middel","Align left":"Belyn links","Align right":"Belyn regs",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Black:"","Block quote":"Verwysingsaanhaling",Blue:"",Bold:"Vet","Bold text":"",Cancel:"Kanselleer",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"Bronkode","Code block":"","Content editing keystrokes":"","Dim grey":"","Drag to move":"","Dropdown toolbar":"","Edit block":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"",Green:"",Grey:"","Help Contents. To close this dialog press ESC.":"",HEX:"","Insert code block":"Voeg bronkodeblok in",Italic:"Kursief","Italic text":"",Justify:"Belyn beide kante","Light blue":"","Light green":"","Light grey":"",MENU_BAR_MENU_EDIT:"Wysig",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Open the accessibility help dialog":"",Orange:"","Plain text":"Gewone skrif","Press %0 for help.":"",Previous:"",Purple:"",Red:"","Remove color":"Verwyder kleur","Restore default":"Herstel verstek","Rich Text Editor":"",Save:"Stoor","Show more items":"Wys meer items",Strikethrough:"Deurstreep","Strikethrough text":"",Subscript:"Onderskrif",Superscript:"Boskrif","Text alignment":"Teksbelyning","Text alignment toolbar":"Teksbelyning nutsbank","These keyboard shortcuts allow for quick access to content editing features.":"","Toggle caption off":"","Toggle caption on":"",Turquoise:"",Underline:"Onderstreep","Underline text":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"",Yellow:""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["ast"]=e["ast"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"",Accept:"",Accessibility:"","Accessibility help":"",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Black:"",Blue:"",Bold:"Negrina","Bold text":"","Break text":"","Bulleted List":"Llista con viñetes","Bulleted list styles toolbar":"",Cancel:"Encaboxar","Caption for image: %0":"","Caption for the image":"","Centered image":"","Change image text alternative":"",Circle:"",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"","Content editing keystrokes":"","Create link":"",Decimal:"","Decimal with leading zero":"","Decrease list item indent":"","Dim grey":"",Disc:"",Downloadable:"","Drag to move":"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Enter image caption":"","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"","Full size image":"Imaxen a tamañu completu",Green:"",Grey:"","Help Contents. To close this dialog press ESC.":"",HEX:"","Image from computer":"","Image resize list":"","Image toolbar":"","image widget":"complementu d'imaxen","In line":"","Increase list item indent":"",Insert:"","Insert image":"","Insert image via URL":"","Invalid start index value.":"",Italic:"Cursiva","Italic text":"","Keystrokes that can be used in a list":"","Left aligned image":"","Light blue":"","Light green":"","Light grey":"",Link:"Enllazar","Link image":"","Link URL":"URL del enllaz","List properties":"","Lower-latin":"","Lowerroman":"",MENU_BAR_MENU_EDIT:"",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of a link":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Numbered List":"Llista numberada","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"","Open the accessibility help dialog":"",Orange:"",Original:"","Press %0 for help.":"",Previous:"",Purple:"",Red:"",Redo:"Refacer","Remove color":"","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"","Reversed order":"","Rich Text Editor":"Editor de testu arriquecíu","Right aligned image":"",Save:"Guardar","Show more items":"","Side image":"Imaxen llateral",Square:"","Start at":"","Start index must be greater than 0.":"",Strikethrough:"","Strikethrough text":"",Subscript:"",Superscript:"","Text alternative":"","These keyboard shortcuts allow for quick access to content editing features.":"","This link has no URL":"","To-do List":"","Toggle caption off":"","Toggle caption on":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lowerlatin list style":"","Toggle the lowerroman list style":"","Toggle the square list style":"","Toggle the upperlatin list style":"","Toggle the upperroman list style":"",Turquoise:"",Underline:"","Underline text":"",Undo:"Desfacer",Unlink:"Desenllazar",Update:"","Update image URL":"","Upload failed":"","Upload from computer":"","Upload image from computer":"","Upper-latin":"","Upper-roman":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"","Wrap text":"",Yellow:""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["bs"]=e["bs"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"%0 od %1",Accept:"",Accessibility:"","Accessibility help":"","Align center":"Centrirati","Align left":"Lijevo poravnanje","Align right":"Desno poravnanje",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Big:"",Black:"","Block quote":"Citat",Blue:"",Bold:"Podebljano","Bold text":"","Break text":"",Cancel:"Poništi","Caption for image: %0":"","Caption for the image":"","Centered image":"Centrirana slika","Change image text alternative":"Promijeni ALT atribut za sliku","Choose heading":"Odaberi naslov",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"Kod","Code block":"","Content editing keystrokes":"",Default:"Zadani","Dim grey":"","Document colors":"","Drag to move":"","Dropdown toolbar":"","Edit block":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Enter image caption":"Unesi naziv slike","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"","Font Background Color":"Boja pozadine","Font Color":"Boja","Font Family":"Font","Font Size":"Veličina fonta","Full size image":"",Green:"",Grey:"",Heading:"Naslov","Heading 1":"Naslov 1","Heading 2":"Naslov 2","Heading 3":"Naslov 3","Heading 4":"Naslov 4","Heading 5":"Naslov 5","Heading 6":"Naslov 6","Help Contents. To close this dialog press ESC.":"",HEX:"",Huge:"","Image from computer":"","Image resize list":"Lista veličina slike","Image toolbar":"","image widget":"","In line":"",Insert:"Umetni","Insert code block":"Umetni kod blok","Insert image":"Umetni sliku","Insert image via URL":"Umetni sliku preko URLa",Italic:"Zakrivljeno","Italic text":"",Justify:"","Left aligned image":"Lijevo poravnata slika","Light blue":"","Light green":"","Light grey":"",MENU_BAR_MENU_EDIT:"Uredi",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"Umetni",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Open the accessibility help dialog":"",Orange:"",Original:"Original",Paragraph:"Paragraf","Plain text":"Tekst","Press %0 for help.":"",Previous:"",Purple:"",Red:"","Remove color":"Ukloni boju","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"Promijeni veličinu slike","Resize image to %0":"","Resize image to the original size":"Postavi originalnu veličinu slike","Restore default":"Vrati na zadano","Rich Text Editor":"","Right aligned image":"Desno poravnata slika",Save:"Sačuvaj","Show more items":"Prikaži više stavki","Side image":"",Small:"",Strikethrough:"Precrtano","Strikethrough text":"",Subscript:"",Superscript:"","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje teksta","Text alternative":"ALT atribut","These keyboard shortcuts allow for quick access to content editing features.":"",Tiny:"","Toggle caption off":"","Toggle caption on":"",Turquoise:"","Type or paste your content here.":"Unesite ili zalijepite vaš sadržaj ovdje","Type your title":"Unesite naslov",Underline:"Podcrtano","Underline text":"",Update:"Ažuriraj","Update image URL":"Ažuriraj URL slike","Upload failed":"Učitavanje slike nije uspjelo","Upload from computer":"","Upload image from computer":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"","Wrap text":"Prelomi tekst",Yellow:""});t.getPluralForm=function(e){return e%10==1&&e%100!=11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["eo"]=e["eo"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"",Accept:"",Accessibility:"","Accessibility help":"",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Black:"",Blue:"",Bold:"grasa","Bold text":"","Break text":"","Bulleted List":"Bula Listo","Bulleted list styles toolbar":"",Cancel:"Nuligi","Caption for image: %0":"","Caption for the image":"","Centered image":"","Change image text alternative":"Ŝanĝu la alternativan tekston de la bildo","Choose heading":"Elektu ĉapon",Circle:"",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"","Content editing keystrokes":"","Create link":"",Decimal:"","Decimal with leading zero":"","Decrease list item indent":"","Dim grey":"",Disc:"",Downloadable:"","Drag to move":"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Enter image caption":"Skribu klarigon pri la bildo","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"","Full size image":"Bildo kun reala dimensio",Green:"",Grey:"",Heading:"Ĉapo","Heading 1":"Ĉapo 1","Heading 2":"Ĉapo 2","Heading 3":"Ĉapo 3","Heading 4":"","Heading 5":"","Heading 6":"","Help Contents. To close this dialog press ESC.":"",HEX:"","Image from computer":"","Image resize list":"","Image toolbar":"","image widget":"bilda fenestraĵo","In line":"","Increase list item indent":"",Insert:"","Insert image":"Enmetu bildon","Insert image via URL":"","Invalid start index value.":"",Italic:"kursiva","Italic text":"","Keystrokes that can be used in a list":"","Left aligned image":"","Light blue":"","Light green":"","Light grey":"",Link:"Ligilo","Link image":"","Link URL":"URL de la ligilo","List properties":"","Lower-latin":"","Lowerroman":"",MENU_BAR_MENU_EDIT:"",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of a link":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Numbered List":"Numerita Listo","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"","Open the accessibility help dialog":"",Orange:"",Original:"",Paragraph:"Paragrafo","Press %0 for help.":"",Previous:"",Purple:"",Red:"",Redo:"Refari","Remove color":"","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"","Reversed order":"","Rich Text Editor":"Redaktilo de Riĉa Teksto","Right aligned image":"",Save:"Konservi","Show more items":"","Side image":"Flanka biildo",Square:"","Start at":"","Start index must be greater than 0.":"",Strikethrough:"","Strikethrough text":"",Subscript:"",Superscript:"","Text alternative":"Alternativa teksto","These keyboard shortcuts allow for quick access to content editing features.":"","This link has no URL":"","To-do List":"","Toggle caption off":"","Toggle caption on":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lowerlatin list style":"","Toggle the lowerroman list style":"","Toggle the square list style":"","Toggle the upperlatin list style":"","Toggle the upperroman list style":"",Turquoise:"","Type or paste your content here.":"","Type your title":"",Underline:"","Underline text":"",Undo:"Malfari",Unlink:"Malligi",Update:"","Update image URL":"","Upload failed":"","Upload from computer":"","Upload image from computer":"","Upper-latin":"","Upper-roman":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"","Wrap text":"",Yellow:""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
(function(e){const t=e["es-co"]=e["es-co"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"%0 de %1",Accept:"",Accessibility:"","Accessibility help":"","Align center":"Centrar","Align left":"Alinear a la izquierda","Align right":"Alinear a la derecha",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Big:"Grande",Black:"","Block quote":"Cita de bloque",Blue:"",Bold:"Negrita","Bold text":"","Break text":"",Cancel:"Cancelar","Caption for image: %0":"","Caption for the image":"","Centered image":"","Change image text alternative":"",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"Código","Code block":"","Content editing keystrokes":"","Copy selected content":"Copiar contenido seleccionado",Default:"Por defecto","Dim grey":"","Document colors":"Colores del documento","Drag to move":"","Dropdown toolbar":"","Edit block":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Enter image caption":"","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"","Font Background Color":"Color de fondo de fuente","Font Color":"Color de fuente","Font Family":"Familia de fuente","Font Size":"Tamaño de fuente","Full size image":"",Green:"",Grey:"","Help Contents. To close this dialog press ESC.":"",HEX:"",Huge:"Enorme","Image from computer":"","Image resize list":"","Image toolbar":"","image widget":"","In line":"",Insert:"Insertar","Insert code block":"Insertar bloque de código","Insert image":"","Insert image via URL":"",Italic:"Cursiva","Italic text":"Texto en cursiva",Justify:"Justificar","Left aligned image":"","Light blue":"","Light green":"","Light grey":"",MENU_BAR_MENU_EDIT:"Editar",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"Insertar",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Open the accessibility help dialog":"",Orange:"",Original:"","Paste content":"Pegar contenido","Paste content as plain text":"Pegar contenido como texto plano","Plain text":"Texto plano","Press %0 for help.":"",Previous:"",Purple:"",Red:"","Remove color":"Quitar color","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"Restaurar valores predeterminados","Revert autoformatting action":"Revertir la acción de formato automático","Rich Text Editor":"","Right aligned image":"",Save:"Guardar","Show more items":"Mostrar más elementos","Side image":"",Small:"Pequeña",Strikethrough:"Tachado","Strikethrough text":"",Subscript:"Subíndice",Superscript:"Superíndice","Text alignment":"Alineación de texto","Text alignment toolbar":"Herramientas de alineación de texto","Text alternative":"","These keyboard shortcuts allow for quick access to content editing features.":"",Tiny:"Diminuta","Toggle caption off":"","Toggle caption on":"",Turquoise:"",Underline:"Subrayado","Underline text":"",Update:"","Update image URL":"","Upload failed":"","Upload from computer":"","Upload image from computer":"","Upload in progress":"Carga en progreso","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"","Wrap text":"",Yellow:""});t.getPluralForm=function(e){return e==1?0:e!=0&&e%1e6==0?1:2}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["eu"]=e["eu"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"",Accept:"",Accessibility:"","Accessibility help":"",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Black:"","Block quote":"Aipua",Blue:"",Bold:"Lodia","Bold text":"","Break text":"","Bulleted List":"Buletdun zerrenda","Bulleted list styles toolbar":"",Cancel:"Utzi","Caption for image: %0":"","Caption for the image":"","Centered image":"Zentratutako irudia","Change image text alternative":"Aldatu irudiaren ordezko testua","Choose heading":"Aukeratu izenburua",Circle:"",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"Kodea","Content editing keystrokes":"","Create link":"",Decimal:"","Decimal with leading zero":"","Decrease list item indent":"","Dim grey":"",Disc:"",Downloadable:"","Drag to move":"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Enter image caption":"Sartu irudiaren epigrafea","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"","Full size image":"Tamaina osoko irudia",Green:"",Grey:"",Heading:"Izenburua","Heading 1":"Izenburua 1","Heading 2":"Izenburua 2","Heading 3":"Izenburua 3","Heading 4":"","Heading 5":"","Heading 6":"","Help Contents. To close this dialog press ESC.":"",HEX:"","Image from computer":"","Image resize list":"","Image toolbar":"","image widget":"irudi widgeta","In line":"","Increase list item indent":"",Insert:"","Insert image":"Txertatu irudia","Insert image via URL":"","Invalid start index value.":"",Italic:"Etzana","Italic text":"","Keystrokes that can be used in a list":"","Left aligned image":"Ezkerrean lerrokatutako irudia","Light blue":"","Light green":"","Light grey":"",Link:"Esteka","Link image":"","Link URL":"Estekaren URLa","List properties":"","Lower-latin":"","Lowerroman":"",MENU_BAR_MENU_EDIT:"",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of a link":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Numbered List":"Zenbakidun zerrenda","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"","Open the accessibility help dialog":"",Orange:"",Original:"",Paragraph:"Paragrafoa","Press %0 for help.":"",Previous:"",Purple:"",Red:"",Redo:"Berregin","Remove color":"","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"","Reversed order":"","Rich Text Editor":"Testu aberastuaren editorea","Right aligned image":"Eskuinean lerrokatutako irudia",Save:"Gorde","Show more items":"","Side image":"Alboko irudia",Square:"","Start at":"","Start index must be greater than 0.":"",Strikethrough:"","Strikethrough text":"",Subscript:"",Superscript:"","Text alternative":"Ordezko testua","These keyboard shortcuts allow for quick access to content editing features.":"","This link has no URL":"","To-do List":"","Toggle caption off":"","Toggle caption on":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lowerlatin list style":"","Toggle the lowerroman list style":"","Toggle the square list style":"","Toggle the upperlatin list style":"","Toggle the upperroman list style":"",Turquoise:"","Type or paste your content here.":"","Type your title":"",Underline:"Azpimarra","Underline text":"",Undo:"Desegin",Unlink:"Desestekatu",Update:"","Update image URL":"","Upload failed":"Kargatzeak huts egin du","Upload from computer":"","Upload image from computer":"","Upper-latin":"","Upper-roman":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"","Wrap text":"",Yellow:""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["gu"]=e["gu"]||{};t.dictionary=Object.assign(t.dictionary||{},{"%0 of %1":"",Accept:"","Block quote":" વિચાર ટાંકો",Bold:"ઘાટુ - બોલ્ડ્","Bold text":"",Cancel:"",Clear:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"","Content editing keystrokes":"","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"",Italic:"ત્રાંસુ - ઇટલિક્","Italic text":"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"","Open the accessibility help dialog":"","Remove color":"","Restore default":"",Save:"","Show more items":"",Strikethrough:"","Strikethrough text":"",Subscript:"",Superscript:"","These keyboard shortcuts allow for quick access to content editing features.":"","Toggle caption off":"","Toggle caption on":"",Underline:"નીચે લિટી - અન્ડરલાઇન્","Underline text":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["hy"]=e["hy"]||{};t.dictionary=Object.assign(t.dictionary||{},{"%0 of %1":"",Accept:"","Align cell text to the bottom":"","Align cell text to the center":"","Align cell text to the left":"","Align cell text to the middle":"","Align cell text to the right":"","Align cell text to the top":"","Align table to the left":"","Align table to the right":"",Alignment:"",Background:"",Bold:"Թավագիր","Bold text":"",Border:"",Cancel:"Չեղարկել","Cell properties":"","Center table":"","Choose heading":"",Clear:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"Կոդ",Color:"","Color picker":"",Column:"Սյունակ","Content editing keystrokes":"","Create link":"",Dashed:"","Delete column":"","Delete row":"",Dimensions:"",Dotted:"",Double:"",Downloadable:"","Edit link":"Խմբագրել հղումը","Enter table caption":"","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"",Groove:"","Header column":"","Header row":"",Heading:"","Heading 1":"Վերնագիր 1","Heading 2":"Վերնագիր 2","Heading 3":"Վերնագիր 3","Heading 4":"","Heading 5":"","Heading 6":"",Height:"","Horizontal text alignment toolbar":"","Insert a new table row (when in the last cell of a table)":"","Insert column left":"","Insert column right":"","Insert row above":"","Insert row below":"","Insert table":"",Inset:"",Italic:"Շեղագիր","Italic text":"","Justify cell text":"","Keystrokes that can be used in a table cell":"",Link:"Հղում","Link image":"","Link URL":"","Merge cell down":"","Merge cell left":"","Merge cell right":"","Merge cell up":"","Merge cells":"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of a link":"","Move out of an inline code style":"","Move the selection to the next cell":"","Move the selection to the previous cell":"","Navigate through the table":"","Navigate through the toolbar or menu bar":"",None:"","Open in a new tab":"","Open link in new tab":"","Open the accessibility help dialog":"",Outset:"",Padding:"",Paragraph:"","Remove color":"","Restore default":"",Ridge:"",Row:"",Save:"","Select column":"","Select row":"","Show more items":"",Solid:"","Split cell horizontally":"","Split cell vertically":"",Strikethrough:"Գծանշել","Strikethrough text":"",Style:"",Subscript:"Ենթատեքստ",Superscript:"Գերագիր",Table:"","Table alignment toolbar":"","Table cell text alignment":"","Table properties":"","Table toolbar":"",'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".':"",'The value is invalid. Try "10px" or "2em" or simply "2".':"","These keyboard shortcuts allow for quick access to content editing features.":"","This link has no URL":"","Toggle caption off":"","Toggle caption on":"","Type or paste your content here.":"","Type your title":"",Underline:"Ընդգծել","Underline text":"",Unlink:"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"","Vertical text alignment toolbar":"",Width:""});t.getPluralForm=function(e){return e!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
(function(e){const t=e["jv"]=e["jv"]||{};t.dictionary=Object.assign(t.dictionary||{},{"(may require <kbd>Fn</kbd>)":"","%0 of %1":"%0 saking %1",Accept:"",Accessibility:"","Accessibility help":"","Align center":"Rata tengah","Align left":"Rata kiwa","Align right":"Rata tengen",Aquamarine:"","Below, you can find a list of keyboard shortcuts that can be used in the editor.":"",Big:"Ageng",Black:"",Blue:"",Bold:"Kandhel","Bold text":"","Break text":"","Bulleted List":"","Bulleted list styles toolbar":"",Cancel:"Batal","Caption for image: %0":"","Caption for the image":"","Centered image":"Gambar ing tengah","Change image text alternative":"","Choose heading":"",Circle:"Bunder",Clear:"","Click to edit block":"",Close:"","Close contextual balloons, dropdowns, and dialogs":"",Code:"Kode","Code block":"","Content editing keystrokes":"",Decimal:"","Decimal with leading zero":"","Decrease list item indent":"",Default:"Default","Dim grey":"",Disc:"Kaset","Document colors":"Warni dokumen","Drag to move":"","Dropdown toolbar":"","Edit block":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor dialog":"","Editor editing area: %0":"","Editor menu bar":"","Editor toolbar":"","Enter image caption":"","Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.":"","Font Background Color":"Warni Latar Aksara","Font Color":"Warni aksara","Font Family":"Jinising Aksara","Font Size":"Ukuran aksara","Full size image":"Gambar ukuran kebak",Green:"",Grey:"",Heading:"","Heading 1":"","Heading 2":"","Heading 3":"","Heading 4":"","Heading 5":"","Heading 6":"","Help Contents. To close this dialog press ESC.":"",HEX:"",Huge:"Langkung ageng","Image from computer":"","Image resize list":"","Image toolbar":"","image widget":"","In line":"","Increase list item indent":"",Insert:"Tambah","Insert code block":"","Insert image":"Tambahaken gambar","Insert image via URL":"Tambah gambar saking URL","Invalid start index value.":"",Italic:"Miring","Italic text":"",Justify:"Rata kiwa tengen","Keystrokes that can be used in a list":"","Left aligned image":"Gambar ing kiwa","Light blue":"","Light green":"","Light grey":"","List properties":"","Lower-latin":"","Lowerroman":"",MENU_BAR_MENU_EDIT:"Ebah",MENU_BAR_MENU_FILE:"",MENU_BAR_MENU_FONT:"",MENU_BAR_MENU_FORMAT:"",MENU_BAR_MENU_HELP:"",MENU_BAR_MENU_INSERT:"Tambah",MENU_BAR_MENU_TEXT:"",MENU_BAR_MENU_TOOLS:"",MENU_BAR_MENU_VIEW:"","Move focus between form fields (inputs, buttons, etc.)":"","Move focus in and out of an active dialog window":"","Move focus to the menu bar, navigate between menu bars":"","Move focus to the toolbar, navigate between toolbars":"","Move out of an inline code style":"","Navigate through the toolbar or menu bar":"",Next:"","No results found":"","No searchable items":"","Numbered List":"","Numbered list styles toolbar":"","Open the accessibility help dialog":"",Orange:"",Original:"Asli",Paragraph:"","Plain text":"Seratan biasa","Press %0 for help.":"",Previous:"",Purple:"",Red:"","Remove color":"Busek warni","Replace from computer":"","Replace image":"","Replace image from computer":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"Mangsulaken default","Reversed order":"Dipunwangsul","Rich Text Editor":"","Right aligned image":"Gambar ing tengen",Save:"Rimat","Show more items":"Tampilaken langkung kathah","Side image":"",Small:"Alit",Square:"Kotak","Start at":"Wiwit saking","Start index must be greater than 0.":"",Strikethrough:"Seratan dicoret","Strikethrough text":"",Subscript:"",Superscript:"","Text alignment":"Perataan seratan","Text alignment toolbar":"","Text alternative":"","These keyboard shortcuts allow for quick access to content editing features.":"",Tiny:"Langkung alit","To-do List":"","Toggle caption off":"","Toggle caption on":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lowerlatin list style":"","Toggle the lowerroman list style":"","Toggle the square list style":"","Toggle the upperlatin list style":"","Toggle the upperroman list style":"",Turquoise:"","Type or paste your content here.":"Serataken utawi nyukani babagan ing ngriki","Type your title":"Serataken irah-irahan",Underline:"Garis ngandhap","Underline text":"",Update:"","Update image URL":"","Upload failed":"","Upload from computer":"","Upload image from computer":"","Upper-latin":"","Upper-roman":"","Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.":"","User interface and content navigation keystrokes":"",White:"","Wrap text":"",Yellow:""});t.getPluralForm=function(e){return 0}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

View File

@ -0,0 +1 @@
(function(n){const t=n["kk"]=n["kk"]||{};t.dictionary=Object.assign(t.dictionary||{},{"Align center":"Ортадан туралау","Align left":"Солға туралау","Align right":"Оңға туралау",Justify:"","Text alignment":"Мәтінді туралау","Text alignment toolbar":"Мәтінді туралау құралдар тақтасы"});t.getPluralForm=function(n){return n!=1}})(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More