mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add badges to editor
This commit is contained in:
parent
cc68ea2e94
commit
8965ee67c3
7 changed files with 308 additions and 0 deletions
|
|
@ -25,6 +25,7 @@ import { NoTextInput } from '@components/Objects/Editor/Extensions/NoTextInput/N
|
||||||
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
|
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
|
||||||
import AICanvaToolkit from './AI/AICanvaToolkit'
|
import AICanvaToolkit from './AI/AICanvaToolkit'
|
||||||
import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects'
|
import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects'
|
||||||
|
import Badges from '@components/Objects/Editor/Extensions/Badges/Badges'
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string
|
content: string
|
||||||
|
|
@ -90,6 +91,10 @@ function Canva(props: Editor) {
|
||||||
editable: isEditable,
|
editable: isEditable,
|
||||||
activity: props.activity,
|
activity: props.activity,
|
||||||
}),
|
}),
|
||||||
|
Badges.configure({
|
||||||
|
editable: isEditable,
|
||||||
|
activity: props.activity,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
content: props.content,
|
content: props.content,
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||||
import ActiveAvatars from './ActiveAvatars'
|
import ActiveAvatars from './ActiveAvatars'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects'
|
import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects'
|
||||||
|
import Badges from './Extensions/Badges/Badges'
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string
|
content: string
|
||||||
|
|
@ -138,6 +139,10 @@ function Editor(props: Editor) {
|
||||||
editable: true,
|
editable: true,
|
||||||
activity: props.activity,
|
activity: props.activity,
|
||||||
}),
|
}),
|
||||||
|
Badges.configure({
|
||||||
|
editable: true,
|
||||||
|
activity: props.activity,
|
||||||
|
}),
|
||||||
|
|
||||||
// Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true
|
// Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true
|
||||||
...(props.isCollabEnabledOnThisOrg ? [
|
...(props.isCollabEnabledOnThisOrg ? [
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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<HTMLDivElement>(null)
|
||||||
|
const colorPickerRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
<div className='flex space-x-2 items-center'>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'flex space-x-1 py-1.5 items-center w-fit rounded-full outline outline-2 outline-white/20 px-3.5 font-semibold nice-shadow text-sm my-2',
|
||||||
|
getBadgeColor(color)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center space-x-1">
|
||||||
|
<span className='text'>{emoji}</span>
|
||||||
|
{isEditable && (
|
||||||
|
<button onClick={() => setShowEmojiPicker(!showEmojiPicker)}>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<NodeViewContent
|
||||||
|
contentEditable={isEditable}
|
||||||
|
className="content capitalize text tracking-wide "
|
||||||
|
>
|
||||||
|
</NodeViewContent>
|
||||||
|
{isEditable && (
|
||||||
|
<div className="flex items-center justify-center space-x-2 relative">
|
||||||
|
<button onClick={() => setShowColorPicker(!showColorPicker)}>
|
||||||
|
<Palette size={14} />
|
||||||
|
</button>
|
||||||
|
{showColorPicker && (
|
||||||
|
<div ref={colorPickerRef} className="absolute left-full ml-2 p-2 bg-white rounded-full nice-shadow">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{colors.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className={`w-8 h-8 rounded-full ${getBadgeColor(c)} hover:ring-2 hover:ring-opacity-50 focus:outline-none focus:ring-2 focus:ring-opacity-50`}
|
||||||
|
onClick={() => handleColorSelect(c)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isEditable && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPredefinedCallouts(!showPredefinedCallouts)}
|
||||||
|
className="text-neutral-300 hover:text-neutral-400 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isEditable && showPredefinedCallouts && (
|
||||||
|
<div className='flex flex-wrap gap-2 absolute mt-8 bg-white/90 backdrop-blur-md p-2 rounded-lg nice-shadow'>
|
||||||
|
{predefinedBadges.map((badge, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => handlePredefinedBadgeSelect(badge)}
|
||||||
|
className={`flex text-xs items-center px-3 py-1 rounded-xl space-x-2 ${getBadgeColor(badge.color)} text-gray-600 font-bold light-shadow hover:opacity-80 transition-all duration-100 ease-linear`}
|
||||||
|
>
|
||||||
|
<span className='text-xs'>{badge.emoji}</span>
|
||||||
|
<span className="content capitalize">{badge.content}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditable && showEmojiPicker && (
|
||||||
|
<div ref={pickerRef}>
|
||||||
|
<Picker
|
||||||
|
searchPosition="top"
|
||||||
|
theme="light"
|
||||||
|
previewPosition="none"
|
||||||
|
maxFrequentRows={0}
|
||||||
|
autoFocus={false}
|
||||||
|
onEmojiSelect={handleEmojiSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</NodeViewWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BadgesExtension;
|
||||||
|
|
@ -16,7 +16,10 @@ import {
|
||||||
Cuboid,
|
Cuboid,
|
||||||
FileText,
|
FileText,
|
||||||
ImagePlus,
|
ImagePlus,
|
||||||
|
Lightbulb,
|
||||||
Sigma,
|
Sigma,
|
||||||
|
Tag,
|
||||||
|
Tags,
|
||||||
Video,
|
Video,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { SiYoutube } from '@icons-pack/react-simple-icons'
|
import { SiYoutube } from '@icons-pack/react-simple-icons'
|
||||||
|
|
@ -203,6 +206,21 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
<Cuboid size={15} />
|
<Cuboid size={15} />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
<ToolTip content={'Badges'}>
|
||||||
|
<ToolBtn
|
||||||
|
onClick={() => editor.chain().focus().insertContent({
|
||||||
|
type: 'badge',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'This is a Badge'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).run()}
|
||||||
|
>
|
||||||
|
<Tags size={15} />
|
||||||
|
</ToolBtn>
|
||||||
|
</ToolTip>
|
||||||
</ToolButtonsWrapper>
|
</ToolButtonsWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"lint:fix": "eslint --fix ."
|
"lint:fix": "eslint --fix ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@hocuspocus/provider": "^2.13.7",
|
"@hocuspocus/provider": "^2.13.7",
|
||||||
"@icons-pack/react-simple-icons": "^10.0.0",
|
"@icons-pack/react-simple-icons": "^10.0.0",
|
||||||
"@radix-ui/colors": "^0.1.9",
|
"@radix-ui/colors": "^0.1.9",
|
||||||
|
|
|
||||||
19
apps/web/pnpm-lock.yaml
generated
19
apps/web/pnpm-lock.yaml
generated
|
|
@ -8,6 +8,9 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@emoji-mart/react':
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(emoji-mart@5.6.0)(react@18.3.1)
|
||||||
'@hocuspocus/provider':
|
'@hocuspocus/provider':
|
||||||
specifier: ^2.13.7
|
specifier: ^2.13.7
|
||||||
version: 2.13.7(y-protocols@1.0.6(yjs@13.6.19))(yjs@13.6.19)
|
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':
|
'@emnapi/runtime@1.3.0':
|
||||||
resolution: {integrity: sha512-XMBySMuNZs3DM96xcJmLW4EfGnf+uGmFNjzpehMjuX5PLB5j87ar2Zc4e3PVeZ3I5g3tYtAqskB28manlF69Zw==}
|
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':
|
'@emotion/is-prop-valid@0.8.8':
|
||||||
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
||||||
|
|
||||||
|
|
@ -2171,6 +2180,9 @@ packages:
|
||||||
electron-to-chromium@1.5.36:
|
electron-to-chromium@1.5.36:
|
||||||
resolution: {integrity: sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==}
|
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:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
|
@ -4141,6 +4153,11 @@ snapshots:
|
||||||
tslib: 2.7.0
|
tslib: 2.7.0
|
||||||
optional: true
|
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':
|
'@emotion/is-prop-valid@0.8.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emotion/memoize': 0.7.4
|
'@emotion/memoize': 0.7.4
|
||||||
|
|
@ -6143,6 +6160,8 @@ snapshots:
|
||||||
|
|
||||||
electron-to-chromium@1.5.36: {}
|
electron-to-chromium@1.5.36: {}
|
||||||
|
|
||||||
|
emoji-mart@5.6.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue