diff --git a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx index ceda4d69..998cf8db 100644 --- a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx +++ b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx @@ -25,6 +25,7 @@ import { NoTextInput } from '@components/Objects/Editor/Extensions/NoTextInput/N import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext' import AICanvaToolkit from './AI/AICanvaToolkit' import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects' +import Badges from '@components/Objects/Editor/Extensions/Badges/Badges' interface Editor { content: string @@ -90,6 +91,10 @@ function Canva(props: Editor) { editable: isEditable, activity: props.activity, }), + Badges.configure({ + editable: isEditable, + activity: props.activity, + }), ], content: props.content, diff --git a/apps/web/components/Objects/Editor/Editor.tsx b/apps/web/components/Objects/Editor/Editor.tsx index 22d4c13d..f416d35a 100644 --- a/apps/web/components/Objects/Editor/Editor.tsx +++ b/apps/web/components/Objects/Editor/Editor.tsx @@ -49,6 +49,7 @@ import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import ActiveAvatars from './ActiveAvatars' import { getUriWithOrg } from '@services/config/config' import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects' +import Badges from './Extensions/Badges/Badges' interface Editor { content: string @@ -138,6 +139,10 @@ function Editor(props: Editor) { editable: true, activity: props.activity, }), + Badges.configure({ + editable: true, + activity: props.activity, + }), // Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true ...(props.isCollabEnabledOnThisOrg ? [ diff --git a/apps/web/components/Objects/Editor/Extensions/Badges/Badges.ts b/apps/web/components/Objects/Editor/Extensions/Badges/Badges.ts new file mode 100644 index 00000000..2d2dda19 --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/Badges/Badges.ts @@ -0,0 +1,39 @@ +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { mergeAttributes, Node } from "@tiptap/core"; +import BadgesExtension from "@/components/Objects/Editor/Extensions/Badges/BadgesExtension"; + +export default Node.create({ + name: "badge", + group: "block", + draggable: true, + content: "text*", + + // TODO : multi line support + + addAttributes() { + return { + color: { + default: 'sky', + }, + emoji: { + default: '💡', + }, + }; + }, + + parseHTML() { + return [ + { + tag: "badge", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["badge", mergeAttributes(HTMLAttributes), 0]; + }, + + addNodeView() { + return ReactNodeViewRenderer(BadgesExtension); + }, +}); diff --git a/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx b/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx new file mode 100644 index 00000000..888eb730 --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx @@ -0,0 +1,221 @@ +import { NodeViewContent, NodeViewWrapper } from '@tiptap/react' +import React, { useState, useRef, useEffect } from 'react' +import Picker from '@emoji-mart/react' +import { ArrowRight, ChevronDown, ChevronRight, EllipsisVertical, Palette, Plus } from 'lucide-react' +import { twMerge } from 'tailwind-merge' +import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' + +const BadgesExtension: React.FC = (props: any) => { + const [color, setColor] = useState(props.node.attrs.color) + const [emoji, setEmoji] = useState(props.node.attrs.emoji) + const [showEmojiPicker, setShowEmojiPicker] = useState(false) + const [showColorPicker, setShowColorPicker] = useState(false) + const [showPredefinedCallouts, setShowPredefinedCallouts] = useState(false) + const pickerRef = useRef(null) + const colorPickerRef = useRef(null) + const editorState = useEditorProvider() as any + const isEditable = editorState.isEditable + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + (pickerRef.current && !pickerRef.current.contains(event.target as Node)) || + (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) + ) { + setShowEmojiPicker(false) + setShowColorPicker(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + const handleEmojiSelect = (emoji: any) => { + setEmoji(emoji.native) + setShowEmojiPicker(false) + props.updateAttributes({ + emoji: emoji.native, + }) + } + + const handleColorSelect = (selectedColor: string) => { + setColor(selectedColor) + setShowColorPicker(false) + props.updateAttributes({ + color: selectedColor, + }) + } + + const handlePredefinedBadgeSelect = (badge: typeof predefinedBadges[0]) => { + setEmoji(badge.emoji) + setColor(badge.color) + + props.updateAttributes({ + emoji: badge.emoji, + color: badge.color, + }) + + // Insert the predefined content + const { editor } = props + if (editor) { + editor.commands.setTextSelection({ from: props.getPos() + 1, to: props.getPos() + props.node.nodeSize - 1 }) + editor.commands.insertContent(badge.content) + } + + setShowPredefinedCallouts(false) + } + + const colors = ['sky', 'green', 'yellow', 'red', 'purple', 'teal', 'amber', 'indigo', 'neutral'] + const predefinedBadges = [ + { + emoji: '📝', + color: 'sky', + content: 'Key Concept' + }, + { + emoji: '💡', + color: 'yellow', + content: 'Example' + }, + { + emoji: '🔍', + color: 'teal', + content: 'Deep Dive' + }, + { + emoji: '⚠️', + color: 'red', + content: 'Important Note' + }, + { + emoji: '🧠', + color: 'purple', + content: 'Remember This' + }, + { + emoji: '🏋️', + color: 'green', + content: 'Exercise' + }, + { + emoji: '🎯', + color: 'amber', + content: 'Learning Objective' + }, + { + emoji: '📚', + color: 'indigo', + content: 'Further Reading' + }, + { + emoji: '💬', + color: 'neutral', + content: 'Discussion Topic' + } + ] + + const getBadgeColor = (color: string) => { + switch (color) { + case 'sky': return 'bg-sky-400 text-sky-50'; + case 'green': return 'bg-green-400 text-green-50'; + case 'yellow': return 'bg-yellow-400 text-black'; + case 'red': return 'bg-red-500 text-red-50'; + case 'purple': return 'bg-purple-400 text-purple-50'; + case 'pink': return 'bg-pink-400 text-pink-50'; + case 'teal': return 'bg-teal-400 text-teal-900'; + case 'amber': return 'bg-amber-600 text-amber-100'; + case 'indigo': return 'bg-indigo-400 text-indigo-50'; + case 'neutral': return 'bg-neutral-800 text-white'; + default: return 'bg-sky-400 text-white'; + } + } + + return ( + +
+
+
+ {emoji} + {isEditable && ( + + )} +
+ + + {isEditable && ( +
+ + {showColorPicker && ( +
+
+ {colors.map((c) => ( +
+
+ )} +
+ )} +
+ {isEditable && ( + + )} + {isEditable && showPredefinedCallouts && ( +
+ {predefinedBadges.map((badge, index) => ( + + ))} +
+ )} +
+ + {isEditable && showEmojiPicker && ( +
+ +
+ )} + + +
+ ) +} + +export default BadgesExtension; diff --git a/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx b/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx index 30ec82c9..f967e985 100644 --- a/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx +++ b/apps/web/components/Objects/Editor/Toolbar/ToolbarButtons.tsx @@ -16,7 +16,10 @@ import { Cuboid, FileText, ImagePlus, + Lightbulb, Sigma, + Tag, + Tags, Video, } from 'lucide-react' import { SiYoutube } from '@icons-pack/react-simple-icons' @@ -203,6 +206,21 @@ export const ToolbarButtons = ({ editor, props }: any) => { + + editor.chain().focus().insertContent({ + type: 'badge', + content: [ + { + type: 'text', + text: 'This is a Badge' + } + ] + }).run()} + > + + + ) } diff --git a/apps/web/package.json b/apps/web/package.json index 67aede28..10c0d6ee 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ "lint:fix": "eslint --fix ." }, "dependencies": { + "@emoji-mart/react": "^1.1.1", "@hocuspocus/provider": "^2.13.7", "@icons-pack/react-simple-icons": "^10.0.0", "@radix-ui/colors": "^0.1.9", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 81acb2c2..9bd75d33 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@emoji-mart/react': + specifier: ^1.1.1 + version: 1.1.1(emoji-mart@5.6.0)(react@18.3.1) '@hocuspocus/provider': specifier: ^2.13.7 version: 2.13.7(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19) @@ -329,6 +332,12 @@ packages: '@emnapi/runtime@1.3.0': resolution: {integrity: sha512-XMBySMuNZs3DM96xcJmLW4EfGnf+uGmFNjzpehMjuX5PLB5j87ar2Zc4e3PVeZ3I5g3tYtAqskB28manlF69Zw==} + '@emoji-mart/react@1.1.1': + resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} + peerDependencies: + emoji-mart: ^5.2 + react: ^16.8 || ^17 || ^18 + '@emotion/is-prop-valid@0.8.8': resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} @@ -2171,6 +2180,9 @@ packages: electron-to-chromium@1.5.36: resolution: {integrity: sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==} + emoji-mart@5.6.0: + resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4141,6 +4153,11 @@ snapshots: tslib: 2.7.0 optional: true + '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.3.1)': + dependencies: + emoji-mart: 5.6.0 + react: 18.3.1 + '@emotion/is-prop-valid@0.8.8': dependencies: '@emotion/memoize': 0.7.4 @@ -6143,6 +6160,8 @@ snapshots: electron-to-chromium@1.5.36: {} + emoji-mart@5.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {}