From 902f8de516d1f298da4d5b0660e23a56e954e258 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Mon, 1 Sep 2025 02:06:19 +0700 Subject: [PATCH] feat: update ckeditor --- .../account-list/component/table.tsx | 260 +++++++++++++----- app/[locale]/globals.css | 98 +++++++ app/[locale]/layout.tsx | 1 + components/editor/custom-editor.js | 140 ++++++++-- components/editor/view-editor.js | 111 +++++++- components/form/content/image-detail-form.tsx | 4 +- .../content/task-ta/audio-detail-form.tsx | 4 +- .../content/task-ta/image-detail-form.tsx | 4 +- .../form/content/task-ta/teks-detail-form.tsx | 4 +- .../content/task-ta/video-detail-form.tsx | 4 +- components/form/content/teks-detail-form.tsx | 4 +- components/form/content/video-detail-form.tsx | 2 +- style/ckeditor.css | 215 +++++++++++++++ 13 files changed, 733 insertions(+), 118 deletions(-) create mode 100644 style/ckeditor.css diff --git a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx index 14c30f78..c2388f79 100644 --- a/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx +++ b/app/[locale]/(protected)/admin/broadcast/campaign-list/account-list/component/table.tsx @@ -48,7 +48,8 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { Icon } from "@iconify/react"; import { useParams, useSearchParams } from "next/navigation"; -import { UserIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { X } from "lucide-react"; import columns from "./column"; import TablePagination from "@/components/table/table-pagination"; @@ -57,7 +58,16 @@ import { deleteMediaBlastCampaignAccount, saveMediaBlastCampaignAccount, } from "@/service/broadcast/broadcast"; -import { close, loading, error } from "@/config/swal"; +import { close, loading, error, success } from "@/config/swal"; + +// Mock data for available accounts - replace with actual API call +const availableAccounts = [ + { id: "1", accountName: "Account 1", category: "polri" }, + { id: "2", accountName: "Account 2", category: "jurnalis" }, + { id: "3", accountName: "Account 3", category: "umum" }, + { id: "4", accountName: "Account 4", category: "ksp" }, + { id: "5", accountName: "Account 5", category: "polri" }, +]; const AccountListTable = () => { const params = useParams(); @@ -82,9 +92,11 @@ const AccountListTable = () => { const [filtered, setFiltered] = React.useState([]); // --- state utk Dialog Pilih Akun --- + const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [accountCategory, setAccountCategory] = React.useState(""); const [selectedAccount, setSelectedAccount] = React.useState([]); const [selectedCategory, setSelectedCategory] = React.useState(""); + const [availableAccountsList, setAvailableAccountsList] = React.useState(availableAccounts); const table = useReactTable({ data: dataTable, @@ -154,19 +166,72 @@ const AccountListTable = () => { } async function saveCampaignAccount() { - for (const acc of selectedAccount) { - const request = { - mediaBlastCampaignId: campaignId, - mediaBlastAccountId: acc.id, - }; - const response = await saveMediaBlastCampaignAccount(request); - if (response?.error) { - error(response.message); + try { + loading(); + + if (accountCategory === "all-account") { + // Handle all accounts selection + const allAccounts = availableAccountsList.map(acc => ({ + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + })); + + for (const request of allAccounts) { + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + return; + } + } + } else if (accountCategory === "kategori" && selectedCategory) { + // Handle category selection + const categoryAccounts = availableAccountsList.filter( + acc => acc.category === selectedCategory + ); + + for (const acc of categoryAccounts) { + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + }; + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + return; + } + } + } else if (accountCategory === "custom") { + // Handle custom selection + for (const acc of selectedAccount) { + const request = { + mediaBlastCampaignId: campaignId, + mediaBlastAccountId: acc.id, + }; + const response = await saveMediaBlastCampaignAccount(request); + if (response?.error) { + error(response.message); + return; + } + } } + + close(); + success("Akun berhasil ditambahkan ke campaign!"); + resetDialogState(); + fetchData(); + } catch (err) { + close(); + error("Terjadi kesalahan saat menyimpan akun"); } - fetchData(); } + const resetDialogState = () => { + setAccountCategory(""); + setSelectedAccount([]); + setSelectedCategory(""); + setIsDialogOpen(false); + }; + const handleFilter = (id: string, checked: boolean) => { let temp = [...filtered]; if (checked) temp = [...temp, id]; @@ -174,86 +239,143 @@ const AccountListTable = () => { setFiltered(temp); }; + const handleAccountSelection = (accountId: string, checked: boolean) => { + if (checked) { + const account = availableAccountsList.find(acc => acc.id === accountId); + if (account && !selectedAccount.find(acc => acc.id === accountId)) { + setSelectedAccount([...selectedAccount, account]); + } + } else { + setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); + } + }; + + const removeSelectedAccount = (accountId: string) => { + setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); + }; + + const getFilteredAccounts = () => { + if (accountCategory === "kategori" && selectedCategory) { + return availableAccountsList.filter(acc => acc.category === selectedCategory); + } + return availableAccountsList; + }; + return (

Daftar Akun

{/* === Dialog Pilih Akun === */} - + - + Pilih Akun Untuk Campaign Ini - setAccountCategory(val)} - className="space-y-3" - > -
- - -
-
- - -
-
- - -
-
+
+ { + setAccountCategory(val); + setSelectedAccount([]); + setSelectedCategory(""); + }} + className="space-y-3" + > +
+ + +
+
+ + +
+
+ + +
+
-
- {accountCategory === "custom" && ( - <> - setSelectedCategory(val)}> - + - {dataTable.map((acc) => ( - - {acc.accountName} - - ))} + Umum + Polri + KSP + Jurnalis - {selectedAccount.length < 1 && ( -

- Pilih minimal 1 akun -

+
+ )} + + {/* Custom Account Selection */} + {accountCategory === "custom" && ( +
+ +
+ {getFilteredAccounts().map((acc) => ( +
+ selected.id === acc.id)} + onCheckedChange={(checked) => handleAccountSelection(acc.id, Boolean(checked))} + /> + +
+ ))} +
+ + {/* Selected Accounts Display */} + {selectedAccount.length > 0 && ( +
+ +
+ {selectedAccount.map((acc) => ( + + {acc.accountName} + removeSelectedAccount(acc.id)} + /> + + ))} +
+
)} - - )} - - {accountCategory === "kategori" && ( - +
)} + {/* All Accounts Info */} {accountCategory === "all-account" && ( -

Semua akun dipilih

+
+

+ Semua akun ({availableAccountsList.length} akun) akan ditambahkan ke campaign ini. +

+
+ )} + + {/* Category Accounts Info */} + {accountCategory === "kategori" && selectedCategory && ( +
+

+ {getFilteredAccounts().length} akun dari kategori "{selectedCategory}" akan ditambahkan. +

+
)}
@@ -261,13 +383,17 @@ const AccountListTable = () => { - +
diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css index 688a4f26..25750c7f 100644 --- a/app/[locale]/globals.css +++ b/app/[locale]/globals.css @@ -575,10 +575,108 @@ html[dir="rtl"] .react-select .select__loading-indicator { background: #9ca3af; } +/* CKEditor Styling */ .ck-editor__editable_inline { min-height: 200px; } +/* Main CKEditor content area styling */ +.ck.ck-editor__editable { + padding: 1.5em 2em !important; + min-height: 400px; + max-height: 600px; + line-height: 1.6; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #cbd5e1 #f1f5f9; +} + +/* CKEditor content styling */ +.ck.ck-editor__editable .ck-content { + padding: 0; +} + +/* CKEditor scrollbar styling */ +.ck.ck-editor__editable::-webkit-scrollbar { + width: 8px; +} + +.ck.ck-editor__editable::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 4px; +} + +.ck.ck-editor__editable::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +.ck.ck-editor__editable::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* CKEditor editable area focus state */ +.ck.ck-editor__editable.ck-focused { + border-color: #1a9aef; + box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2); +} + +/* CKEditor toolbar styling */ +.ck.ck-toolbar { + border-radius: 4px 4px 0 0; +} + +/* CKEditor editable border styling */ +.ck.ck-editor__editable { + border-radius: 0 0 4px 4px; + border: 1px solid #d1d5db; +} + +/* CKEditor content typography */ +.ck.ck-editor__editable p { + margin: 0.5em 0; +} + +/* View Editor specific styling (read-only mode) */ +.ckeditor-view-wrapper .ck.ck-editor__editable { + background-color: #f8fafc !important; + color: #4b5563 !important; + cursor: default !important; + border: 1px solid #d1d5db !important; + border-radius: 6px !important; +} + +.ckeditor-view-wrapper .ck.ck-editor__editable.ck-focused { + border-color: #d1d5db !important; + box-shadow: none !important; +} + +.ckeditor-view-wrapper .ck.ck-toolbar { + display: none !important; +} + +.ck.ck-editor__editable h1, +.ck.ck-editor__editable h2, +.ck.ck-editor__editable h3, +.ck.ck-editor__editable h4, +.ck.ck-editor__editable h5, +.ck.ck-editor__editable h6 { + margin: 1em 0 0.5em 0; +} + +.ck.ck-editor__editable ul, +.ck.ck-editor__editable ol { + margin: 0.5em 0; + padding-left: 2em; +} + +.ck.ck-editor__editable blockquote { + margin: 1em 0; + padding: 0.5em 1em; + border-left: 4px solid #d1d5db; + background-color: #f9fafb; +} + /* Hide FullCalendar grid elements */ .fc-view-harness:has(.hide-calendar-grid) { display: none; diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 67ff5946..3204db83 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import "./theme.css"; +import "../../style/ckeditor.css"; import { ThemeProvider } from "@/providers/theme-provider"; import MountedProvider from "@/providers/mounted.provider"; import { Toaster } from "@/components/ui/toaster"; diff --git a/components/editor/custom-editor.js b/components/editor/custom-editor.js index f28746b2..a3f7ed77 100644 --- a/components/editor/custom-editor.js +++ b/components/editor/custom-editor.js @@ -5,36 +5,118 @@ import { CKEditor } from "@ckeditor/ckeditor5-react"; import Editor from "ckeditor5-custom-build"; function CustomEditor(props) { + const maxHeight = props.maxHeight || 600; // Default max height 600px + return ( - { - const data = editor.getData(); - console.log({ event, editor, data }); - props.onChange(data); - }} - config={{ - toolbar: [ - "heading", - "fontsize", - "bold", - "italic", - "link", - "numberedList", - "bulletedList", - "undo", - "redo", - "alignment", - "outdent", - "indent", - "blockQuote", - "insertTable", - "codeBlock", - "sourceEditing", - ], - }} - /> +
+ { + const data = editor.getData(); + console.log({ event, editor, data }); + props.onChange(data); + }} + config={{ + toolbar: [ + "heading", + "fontsize", + "bold", + "italic", + "link", + "numberedList", + "bulletedList", + "undo", + "redo", + "alignment", + "outdent", + "indent", + "blockQuote", + "insertTable", + "codeBlock", + "sourceEditing", + ], + // Add content styling configuration + 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'], + // Better mobile support + mobile: { + theme: 'silver' + } + }} + /> + +
); } diff --git a/components/editor/view-editor.js b/components/editor/view-editor.js index c2529003..aee35c00 100644 --- a/components/editor/view-editor.js +++ b/components/editor/view-editor.js @@ -3,16 +3,109 @@ import { CKEditor } from "@ckeditor/ckeditor5-react"; import Editor from "ckeditor5-custom-build"; function ViewEditor(props) { + const maxHeight = props.maxHeight || 600; // Default max height 600px + return ( - +
+ + +
); } diff --git a/components/form/content/image-detail-form.tsx b/components/form/content/image-detail-form.tsx index 037f2836..7f2f620f 100644 --- a/components/form/content/image-detail-form.tsx +++ b/components/form/content/image-detail-form.tsx @@ -950,8 +950,8 @@ export default function FormImageDetail() { setCheckedLevels(levels); } - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/audio-detail-form.tsx b/components/form/content/task-ta/audio-detail-form.tsx index f8470870..46534ac9 100644 --- a/components/form/content/task-ta/audio-detail-form.tsx +++ b/components/form/content/task-ta/audio-detail-form.tsx @@ -261,8 +261,8 @@ export default function FormAudioTaDetail() { }); setupPlacementCheck(details?.files?.length); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/image-detail-form.tsx b/components/form/content/task-ta/image-detail-form.tsx index 92280fcf..70f4fc1d 100644 --- a/components/form/content/task-ta/image-detail-form.tsx +++ b/components/form/content/task-ta/image-detail-form.tsx @@ -247,8 +247,8 @@ export default function FormImageTaDetail() { }); setupPlacementCheck(details?.files?.length); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/teks-detail-form.tsx b/components/form/content/task-ta/teks-detail-form.tsx index 0bb0cc98..72d9ada0 100644 --- a/components/form/content/task-ta/teks-detail-form.tsx +++ b/components/form/content/task-ta/teks-detail-form.tsx @@ -246,8 +246,8 @@ export default function FormTeksTaDetail() { format: details?.files[0]?.format, }); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/task-ta/video-detail-form.tsx b/components/form/content/task-ta/video-detail-form.tsx index 6c201bc7..c3b9ce82 100644 --- a/components/form/content/task-ta/video-detail-form.tsx +++ b/components/form/content/task-ta/video-detail-form.tsx @@ -237,8 +237,8 @@ export default function FormVideoTaDetail() { format: details?.files[0]?.format, }); - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/teks-detail-form.tsx b/components/form/content/teks-detail-form.tsx index 3f2659b9..c54a94e1 100644 --- a/components/form/content/teks-detail-form.tsx +++ b/components/form/content/teks-detail-form.tsx @@ -519,8 +519,8 @@ export default function FormTeksDetail() { setCheckedLevels(levels); } - if (details.publishedForObject) { - const publisherIds = details.publishedForObject.map( + if (details?.publishedForObject) { + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/components/form/content/video-detail-form.tsx b/components/form/content/video-detail-form.tsx index 857eec2f..048d53d3 100644 --- a/components/form/content/video-detail-form.tsx +++ b/components/form/content/video-detail-form.tsx @@ -492,7 +492,7 @@ export default function FormVideoDetail() { } if (details?.publishedForObject) { - const publisherIds = details.publishedForObject.map( + const publisherIds = details?.publishedForObject?.map( (obj: any) => obj.id ); setSelectedPublishers(publisherIds); diff --git a/style/ckeditor.css b/style/ckeditor.css new file mode 100644 index 00000000..e55d0761 --- /dev/null +++ b/style/ckeditor.css @@ -0,0 +1,215 @@ +/* CKEditor Custom Styling */ + +/* Main editor container */ +.ck.ck-editor { + 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); +} + +/* Toolbar styling */ +.ck.ck-toolbar { + background: #f8fafc; + border: 1px solid #d1d5db; + border-bottom: none; + border-radius: 6px 6px 0 0; + padding: 8px; +} + +.ck.ck-toolbar .ck-toolbar__items { + gap: 4px; +} + +/* Main editable area */ +.ck.ck-editor__editable { + background: #ffffff; + border: 1px solid #d1d5db; + border-top: none; + border-radius: 0 0 6px 6px; + padding: 1.5em 2em !important; + min-height: 400px; + line-height: 1.6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: #333; +} + +/* Focus state */ +.ck.ck-editor__editable.ck-focused { + border-color: #1a9aef; + box-shadow: 0 0 0 2px rgba(26, 154, 239, 0.2); + outline: none; +} + +/* Content styling */ +.ck.ck-editor__editable .ck-content { + padding: 0; +} + +/* Typography improvements */ +.ck.ck-editor__editable p { + margin: 0.5em 0; + line-height: 1.6; +} + +.ck.ck-editor__editable h1, +.ck.ck-editor__editable h2, +.ck.ck-editor__editable h3, +.ck.ck-editor__editable h4, +.ck.ck-editor__editable h5, +.ck.ck-editor__editable h6 { + margin: 1em 0 0.5em 0; + font-weight: 600; + line-height: 1.4; +} + +.ck.ck-editor__editable h1 { font-size: 1.75em; } +.ck.ck-editor__editable h2 { font-size: 1.5em; } +.ck.ck-editor__editable h3 { font-size: 1.25em; } +.ck.ck-editor__editable h4 { font-size: 1.1em; } +.ck.ck-editor__editable h5 { font-size: 1em; } +.ck.ck-editor__editable h6 { font-size: 0.9em; } + +/* Lists */ +.ck.ck-editor__editable ul, +.ck.ck-editor__editable ol { + margin: 0.5em 0; + padding-left: 2em; +} + +.ck.ck-editor__editable li { + margin: 0.25em 0; + line-height: 1.6; +} + +/* Blockquotes */ +.ck.ck-editor__editable blockquote { + margin: 1em 0; + padding: 0.75em 1em; + border-left: 4px solid #1a9aef; + background-color: #f8fafc; + border-radius: 0 4px 4px 0; + font-style: italic; + color: #4b5563; +} + +/* Tables */ +.ck.ck-editor__editable table { + border-collapse: collapse; + width: 100%; + margin: 1em 0; +} + +.ck.ck-editor__editable table td, +.ck.ck-editor__editable table th { + border: 1px solid #d1d5db; + padding: 0.5em 0.75em; + text-align: left; +} + +.ck.ck-editor__editable table th { + background-color: #f8fafc; + font-weight: 600; +} + +/* Links */ +.ck.ck-editor__editable a { + color: #1a9aef; + text-decoration: underline; +} + +.ck.ck-editor__editable a:hover { + color: #0d7cd6; +} + +/* Code blocks */ +.ck.ck-editor__editable pre { + background-color: #f8fafc; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 1em; + margin: 1em 0; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.4; +} + +.ck.ck-editor__editable code { + background-color: #f1f5f9; + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9em; +} + +/* Images */ +.ck.ck-editor__editable img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 0.5em 0; +} + +/* Horizontal rule */ +.ck.ck-editor__editable hr { + border: none; + border-top: 1px solid #d1d5db; + margin: 2em 0; +} + +/* Placeholder text */ +.ck.ck-editor__editable.ck-blurred:empty::before { + content: attr(data-placeholder); + color: #9ca3af; + font-style: italic; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .ck.ck-editor__editable { + padding: 1em 1.5em !important; + font-size: 16px; /* Better for mobile */ + } + + .ck.ck-toolbar { + padding: 6px; + } + + .ck.ck-toolbar .ck-toolbar__items { + gap: 2px; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .ck.ck-editor__editable { + background: #1f2937; + color: #f9fafb; + border-color: #4b5563; + } + + .ck.ck-editor__editable h1, + .ck.ck-editor__editable h2, + .ck.ck-editor__editable h3, + .ck.ck-editor__editable h4, + .ck.ck-editor__editable h5, + .ck.ck-editor__editable h6 { + color: #f9fafb; + } + + .ck.ck-editor__editable blockquote { + background-color: #374151; + border-left-color: #1a9aef; + color: #d1d5db; + } + + .ck.ck-editor__editable pre { + background-color: #374151; + border-color: #4b5563; + } + + .ck.ck-editor__editable code { + background-color: #4b5563; + } +}