diff --git a/bun.lock b/bun.lock index 8f2fcf2f..6a0ca50e 100644 --- a/bun.lock +++ b/bun.lock @@ -21,11 +21,12 @@ "@react-pdf/renderer": "^3.4.4", "@tabler/icons-react": "^3.31.0", "@tiptap/extension-highlight": "^2.2.3", + "@tiptap/extension-image": "^2.11.7", "@tiptap/extension-link": "^2.2.3", "@tiptap/extension-subscript": "^2.2.3", "@tiptap/extension-superscript": "^2.2.3", "@tiptap/extension-text-align": "^2.2.3", - "@tiptap/extension-underline": "^2.2.3", + "@tiptap/extension-underline": "^2.11.7", "@tiptap/pm": "^2.2.3", "@tiptap/react": "^2.2.3", "@tiptap/starter-kit": "^2.2.3", @@ -955,6 +956,8 @@ "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@2.11.5", "", { "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-3up2r1Du8/5/4ZYzTC0DjTwhgPI3dn8jhOCLu73m5F3OGvK/9whcXoeWoX103hYMnGDxBlfOje71yQuN35FL4A=="], + "@tiptap/extension-image": ["@tiptap/extension-image@2.11.7", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-YvCmTDB7Oo+A56tR4S/gcNaYpqU4DDlSQcRp5IQvmQV5EekSe0lnEazGDoqOCwsit9qQhj4MPQJhKrnaWrJUrg=="], + "@tiptap/extension-italic": ["@tiptap/extension-italic@2.11.5", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA=="], "@tiptap/extension-link": ["@tiptap/extension-link@2.11.5", "", { "dependencies": { "linkifyjs": "^4.2.0" }, "peerDependencies": { "@tiptap/core": "^2.7.0", "@tiptap/pm": "^2.7.0" } }, "sha512-4Iu/aPzevbYpe50xDI0ZkqRa6nkZ9eF270Ue2qaF3Ab47nehj+9Jl78XXzo8+LTyFMnrETI73TAs1aC/IGySeQ=="], @@ -977,7 +980,7 @@ "@tiptap/extension-text-style": ["@tiptap/extension-text-style@2.11.5", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ=="], - "@tiptap/extension-underline": ["@tiptap/extension-underline@2.11.5", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-YpWHXNIkSoRSuzT2cvgKpyJ2tTz3LzqkTM64uC+uTJ8cUkvXIWUWejJR42q8ma/mTlQe4lHff4IQ0Sf58Digtw=="], + "@tiptap/extension-underline": ["@tiptap/extension-underline@2.11.7", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-NtoQw6PGijOAtXC6G+0Aq0/Z5wwEjPhNHs8nsjXogfWIgaj/aI4/zfBnA06eI3WT+emMYQTl0fTc4CUPnLVU8g=="], "@tiptap/pm": ["@tiptap/pm@2.11.5", "", { "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.23.0", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.4.1", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.3", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.37.0" } }, "sha512-z9JFtqc5ZOsdQLd9vRnXfTCQ8v5ADAfRt9Nm7SqP6FUHII8E1hs38ACzf5xursmth/VonJYb5+73Pqxk1hGIPw=="], diff --git a/package.json b/package.json index 745edb3d..fd8c5fdd 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,12 @@ "@react-pdf/renderer": "^3.4.4", "@tabler/icons-react": "^3.31.0", "@tiptap/extension-highlight": "^2.2.3", + "@tiptap/extension-image": "^2.11.7", "@tiptap/extension-link": "^2.2.3", "@tiptap/extension-subscript": "^2.2.3", "@tiptap/extension-superscript": "^2.2.3", "@tiptap/extension-text-align": "^2.2.3", - "@tiptap/extension-underline": "^2.2.3", + "@tiptap/extension-underline": "^2.11.7", "@tiptap/pm": "^2.2.3", "@tiptap/react": "^2.2.3", "@tiptap/starter-kit": "^2.2.3", diff --git a/src/app/dev/forum/detail/main-detail/[id]/page.tsx b/src/app/dev/forum/detail/main-detail/[id]/page.tsx index 596596bb..325f4bb7 100644 --- a/src/app/dev/forum/detail/main-detail/[id]/page.tsx +++ b/src/app/dev/forum/detail/main-detail/[id]/page.tsx @@ -3,10 +3,10 @@ import Forum_MainDetail from "@/app_modules/forum/detail/main_detail"; export default async function Page() { const userLoginId = await funGetUserIdByToken(); - return ( <> + {/* */} ); } diff --git a/src/app/zCoba/text_editor/page.tsx b/src/app/zCoba/text_editor/page.tsx new file mode 100644 index 00000000..76fc18ac --- /dev/null +++ b/src/app/zCoba/text_editor/page.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Image from "@tiptap/extension-image"; +import { useState } from "react"; +import { + Box, + Button, + Group, + Image as MantineImage, + Stack, + Text, +} from "@mantine/core"; +import Underline from "@tiptap/extension-underline"; +import { MainColor } from "@/app_modules/_global/color"; + +const listStiker = [ + { + id: 2, + name: "stiker2", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQN9AKmsBY4yqdn3GueJJEVPJbfmf853gDL4cN8uc9eqsCTiJ1fzhcpywzVP68NCJEA5NQ&usqp=CAU", + }, + { + id: 3, + name: "stiker3", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS2lkV3ZiQ8m-OELSui2JGVy80vnh1cyRUV7NrgFNluPVVs2HUAyCHwCMAKGe2s5jk2sn8&usqp=CAU", + }, + { + id: 4, + name: "stiker4", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQHy9ZdsPc6dHgVTl5yIGpRJ-KtpTIsXA2_kbfO1Oc-pv_f7CNKGxhO56RjKujE3xCyb9k&usqp=CAU", + }, +]; + +export default function RichTextWithStickers() { + const [chat, setChat] = useState([]); + + const editor = useEditor({ + extensions: [ + StarterKit, // Sudah include Bold, Italic, dll + Underline, // Tambahan untuk underline + Image, + ], + content: "", + }); + + const insertSticker = (url: string) => { + editor?.chain().focus().setImage({ src: url }).run(); + }; + + return ( + + Tiptap Editor dengan Stiker Inline + + + + + + + + + + + + + + {listStiker.map((item) => ( + insertSticker(item.url)} + style={{ + border: "none", + background: "transparent", + cursor: "pointer", + }} + > + + + ))} + + + {/* + {chat.map((item, index) => ( + + ))} + */} + + ); +} diff --git a/src/app/zCoba/text_editor2/page.tsx b/src/app/zCoba/text_editor2/page.tsx new file mode 100644 index 00000000..b868b363 --- /dev/null +++ b/src/app/zCoba/text_editor2/page.tsx @@ -0,0 +1,226 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { + Box, + Button, + Group, + Image, + Paper, + ScrollArea, + SimpleGrid, + Stack, + Text, + Tooltip, + Modal, +} from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import dynamic from "next/dynamic"; +import { MainColor } from "@/app_modules/_global/color"; + +// Dynamic import ReactQuill dengan SSR disabled +const ReactQuill = dynamic( + async () => { + const { default: RQ } = await import("react-quill"); + // Tidak perlu import CSS dengan import statement + return function comp({ forwardedRef, ...props }: any) { + return ; + }; + }, + { ssr: false, loading: () =>

Loading Editor...

} +); + +const listStiker = [ + { + id: 1, + name: "stiker2", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQN9AKmsBY4yqdn3GueJJEVPJbfmf853gDL4cN8uc9eqsCTiJ1fzhcpywzVP68NCJEA5NQ&usqp=CAU", + }, + { + id: 2, + name: "stiker3", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS2lkV3ZiQ8m-OELSui2JGVy80vnh1cyRUV7NrgFNluPVVs2HUAyCHwCMAKGe2s5jk2sn8&usqp=CAU", + }, + { + id: 3, + name: "stiker4", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQHy9ZdsPc6dHgVTl5yIGpRJ-KtpTIsXA2_kbfO1Oc-pv_f7CNKGxhO56RjKujE3xCyb9k&usqp=CAU", + }, +]; + +type ChatItem = { + content: string; // HTML content including text and stickers +}; + +export default function Page() { + const [editorContent, setEditorContent] = useState(""); + const [chat, setChat] = useState([]); + const [opened, { open, close }] = useDisclosure(false); + const quillRef = React.useRef(null); + const [quillLoaded, setQuillLoaded] = useState(false); + + // Load CSS on client-side only + useEffect(() => { + // Add Quill CSS via tag + const link = document.createElement("link"); + link.href = "https://cdn.quilljs.com/1.3.6/quill.snow.css"; + link.rel = "stylesheet"; + document.head.appendChild(link); + + // Add custom style for stickers inside Quill editor + const style = document.createElement("style"); + style.textContent = ` + .ql-editor img { + max-width: 40px !important; + max-height: 40px !important; + } + .chat-content img { + max-width: 40px !important; + max-height: 40px !important; + } + `; + document.head.appendChild(style); + + setQuillLoaded(true); + + return () => { + // Clean up when component unmounts + document.head.removeChild(link); + document.head.removeChild(style); + }; + }, []); + + // Custom toolbar options for ReactQuill + const modules = { + toolbar: [ + [{ header: [1, 2, false] }], + ["bold", "italic", "underline", "strike", "blockquote"], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "image"], + ["clean"], + ], + }; + + const formats = [ + "header", + "bold", + "italic", + "underline", + "strike", + "blockquote", + "list", + "bullet", + "link", + "image", + ]; + + const insertSticker = (stickerUrl: string) => { + if (!quillRef.current) return; + + const quill = quillRef.current.getEditor(); + const range = quill.getSelection(true); + + // Custom image insertion with size + // Use custom blot or HTML string with size attributes + const stickerHtml = `sticker`; + + // Insert HTML at cursor position + quill.clipboard.dangerouslyPasteHTML(range.index, stickerHtml); + + // Move cursor after inserted sticker + quill.setSelection(range.index + 1, 0); + + // Focus back on editor + quill.focus(); + + // Close sticker modal + close(); + }; + + // Function to send message + const sendMessage = () => { + if (editorContent.trim() !== "") { + setChat((prev) => [...prev, { content: editorContent }]); + setEditorContent(""); // Clear after sending + } + }; + + return ( + + + + + + {chat.map((item, index) => ( + +
+ + ))} + + + + + + Chat Preview Data: + + +
+              {JSON.stringify(chat, null, 2)}
+            
+
+
+ + + + + {quillLoaded && ( + + )} + + + + + + + + + + {/* Sticker Modal */} + + + {listStiker.map((item) => ( + + + {item.name} insertSticker(item.url)} + /> + + + ))} + + + + ); +} diff --git a/src/app_modules/_global/lib/stiker.ts b/src/app_modules/_global/lib/stiker.ts new file mode 100644 index 00000000..237cf009 --- /dev/null +++ b/src/app_modules/_global/lib/stiker.ts @@ -0,0 +1,17 @@ +export const listStiker = [ + { + id: 1, + name: "stiker2", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQN9AKmsBY4yqdn3GueJJEVPJbfmf853gDL4cN8uc9eqsCTiJ1fzhcpywzVP68NCJEA5NQ&usqp=CAU", + }, + { + id: 2, + name: "stiker3", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS2lkV3ZiQ8m-OELSui2JGVy80vnh1cyRUV7NrgFNluPVVs2HUAyCHwCMAKGe2s5jk2sn8&usqp=CAU", + }, + { + id: 3, + name: "stiker4", + url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQHy9ZdsPc6dHgVTl5yIGpRJ-KtpTIsXA2_kbfO1Oc-pv_f7CNKGxhO56RjKujE3xCyb9k&usqp=CAU", + }, +]; \ No newline at end of file diff --git a/src/app_modules/forum/component/detail_component/comp_create_commentar.tsx b/src/app_modules/forum/component/detail_component/comp_create_commentar.tsx new file mode 100644 index 00000000..e3033ee6 --- /dev/null +++ b/src/app_modules/forum/component/detail_component/comp_create_commentar.tsx @@ -0,0 +1,133 @@ +"use client"; + +import ComponentGlobal_InputCountDown from "@/app_modules/_global/component/input_countdown"; +import { ComponentGlobal_NotifikasiBerhasil } from "@/app_modules/_global/notif_global/notifikasi_berhasil"; +import { ComponentGlobal_NotifikasiGagal } from "@/app_modules/_global/notif_global/notifikasi_gagal"; +import notifikasiToUser_funCreate from "@/app_modules/notifikasi/fun/create/create_notif_to_user"; +import { Stack, Paper, Group, Button, Divider } from "@mantine/core"; +import { useState } from "react"; +import dynamic from "next/dynamic"; +const ReactQuill = dynamic( + () => { + return import("react-quill"); + }, + { ssr: false } +); +import { forum_funCreateKomentar } from "../../fun/create/fun_create_komentar"; +import { forum_funGetAllKomentarById } from "../../fun/get/get_all_komentar_by_id"; +import { MODEL_FORUM_POSTING } from "../../model/interface"; +import { useParams, useRouter } from "next/navigation"; +import { MainColor } from "@/app_modules/_global/color/color_pallet"; +import mqtt_client from "@/util/mqtt_client"; +import backendLogger from "@/util/backendLogger"; +import { clientLogger } from "@/util/clientLogger"; +export default function Forum_CompCreateComment({ + data, + userLoginId, + onSetNewKomentar, +}: { + data: MODEL_FORUM_POSTING; + userLoginId: string; + onSetNewKomentar: (val: string) => void; +}) { + const param = useParams<{ id: string }>(); + const postingId = param.id; + const [value, setValue] = useState(""); + const [loading, setLoading] = useState(false); + const [isEmpty, setIsEmpty] = useState(false); + + async function onComment() { + if (value.length > 500) { + return null; + } + + try { + setLoading(true); + const createComment = await forum_funCreateKomentar(postingId, value); + if (createComment.status === 201) { + onSetNewKomentar(value); + setValue(""); + setIsEmpty(true); + ComponentGlobal_NotifikasiBerhasil(createComment.message, 2000); + + if (userLoginId !== data.Author.id) { + const dataNotif = { + appId: data.id, + userId: data.authorId, + pesan: value, + kategoriApp: "FORUM", + title: "Komentar baru", + }; + + const createNotifikasi = await notifikasiToUser_funCreate({ + data: dataNotif as any, + }); + + if (createNotifikasi.status === 201) { + mqtt_client.publish( + "USER", + JSON.stringify({ + userId: dataNotif.userId, + count: 1, + }) + ); + } + } + } else { + setLoading(false); + ComponentGlobal_NotifikasiGagal(createComment.message); + } + } catch (error) { + setLoading(false); + clientLogger.error("Error create komentar forum", error); + } + } + + return ( + <> + + + { + setValue(val); + }} + style={{ + overflowY: "auto", + maxHeight: 100, + minHeight: 50, + }} + /> + + + + + + + + + ); +} diff --git a/src/app_modules/forum/detail/main_detail.tsx b/src/app_modules/forum/detail/main_detail.tsx index 58b6de01..7a434242 100644 --- a/src/app_modules/forum/detail/main_detail.tsx +++ b/src/app_modules/forum/detail/main_detail.tsx @@ -34,7 +34,9 @@ export default function Forum_MainDetail({ const [dataPosting, setDataPosting] = useState( null ); - const [listKomentar, setListKomentar] = useState([]); + const [listKomentar, setListKomentar] = useState< + MODEL_FORUM_KOMENTAR[] | null + >(null); const [activePage, setActivePage] = useState(1); const [newKomentar, setNewKomentar] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -130,7 +132,7 @@ export default function Forum_MainDetail({ return ( <> - {!dataPosting || !listKomentar ? ( + {!dataPosting ? ( ) : ( ) : _.isEmpty(listKomentar) ? ( @@ -172,7 +174,7 @@ export default function Forum_MainDetail({ )} data={listKomentar} - setData={setListKomentar} + setData={setListKomentar as any} moreData={handleMoreDataKomentar} > {(item) => ( diff --git a/src/app_modules/forum/model/interface.tsx b/src/app_modules/forum/model/interface.tsx index 83c7a8df..0a001618 100644 --- a/src/app_modules/forum/model/interface.tsx +++ b/src/app_modules/forum/model/interface.tsx @@ -75,3 +75,8 @@ export interface MODEL_FORUM_REPORT_KOMENTAR { userId: string; User: MODEL_USER; } + +export type CommentItem = { + deskripsi: string; + // Add any other properties that CommentItem should have +}; \ No newline at end of file