diff --git a/package-lock.json b/package-lock.json index dc64872..e3548ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "@radix-ui/react-tooltip": "^1.2.7", "@supabase/supabase-js": "^2.88.0", "@tanstack/react-query": "^5.83.0", + "@tiptap/extension-code-block-lowlight": "^3.14.0", "@tiptap/extension-image": "^3.13.0", "@tiptap/extension-link": "^3.13.0", "@tiptap/extension-placeholder": "^3.13.0", @@ -52,8 +53,10 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^3.6.0", + "dompurify": "^3.3.1", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "lowlight": "^3.3.0", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", "qrcode.react": "^4.2.0", @@ -2959,17 +2962,34 @@ } }, "node_modules/@tiptap/extension-code-block": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.13.0.tgz", - "integrity": "sha512-kIwfQ4iqootsWg9e74iYJK54/YMIj6ahUxEltjZRML5z/h4gTDcQt2eTpnEC8yjDjHeUVOR94zH9auCySyk9CQ==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.14.0.tgz", + "integrity": "sha512-hRSdIhhm3Q9JBMQdKaifRVFnAa4sG+M7l1QcTKR3VSYVy2/oR0U+aiOifi5OvMRBUwhaR71Ro+cMT9FH9s26Kg==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.13.0", - "@tiptap/pm": "^3.13.0" + "@tiptap/core": "^3.14.0", + "@tiptap/pm": "^3.14.0" + } + }, + "node_modules/@tiptap/extension-code-block-lowlight": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.14.0.tgz", + "integrity": "sha512-vkiDvPZUadrjAGNzvJYYXl5R+U1XmGALSbm+VlrGCR7iXHgYaMHdkqxHwGZMSqtsF2szPEqcAzLZShlAKl+AkA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.14.0", + "@tiptap/extension-code-block": "^3.14.0", + "@tiptap/pm": "^3.14.0", + "highlight.js": "^11", + "lowlight": "^2 || ^3" } }, "node_modules/@tiptap/extension-document": { @@ -3471,6 +3491,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3540,6 +3569,19 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4411,12 +4453,34 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4439,6 +4503,15 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5033,6 +5106,15 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", @@ -5790,6 +5872,21 @@ "@esbuild/win32-x64": "0.25.0" } }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", diff --git a/package.json b/package.json index 3ae6c54..8019f70 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@radix-ui/react-tooltip": "^1.2.7", "@supabase/supabase-js": "^2.88.0", "@tanstack/react-query": "^5.83.0", + "@tiptap/extension-code-block-lowlight": "^3.14.0", "@tiptap/extension-image": "^3.13.0", "@tiptap/extension-link": "^3.13.0", "@tiptap/extension-placeholder": "^3.13.0", @@ -55,8 +56,10 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^3.6.0", + "dompurify": "^3.3.1", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", + "lowlight": "^3.3.0", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", "qrcode.react": "^4.2.0", diff --git a/src/App.tsx b/src/App.tsx index 30da905..9101255 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,7 @@ import AdminEvents from "./pages/admin/AdminEvents"; import AdminSettings from "./pages/admin/AdminSettings"; import AdminConsulting from "./pages/admin/AdminConsulting"; import AdminReviews from "./pages/admin/AdminReviews"; +import ProductCurriculum from "./pages/admin/ProductCurriculum"; const queryClient = new QueryClient(); @@ -73,6 +74,7 @@ const App = () => ( {/* Admin routes */} } /> } /> + } /> } /> } /> } /> diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index 98f3c73..1a8710d 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -4,12 +4,13 @@ import Link from '@tiptap/extension-link'; import Image from '@tiptap/extension-image'; import Placeholder from '@tiptap/extension-placeholder'; import TextAlign from '@tiptap/extension-text-align'; +import CodeBlock from '@tiptap/extension-code-block'; import { Node } from '@tiptap/core'; import { Button } from '@/components/ui/button'; import { Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon, Image as ImageIcon, Heading1, Heading2, Undo, Redo, - Maximize2, Minimize2, MousePointer, Square, AlignLeft, AlignCenter, AlignRight, AlignJustify, MoreVertical, Minus + Maximize2, Minimize2, MousePointer, Square, AlignLeft, AlignCenter, AlignRight, AlignJustify, MoreVertical, Minus, Code, Copy, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useCallback, useEffect, useState } from 'react'; @@ -18,6 +19,38 @@ import { toast } from '@/hooks/use-toast'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { common, createLowlight } from 'lowlight'; + +// Register common languages for syntax highlighting +const lowlight = createLowlight(common); + +// Code Block Component with Copy Button +const CodeBlockWithCopy = ({ node }: { node: any }) => { + const [copied, setCopied] = useState(false); + const code = node.textContent; + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ +
+        {code}
+      
+
+ ); +}; interface RichTextEditorProps { content: string; @@ -249,6 +282,20 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten. levels: [1, 2, 3], }, horizontalRule: true, + codeBlock: false, // Disable default code block to use custom one + }), + CodeBlock.configure({ + lowlight, + defaultLanguage: 'text', + HTMLAttributes: { + class: 'code-block-wrapper', + }, + }).extend({ + addKeyboardShortcuts() { + return { + 'Mod-Shift-c': () => this.editor.commands.toggleCodeBlock(), + }; + }, }), TextAlign.configure({ types: ['heading', 'paragraph'], @@ -516,6 +563,16 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten. > +