feat: add info callout custom extension

This commit is contained in:
swve 2022-12-08 10:12:46 +01:00
parent e6ebd195d7
commit fe8fdd1769
10 changed files with 163 additions and 59 deletions

View file

@ -0,0 +1,32 @@
import React from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
// Custom Extensions
import InfoCallout from "../Editor/Extensions/Callout/Info/InfoCallout";
interface Editor {
content: string;
element: any;
//course: any;
}
function Canva(props: Editor) {
const isEditable = false;
const editor: any = useEditor({
editable: isEditable,
extensions: [
StarterKit,
// Custom Extensions
InfoCallout.configure({
editable: isEditable,
}),
],
content: props.content,
});
return <EditorContent editor={editor} />;
}
export default Canva;

View file

@ -10,8 +10,10 @@ import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image"; import Image from "next/image";
import styled from "styled-components"; import styled from "styled-components";
import { getBackendUrl } from "../../services/config"; import { getBackendUrl } from "../../services/config";
import { GlobeIcon, PaperPlaneIcon, SlashIcon } from "@radix-ui/react-icons"; import { SlashIcon } from "@radix-ui/react-icons";
import Avvvatars from "avvvatars-react"; import Avvvatars from "avvvatars-react";
// extensions
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
interface Editor { interface Editor {
content: string; content: string;
@ -26,23 +28,27 @@ function Editor(props: Editor) {
const auth: any = React.useContext(AuthContext); const auth: any = React.useContext(AuthContext);
const editor: any = useEditor({ const editor: any = useEditor({
editable: true,
extensions: [ extensions: [
StarterKit.configure({ StarterKit.configure({
// The Collaboration extension comes with its own history handling // The Collaboration extension comes with its own history handling
history: false, // history: false,
}),
InfoCallout.configure({
editable: true,
}), }),
// Register the document with Tiptap // Register the document with Tiptap
Collaboration.configure({ // Collaboration.configure({
document: props.ydoc, // document: props.ydoc,
}), // }),
// Register the collaboration cursor extension // Register the collaboration cursor extension
CollaborationCursor.configure({ // CollaborationCursor.configure({
provider: props.provider, // provider: props.provider,
user: { // user: {
name: auth.userInfo.username, // name: auth.userInfo.username,
color: "#f783ac", // color: "#f783ac",
}, // },
}), // }),
], ],
content: props.content, content: props.content,
@ -65,15 +71,13 @@ function Editor(props: Editor) {
<EditorTop> <EditorTop>
<EditorDocSection> <EditorDocSection>
<EditorInfoWrapper> <EditorInfoWrapper>
<EditorInfoLearnHouseLogo width={23} height={23} src={learnhouseIcon} alt="" /> <EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
<EditorInfoThumbnail src={`${getBackendUrl()}content/uploads/img/${props.course.course.thumbnail}`} alt=""></EditorInfoThumbnail> <EditorInfoThumbnail src={`${getBackendUrl()}content/uploads/img/${props.course.course.thumbnail}`} alt=""></EditorInfoThumbnail>
<EditorInfoDocName> <EditorInfoDocName>
{" "} {" "}
<b>{props.course.course.name}</b> <SlashIcon /> {props.element.name}{" "} <b>{props.course.course.name}</b> <SlashIcon /> {props.element.name}{" "}
</EditorInfoDocName> </EditorInfoDocName>
<EditorSaveButton onClick={() => props.setContent(editor.getJSON())}> <EditorSaveButton onClick={() => props.setContent(editor.getJSON())}>Save</EditorSaveButton>
Save
</EditorSaveButton>
</EditorInfoWrapper> </EditorInfoWrapper>
<EditorButtonsWrapper> <EditorButtonsWrapper>
<ToolbarButtons editor={editor} /> <ToolbarButtons editor={editor} />
@ -90,7 +94,6 @@ function Editor(props: Editor) {
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.99 }} initial={{ opacity: 0, scale: 0.99 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
key="modal"
transition={{ transition={{
type: "spring", type: "spring",
stiffness: 360, stiffness: 360,
@ -106,6 +109,7 @@ function Editor(props: Editor) {
</Page> </Page>
); );
} }
const Page = styled.div` const Page = styled.div`
height: 100vh; height: 100vh;
width: 100%; width: 100%;
@ -113,13 +117,13 @@ const Page = styled.div`
min-width: 100vw; min-width: 100vw;
padding-top: 30px; padding-top: 30px;
// dots background // dots background
background-image: radial-gradient(#4744446b 1px, transparent 1px), radial-gradient(#4744446b 1px, transparent 1px); background-image: radial-gradient(#4744446b 1px, transparent 1px), radial-gradient(#4744446b 1px, transparent 1px);
background-position: 0 0, 25px 25px; background-position: 0 0, 25px 25px;
background-size: 50px 50px; background-size: 50px 50px;
background-attachment: fixed; background-attachment: fixed;
background-repeat: repeat; background-repeat: repeat;
` `;
const EditorTop = styled.div` const EditorTop = styled.div`
background-color: #ffffffb8; background-color: #ffffffb8;

View file

@ -0,0 +1,27 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import InfoCalloutComponent from "./InfoCalloutComponent";
export default Node.create({
name: "calloutInfo",
group: "block",
draggable: true,
content: "inline*",
parseHTML() {
return [
{
tag: "callout-info",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return ReactNodeViewRenderer(InfoCalloutComponent);
},
});

View file

@ -0,0 +1,45 @@
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import React from "react";
import styled from "styled-components";
function InfoCalloutComponent(props: any) {
return (
<NodeViewWrapper>
<InfoCalloutWrapper contentEditable={props.extension.options.editable}>
<div> </div> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
</InfoCalloutWrapper>
</NodeViewWrapper>
);
}
const InfoCalloutWrapper = styled.div`
display: flex;
flex-direction: row;
background: #fefce8;
color: #713f11;
border: 1px solid #fff103;
border-radius: 16px;
margin: 1rem 0;
align-items: center;
padding-left: 15px;
.content {
margin: 5px;
padding: 0.5rem;
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
border-radius: 0.5rem;
}
`;
const DragHandle = styled.div`
position: absolute;
top: 0;
left: 0;
width: 1rem;
height: 100%;
cursor: move;
z-index: 1;
`;
export default InfoCalloutComponent;

View file

@ -1,5 +1,6 @@
import styled from "styled-components"; import styled from "styled-components";
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"; import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon } from "@radix-ui/react-icons";
import { AlertTriangle, Info } from "lucide-react";
export const ToolbarButtons = ({ editor }: any) => { export const ToolbarButtons = ({ editor }: any) => {
if (!editor) { if (!editor) {
@ -31,6 +32,10 @@ export const ToolbarButtons = ({ editor }: any) => {
<option value="5">Heading 5</option> <option value="5">Heading 5</option>
<option value="6">Heading 6</option> <option value="6">Heading 6</option>
</ToolSelect> </ToolSelect>
{/* TODO: fix this : toggling only works one-way */}
<ToolBtn onClick={() => editor.chain().focus().toggleNode('calloutInfo').run()} >
<AlertTriangle size={15} />
</ToolBtn>
</ToolButtonsWrapper> </ToolButtonsWrapper>
); );
}; };
@ -49,10 +54,13 @@ const ToolBtn = styled.div`
width: 25px; width: 25px;
height: 25px; height: 25px;
padding: 5px; padding: 5px;
font-size: 5px;
margin-right: 5px; margin-right: 5px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
svg{
padding: 1px;
}
&.is-active { &.is-active {
background: rgba(176, 176, 176, 0.5); background: rgba(176, 176, 176, 0.5);

View file

@ -17,6 +17,7 @@
"@tiptap/starter-kit": "^2.0.0-beta.199", "@tiptap/starter-kit": "^2.0.0-beta.199",
"avvvatars-react": "^0.4.2", "avvvatars-react": "^0.4.2",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"lucide-react": "^0.104.1",
"next": "12.3.1", "next": "12.3.1",
"react": "18.2.0", "react": "18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -3532,6 +3533,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/lucide-react": {
"version": "0.104.1",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.104.1.tgz",
"integrity": "sha512-BKvhulnLKmBj+6pqUN5ViYk4a5fabMgc4B0a4ZLUnbRqkDDWH3h7Iet6U4WbesJzjWauQrXUlEvQCe5XpFuRnw==",
"peerDependencies": {
"prop-types": "^15.7.2",
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -7577,6 +7587,12 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"lucide-react": {
"version": "0.104.1",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.104.1.tgz",
"integrity": "sha512-BKvhulnLKmBj+6pqUN5ViYk4a5fabMgc4B0a4ZLUnbRqkDDWH3h7Iet6U4WbesJzjWauQrXUlEvQCe5XpFuRnw==",
"requires": {}
},
"memoize-one": { "memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",

View file

@ -18,6 +18,7 @@
"@tiptap/starter-kit": "^2.0.0-beta.199", "@tiptap/starter-kit": "^2.0.0-beta.199",
"avvvatars-react": "^0.4.2", "avvvatars-react": "^0.4.2",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"lucide-react": "^0.104.1",
"next": "12.3.1", "next": "12.3.1",
"react": "18.2.0", "react": "18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",

View file

@ -4,12 +4,10 @@ import styled from "styled-components";
import learnhouseBigIcon from "public/learnhouse_bigicon.png"; import learnhouseBigIcon from "public/learnhouse_bigicon.png";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { PreAlphaLabel } from "../components//UI/Layout";
const Home: NextPage = () => { const Home: NextPage = () => {
return ( return (
<HomePage> <HomePage>
<PreAlphaLabel>🚧 Pre-Alpha</PreAlphaLabel>
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}

View file

@ -1,14 +1,9 @@
import Bold from "@tiptap/extension-bold";
import Document from "@tiptap/extension-document";
import Paragraph from "@tiptap/extension-paragraph";
import StarterKit from "@tiptap/starter-kit";
import Text from "@tiptap/extension-text";
import { generateHTML } from "@tiptap/html";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import Layout from "../../../../../../../components//UI/Layout"; import Layout from "../../../../../../../components//UI/Layout";
import { getElement } from "../../../../../../../services/courses/elements"; import { getElement } from "../../../../../../../services/courses/elements";
import { getBackendUrl } from "../../../../../../../services/config"; import { getBackendUrl } from "../../../../../../../services/config";
import Canva from "../../../../../../../components/Canva/Canva";
function ElementPage() { function ElementPage() {
const router = useRouter(); const router = useRouter();
@ -30,35 +25,6 @@ function ElementPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.isReady]); }, [router.isReady]);
const output = useMemo(() => {
if (router.isReady && !isLoading) {
console.log(element);
if (element.type == "dynamic") {
let content =
Object.keys(element.content).length > 0
? element.content
: {
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: "Hello world, this is a example Canva ⚡️",
},
],
},
],
};
console.log("element", content);
return generateHTML(content, [Document, StarterKit, Paragraph, Text, Bold]);
}
}
}, [element.content]);
return ( return (
<Layout> <Layout>
{isLoading ? ( {isLoading ? (
@ -69,7 +35,7 @@ function ElementPage() {
<h1>{element.name} </h1> <h1>{element.name} </h1>
<hr /> <hr />
{element.type == "dynamic" && <div dangerouslySetInnerHTML={{ __html: output } as any}></div>} {element.type == "dynamic" && <Canva content= {element.content} element={element}/>}
{/* todo : use apis & streams instead of this */} {/* todo : use apis & streams instead of this */}
{element.type == "video" && ( {element.type == "video" && (
<video controls src={`${getBackendUrl()}content/uploads/video/${element.content.video.element_id}/${element.content.video.filename}`}></video> <video controls src={`${getBackendUrl()}content/uploads/video/${element.content.video.element_id}/${element.content.video.filename}`}></video>

View file

@ -16,5 +16,12 @@
"incremental": true "incremental": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"paths": {
"@components/*": ["components/*"],
"@public/*": ["public/*"],
"@images/*": ["public/img/*"],
"@services/*": ["services/*"],
"@editor/*": ["components/Editor/*"]
},
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }