mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add info callout custom extension
This commit is contained in:
parent
e6ebd195d7
commit
fe8fdd1769
10 changed files with 163 additions and 59 deletions
32
front/components/Canva/Canva.tsx
Normal file
32
front/components/Canva/Canva.tsx
Normal 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;
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
16
front/package-lock.json
generated
16
front/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue