mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add Flipcard component support with configuration and styling for enhanced interactivity
This commit is contained in:
parent
0b02bb4a76
commit
98dfad76aa
6 changed files with 465 additions and 0 deletions
|
|
@ -27,6 +27,7 @@ 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'
|
import Badges from '@components/Objects/Editor/Extensions/Badges/Badges'
|
||||||
import Buttons from '@components/Objects/Editor/Extensions/Buttons/Buttons'
|
import Buttons from '@components/Objects/Editor/Extensions/Buttons/Buttons'
|
||||||
|
import Flipcard from '@components/Objects/Editor/Extensions/Flipcard/Flipcard'
|
||||||
import Table from '@tiptap/extension-table'
|
import Table from '@tiptap/extension-table'
|
||||||
import TableHeader from '@tiptap/extension-table-header'
|
import TableHeader from '@tiptap/extension-table-header'
|
||||||
import TableRow from '@tiptap/extension-table-row'
|
import TableRow from '@tiptap/extension-table-row'
|
||||||
|
|
@ -136,6 +137,10 @@ function Canva(props: Editor) {
|
||||||
editable: true,
|
editable: true,
|
||||||
activity: props.activity,
|
activity: props.activity,
|
||||||
}),
|
}),
|
||||||
|
Flipcard.configure({
|
||||||
|
editable: false,
|
||||||
|
activity: props.activity,
|
||||||
|
}),
|
||||||
TableRow,
|
TableRow,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import { getUriWithOrg } from '@services/config/config'
|
||||||
import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects'
|
import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects'
|
||||||
import Badges from './Extensions/Badges/Badges'
|
import Badges from './Extensions/Badges/Badges'
|
||||||
import Buttons from './Extensions/Buttons/Buttons'
|
import Buttons from './Extensions/Buttons/Buttons'
|
||||||
|
import Flipcard from './Extensions/Flipcard/Flipcard'
|
||||||
import { useMediaQuery } from 'usehooks-ts'
|
import { useMediaQuery } from 'usehooks-ts'
|
||||||
import UserAvatar from '../UserAvatar'
|
import UserAvatar from '../UserAvatar'
|
||||||
import UserBlock from './Extensions/Users/UserBlock'
|
import UserBlock from './Extensions/Users/UserBlock'
|
||||||
|
|
@ -169,6 +170,10 @@ function Editor(props: Editor) {
|
||||||
editable: true,
|
editable: true,
|
||||||
activity: props.activity,
|
activity: props.activity,
|
||||||
}),
|
}),
|
||||||
|
Flipcard.configure({
|
||||||
|
editable: true,
|
||||||
|
activity: props.activity,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
content: props.content,
|
content: props.content,
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
import { mergeAttributes, Node } from "@tiptap/core";
|
||||||
|
import FlipcardExtension from "./FlipcardExtension";
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "flipcard",
|
||||||
|
group: "block",
|
||||||
|
draggable: true,
|
||||||
|
content: "text*",
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
question: {
|
||||||
|
default: 'Click to reveal the answer',
|
||||||
|
},
|
||||||
|
answer: {
|
||||||
|
default: 'This is the answer',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
default: 'blue',
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
default: 'center',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'medium',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "flipcard-block",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["flipcard-block", mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(FlipcardExtension);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,358 @@
|
||||||
|
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import { RotateCw, Edit, AlignLeft, AlignCenter, AlignRight, Palette, Maximize2, Minimize2, Square } from 'lucide-react'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
|
|
||||||
|
const FlipcardExtension: React.FC = (props: any) => {
|
||||||
|
const [isFlipped, setIsFlipped] = useState(false)
|
||||||
|
const [question, setQuestion] = useState(props.node.attrs.question)
|
||||||
|
const [answer, setAnswer] = useState(props.node.attrs.answer)
|
||||||
|
const [color, setColor] = useState(props.node.attrs.color || 'blue')
|
||||||
|
const [alignment, setAlignment] = useState(props.node.attrs.alignment || 'center')
|
||||||
|
const [size, setSize] = useState(props.node.attrs.size || 'medium')
|
||||||
|
const [showColorPicker, setShowColorPicker] = useState(false)
|
||||||
|
const [isEditingQuestion, setIsEditingQuestion] = useState(false)
|
||||||
|
const [isEditingAnswer, setIsEditingAnswer] = useState(false)
|
||||||
|
const colorPickerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const questionInputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const answerInputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const editorState = useEditorProvider() as any
|
||||||
|
const isEditable = editorState.isEditable
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) {
|
||||||
|
setShowColorPicker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleFlip = () => {
|
||||||
|
// Allow flipping in both edit and view modes, but prevent when editing text
|
||||||
|
if (!isEditingQuestion && !isEditingAnswer) {
|
||||||
|
setIsFlipped(!isFlipped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuestionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setQuestion(e.target.value)
|
||||||
|
props.updateAttributes({
|
||||||
|
question: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnswerChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setAnswer(e.target.value)
|
||||||
|
props.updateAttributes({
|
||||||
|
answer: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAlignmentChange = (newAlignment: 'left' | 'center' | 'right') => {
|
||||||
|
setAlignment(newAlignment)
|
||||||
|
props.updateAttributes({
|
||||||
|
alignment: newAlignment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColorSelect = (selectedColor: string) => {
|
||||||
|
setColor(selectedColor)
|
||||||
|
setShowColorPicker(false)
|
||||||
|
props.updateAttributes({
|
||||||
|
color: selectedColor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = (newSize: 'small' | 'medium' | 'large') => {
|
||||||
|
setSize(newSize)
|
||||||
|
props.updateAttributes({
|
||||||
|
size: newSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAlignmentClass = () => {
|
||||||
|
switch (alignment) {
|
||||||
|
case 'left': return 'text-left justify-start';
|
||||||
|
case 'center': return 'text-center justify-center';
|
||||||
|
case 'right': return 'text-right justify-end';
|
||||||
|
default: return 'text-center justify-center';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSizeClass = () => {
|
||||||
|
switch (size) {
|
||||||
|
case 'small': return 'w-64 h-36';
|
||||||
|
case 'medium': return 'w-80 h-48';
|
||||||
|
case 'large': return 'w-96 h-60';
|
||||||
|
default: return 'w-80 h-48';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFontSizeClass = () => {
|
||||||
|
switch (size) {
|
||||||
|
case 'small': return 'text-sm';
|
||||||
|
case 'medium': return 'text-lg';
|
||||||
|
case 'large': return 'text-xl';
|
||||||
|
default: return 'text-lg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconSizeClass = () => {
|
||||||
|
switch (size) {
|
||||||
|
case 'small': return 16;
|
||||||
|
case 'medium': return 20;
|
||||||
|
case 'large': return 24;
|
||||||
|
default: return 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCardColor = (color: string, isBack: boolean = false) => {
|
||||||
|
const baseColors = {
|
||||||
|
sky: isBack ? 'bg-sky-600 border-sky-700' : 'bg-sky-500 border-sky-600',
|
||||||
|
green: isBack ? 'bg-green-600 border-green-700' : 'bg-green-500 border-green-600',
|
||||||
|
yellow: isBack ? 'bg-yellow-600 border-yellow-700' : 'bg-yellow-500 border-yellow-600',
|
||||||
|
red: isBack ? 'bg-red-600 border-red-700' : 'bg-red-500 border-red-600',
|
||||||
|
purple: isBack ? 'bg-purple-600 border-purple-700' : 'bg-purple-500 border-purple-600',
|
||||||
|
teal: isBack ? 'bg-teal-600 border-teal-700' : 'bg-teal-500 border-teal-600',
|
||||||
|
amber: isBack ? 'bg-amber-600 border-amber-700' : 'bg-amber-500 border-amber-600',
|
||||||
|
indigo: isBack ? 'bg-indigo-600 border-indigo-700' : 'bg-indigo-500 border-indigo-600',
|
||||||
|
neutral: isBack ? 'bg-neutral-700 border-neutral-800' : 'bg-neutral-600 border-neutral-700',
|
||||||
|
blue: isBack ? 'bg-blue-600 border-blue-700' : 'bg-blue-500 border-blue-600',
|
||||||
|
}
|
||||||
|
return baseColors[color as keyof typeof baseColors] || baseColors.blue
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = ['sky', 'green', 'yellow', 'red', 'purple', 'teal', 'amber', 'indigo', 'neutral', 'blue']
|
||||||
|
|
||||||
|
const handleQuestionEdit = () => {
|
||||||
|
setIsEditingQuestion(true)
|
||||||
|
setTimeout(() => questionInputRef.current?.focus(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnswerEdit = () => {
|
||||||
|
setIsEditingAnswer(true)
|
||||||
|
setTimeout(() => answerInputRef.current?.focus(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuestionBlur = () => {
|
||||||
|
setIsEditingQuestion(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnswerBlur = () => {
|
||||||
|
setIsEditingAnswer(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className={`flipcard-wrapper flex ${getAlignmentClass()} my-4`}>
|
||||||
|
<div className={`flipcard-container ${getSizeClass()} relative`}>
|
||||||
|
<div
|
||||||
|
className={`flipcard-inner cursor-pointer ${
|
||||||
|
isFlipped ? 'flipped' : ''
|
||||||
|
}`}
|
||||||
|
onClick={handleFlip}
|
||||||
|
>
|
||||||
|
{/* Front Side (Question) */}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'flipcard-front border-2 text-white p-6 nice-shadow flex flex-col items-center justify-center text-center',
|
||||||
|
getCardColor(color, false)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-3 select-none pointer-events-none">
|
||||||
|
<RotateCw size={getIconSizeClass()} className="opacity-70" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
{isEditable && isEditingQuestion ? (
|
||||||
|
<textarea
|
||||||
|
ref={questionInputRef}
|
||||||
|
value={question}
|
||||||
|
onChange={handleQuestionChange}
|
||||||
|
onBlur={handleQuestionBlur}
|
||||||
|
className="bg-white/20 backdrop-blur-sm text-white placeholder-white/70 p-2 rounded-lg w-full h-20 resize-none border-none outline-none text-center"
|
||||||
|
placeholder="Enter your question..."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={`text-center font-medium ${getFontSizeClass()} leading-relaxed flex items-center justify-center select-none`}>
|
||||||
|
<span className="select-none pointer-events-none">{question}</span>
|
||||||
|
{isEditable && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleQuestionEdit()
|
||||||
|
}}
|
||||||
|
className="ml-2 opacity-60 hover:opacity-100 flex-shrink-0 pointer-events-auto"
|
||||||
|
>
|
||||||
|
<Edit size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isEditingQuestion && (
|
||||||
|
<div className="text-xs opacity-70 mt-3 select-none pointer-events-none">Click to flip</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Back Side (Answer) */}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'flipcard-back border-2 text-white p-6 nice-shadow flex flex-col items-center justify-center text-center',
|
||||||
|
getCardColor(color, true)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center mb-3 select-none pointer-events-none">
|
||||||
|
<RotateCw size={getIconSizeClass()} className="opacity-70 rotate-180" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
{isEditable && isEditingAnswer ? (
|
||||||
|
<textarea
|
||||||
|
ref={answerInputRef}
|
||||||
|
value={answer}
|
||||||
|
onChange={handleAnswerChange}
|
||||||
|
onBlur={handleAnswerBlur}
|
||||||
|
className="bg-white/20 backdrop-blur-sm text-white placeholder-white/70 p-2 rounded-lg w-full h-20 resize-none border-none outline-none text-center"
|
||||||
|
placeholder="Enter your answer..."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={`text-center font-medium ${getFontSizeClass()} leading-relaxed flex items-center justify-center select-none`}>
|
||||||
|
<span className="select-none pointer-events-none">{answer}</span>
|
||||||
|
{isEditable && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleAnswerEdit()
|
||||||
|
}}
|
||||||
|
className="ml-2 opacity-60 hover:opacity-100 flex-shrink-0 pointer-events-auto"
|
||||||
|
>
|
||||||
|
<Edit size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isEditingAnswer && (
|
||||||
|
<div className="text-xs opacity-70 mt-3">Click to flip back</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor Controls */}
|
||||||
|
{isEditable && (
|
||||||
|
<div className="flex mt-3 space-x-1 justify-center opacity-60 hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleAlignmentChange('left')
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-md transition-colors text-xs ${alignment === 'left' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}`}
|
||||||
|
title="Align Left"
|
||||||
|
>
|
||||||
|
<AlignLeft size={12} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleAlignmentChange('center')
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-md transition-colors text-xs ${alignment === 'center' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}`}
|
||||||
|
title="Align Center"
|
||||||
|
>
|
||||||
|
<AlignCenter size={12} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleAlignmentChange('right')
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-md transition-colors text-xs ${alignment === 'right' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}`}
|
||||||
|
title="Align Right"
|
||||||
|
>
|
||||||
|
<AlignRight size={12} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Size Controls */}
|
||||||
|
<div className="w-px h-4 bg-gray-300 self-center mx-1"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSizeChange('small')
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-md transition-colors text-xs ${size === 'small' ? 'bg-green-100 text-green-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}`}
|
||||||
|
title="Small Size"
|
||||||
|
>
|
||||||
|
<Minimize2 size={12} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSizeChange('medium')
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-md transition-colors text-xs ${size === 'medium' ? 'bg-green-100 text-green-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}`}
|
||||||
|
title="Medium Size"
|
||||||
|
>
|
||||||
|
<Square size={12} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSizeChange('large')
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-md transition-colors text-xs ${size === 'large' ? 'bg-green-100 text-green-600' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'}`}
|
||||||
|
title="Large Size"
|
||||||
|
>
|
||||||
|
<Maximize2 size={12} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-gray-300 self-center mx-1"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setShowColorPicker(!showColorPicker)
|
||||||
|
}}
|
||||||
|
className="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-md transition-colors text-xs"
|
||||||
|
title="Change Color"
|
||||||
|
>
|
||||||
|
<Palette size={12} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsFlipped(!isFlipped)
|
||||||
|
}}
|
||||||
|
className="p-1.5 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-md transition-colors text-xs"
|
||||||
|
title="Preview Flip"
|
||||||
|
>
|
||||||
|
<RotateCw size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Color Picker */}
|
||||||
|
{isEditable && showColorPicker && (
|
||||||
|
<div ref={colorPickerRef} className="absolute top-full mt-2 left-1/2 transform -translate-x-1/2 p-3 bg-white rounded-lg nice-shadow z-10">
|
||||||
|
<div className="flex flex-wrap gap-2 max-w-xs">
|
||||||
|
{colors.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className={`w-8 h-8 rounded-full border-2 border-white hover:scale-110 transform transition-transform ${getCardColor(c)} ${color === c ? 'ring-2 ring-offset-2 ring-gray-400' : ''}`}
|
||||||
|
onClick={() => handleColorSelect(c)}
|
||||||
|
title={c.charAt(0).toUpperCase() + c.slice(1)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlipcardExtension
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Link2,
|
Link2,
|
||||||
MousePointerClick,
|
MousePointerClick,
|
||||||
|
RotateCw,
|
||||||
Sigma,
|
Sigma,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
|
|
@ -430,6 +431,24 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
<Globe size={15} />
|
<Globe size={15} />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
<ToolTip content={'Flipcard'}>
|
||||||
|
<ToolBtn
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().insertContent({
|
||||||
|
type: 'flipcard',
|
||||||
|
attrs: {
|
||||||
|
question: 'Click to reveal the answer',
|
||||||
|
answer: 'This is the answer',
|
||||||
|
color: 'blue',
|
||||||
|
alignment: 'center',
|
||||||
|
size: 'medium'
|
||||||
|
}
|
||||||
|
}).run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RotateCw size={15} />
|
||||||
|
</ToolBtn>
|
||||||
|
</ToolTip>
|
||||||
</ToolButtonsWrapper>
|
</ToolButtonsWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,3 +251,35 @@ layer(base);
|
||||||
@apply border-border;}
|
@apply border-border;}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;}}
|
@apply bg-background text-foreground;}}
|
||||||
|
|
||||||
|
/* Flipcard 3D Animation Styles */
|
||||||
|
.flipcard-container {
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipcard-inner {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.7s ease-in-out;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipcard-inner.flipped {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipcard-front,
|
||||||
|
.flipcard-back {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipcard-back {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue