feat: format with prettier

This commit is contained in:
swve 2024-02-09 21:22:15 +01:00
parent 03fb09c3d6
commit a147ad6610
164 changed files with 11257 additions and 8154 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,77 +1,79 @@
'use client';
import React from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import learnhouseIcon from "public/learnhouse_icon.png";
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
import { motion } from "framer-motion";
import Image from "next/image";
import styled from "styled-components";
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
'use client'
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import learnhouseIcon from 'public/learnhouse_icon.png'
import { ToolbarButtons } from './Toolbar/ToolbarButtons'
import { motion } from 'framer-motion'
import Image from 'next/image'
import styled from 'styled-components'
import { DividerVerticalIcon, SlashIcon } from '@radix-ui/react-icons'
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
import {
AIEditorStateTypes,
useAIEditor,
useAIEditorDispatch,
} from '@components/Contexts/AI/AIEditorContext'
// extensions
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
import ImageBlock from "./Extensions/Image/ImageBlock";
import Youtube from "@tiptap/extension-youtube";
import VideoBlock from "./Extensions/Video/VideoBlock";
import { Eye } from "lucide-react";
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
import PDFBlock from "./Extensions/PDF/PDFBlock";
import QuizBlock from "./Extensions/Quiz/QuizBlock";
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
import Link from "next/link";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { OrderedList } from "@tiptap/extension-ordered-list";
import InfoCallout from './Extensions/Callout/Info/InfoCallout'
import WarningCallout from './Extensions/Callout/Warning/WarningCallout'
import ImageBlock from './Extensions/Image/ImageBlock'
import Youtube from '@tiptap/extension-youtube'
import VideoBlock from './Extensions/Video/VideoBlock'
import { Eye } from 'lucide-react'
import MathEquationBlock from './Extensions/MathEquation/MathEquationBlock'
import PDFBlock from './Extensions/PDF/PDFBlock'
import QuizBlock from './Extensions/Quiz/QuizBlock'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import Link from 'next/link'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { OrderedList } from '@tiptap/extension-ordered-list'
// Lowlight
import { common, createLowlight } from 'lowlight'
const lowlight = createLowlight(common)
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import css from 'highlight.js/lib/languages/css'
import js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
import python from 'highlight.js/lib/languages/python'
import java from 'highlight.js/lib/languages/java'
import { CourseProvider } from "@components/Contexts/CourseContext";
import { useSession } from "@components/Contexts/SessionContext";
import AIEditorToolkit from "./AI/AIEditorToolkit";
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
import UserAvatar from "../UserAvatar";
import { CourseProvider } from '@components/Contexts/CourseContext'
import { useSession } from '@components/Contexts/SessionContext'
import AIEditorToolkit from './AI/AIEditorToolkit'
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import UserAvatar from '../UserAvatar'
interface Editor {
content: string;
ydoc: any;
provider: any;
activity: any;
course: any;
org: any;
setContent: (content: string) => void;
content: string
ydoc: any
provider: any
activity: any
course: any
org: any
setContent: (content: string) => void
}
function Editor(props: Editor) {
const session = useSession() as any;
const dispatchAIEditor = useAIEditorDispatch() as any;
const aiEditorState = useAIEditor() as AIEditorStateTypes;
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
const session = useSession() as any
const dispatchAIEditor = useAIEditorDispatch() as any
const aiEditorState = useAIEditor() as AIEditorStateTypes
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
React.useEffect(() => {
if (is_ai_feature_enabled) {
setIsButtonAvailable(true);
setIsButtonAvailable(true)
}
}, [is_ai_feature_enabled])
// remove course_ from course_uuid
const course_uuid = props.course.course_uuid.substring(7);
const course_uuid = props.course.course_uuid.substring(7)
// remove activity_ from activity_uuid
const activity_uuid = props.activity.activity_uuid.substring(9);
const activity_uuid = props.activity.activity_uuid.substring(9)
// Code Block Languages for Lowlight
lowlight.register('html', html)
@ -124,7 +126,6 @@ function Editor(props: Editor) {
lowlight,
}),
// Register the document with Tiptap
// Collaboration.configure({
// document: props.ydoc,
@ -140,98 +141,153 @@ function Editor(props: Editor) {
],
content: props.content,
});
})
return (
<Page>
<CourseProvider courseuuid={props.course.course_uuid}>
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
key="modal"
transition={{
type: "spring",
stiffness: 360,
damping: 70,
delay: 0.02,
}}
exit={{ opacity: 0 }}
>
<EditorTop className="fixed bg-white bg-opacity-95 backdrop-blur backdrop-brightness-125">
<EditorDocSection>
<EditorInfoWrapper>
<Link href="/">
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
</Link>
<Link target="_blank" href={`/course/${course_uuid}`}>
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
</Link>
<EditorInfoDocName>
{" "}
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
</EditorInfoDocName>
</EditorInfoWrapper>
<EditorButtonsWrapper>
<ToolbarButtons editor={editor} />
</EditorButtonsWrapper>
</EditorDocSection>
<EditorUsersSection className="space-x-2">
<div>
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer" >
{isButtonAvailable && <div
onClick={() => dispatchAIEditor({ type: aiEditorState.isModalOpen ? 'setIsModalClose' : 'setIsModalOpen' })}
<CourseProvider courseuuid={props.course.course_uuid}>
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
key="modal"
transition={{
type: 'spring',
stiffness: 360,
damping: 70,
delay: 0.02,
}}
exit={{ opacity: 0 }}
>
<EditorTop className="fixed bg-white bg-opacity-95 backdrop-blur backdrop-brightness-125">
<EditorDocSection>
<EditorInfoWrapper>
<Link href="/">
<EditorInfoLearnHouseLogo
width={25}
height={25}
src={learnhouseIcon}
alt=""
/>
</Link>
<Link target="_blank" href={`/course/${course_uuid}`}>
<EditorInfoThumbnail
src={`${getCourseThumbnailMediaDirectory(
props.org?.org_uuid,
props.course.course_uuid,
props.course.thumbnail_image
)}`}
alt=""
></EditorInfoThumbnail>
</Link>
<EditorInfoDocName>
{' '}
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{' '}
</EditorInfoDocName>
</EditorInfoWrapper>
<EditorButtonsWrapper>
<ToolbarButtons editor={editor} />
</EditorButtonsWrapper>
</EditorDocSection>
<EditorUsersSection className="space-x-2">
<div>
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer">
{isButtonAvailable && (
<div
onClick={() =>
dispatchAIEditor({
type: aiEditorState.isModalOpen
? 'setIsModalClose'
: 'setIsModalOpen',
})
}
style={{
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
background:
'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
}}
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
{" "}
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105"
>
{' '}
<i>
<Image className='' width={20} src={learnhouseAI_icon} alt="" />
</i>{" "}
<Image
className=""
width={20}
src={learnhouseAI_icon}
alt=""
/>
</i>{' '}
<i className="not-italic text-xs font-bold">AI Editor</i>
</div>}
</div>
</div>
)}
</div>
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
<EditorLeftOptionsSection className="space-x-2 ">
<div className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer" onClick={() => props.setContent(editor.getJSON())}> Save </div>
<ToolTip content="Preview">
<Link target="_blank" href={`/course/${course_uuid}/activity/${activity_uuid}`}>
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
<Eye className="mx-auto items-center" size={15} />
</div>
</Link>
</ToolTip>
</EditorLeftOptionsSection>
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
</div>
<DividerVerticalIcon
style={{
marginTop: 'auto',
marginBottom: 'auto',
color: 'grey',
opacity: '0.5',
}}
/>
<EditorLeftOptionsSection className="space-x-2 ">
<div
className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer"
onClick={() => props.setContent(editor.getJSON())}
>
{' '}
Save{' '}
</div>
<ToolTip content="Preview">
<Link
target="_blank"
href={`/course/${course_uuid}/activity/${activity_uuid}`}
>
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
<Eye className="mx-auto items-center" size={15} />
</div>
</Link>
</ToolTip>
</EditorLeftOptionsSection>
<DividerVerticalIcon
style={{
marginTop: 'auto',
marginBottom: 'auto',
color: 'grey',
opacity: '0.5',
}}
/>
<EditorUserProfileWrapper>
{!session.isAuthenticated && <span>Loading</span>}
{session.isAuthenticated && <UserAvatar width={40} border="border-4" rounded="rounded-full"/>}
</EditorUserProfileWrapper>
</EditorUsersSection>
</EditorTop>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.99 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
type: "spring",
stiffness: 360,
damping: 70,
delay: 0.5,
}}
exit={{ opacity: 0 }}
>
<EditorContentWrapper>
<AIEditorToolkit activity={props.activity} editor={editor} />
<EditorContent editor={editor} />
</EditorContentWrapper>
</motion.div>
</CourseProvider>
<EditorUserProfileWrapper>
{!session.isAuthenticated && <span>Loading</span>}
{session.isAuthenticated && (
<UserAvatar
width={40}
border="border-4"
rounded="rounded-full"
/>
)}
</EditorUserProfileWrapper>
</EditorUsersSection>
</EditorTop>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.99 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
type: 'spring',
stiffness: 360,
damping: 70,
delay: 0.5,
}}
exit={{ opacity: 0 }}
>
<EditorContentWrapper>
<AIEditorToolkit activity={props.activity} editor={editor} />
<EditorContent editor={editor} />
</EditorContentWrapper>
</motion.div>
</CourseProvider>
</Page>
);
)
}
const Page = styled.div`
@ -240,12 +296,15 @@ const Page = styled.div`
padding-top: 30px;
// dots background
background-image: radial-gradient(#4744446b 1px, transparent 1px), radial-gradient(#4744446b 1px, transparent 1px);
background-position: 0 0, 25px 25px;
background-image: radial-gradient(#4744446b 1px, transparent 1px),
radial-gradient(#4744446b 1px, transparent 1px);
background-position:
0 0,
25px 25px;
background-size: 50px 50px;
background-attachment: fixed;
background-repeat: repeat;
`;
`
const EditorTop = styled.div`
border-radius: 15px;
@ -259,35 +318,34 @@ const EditorTop = styled.div`
position: fixed;
z-index: 303;
width: -webkit-fill-available;
`;
`
// Inside EditorTop
const EditorDocSection = styled.div`
display: flex;
flex-direction: column;
`;
`
const EditorUsersSection = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`;
`
const EditorLeftOptionsSection = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`;
`
// Inside EditorDocSection
const EditorInfoWrapper = styled.div`
display: flex;
flex-direction: row;
margin-bottom: 5px;
`;
const EditorButtonsWrapper = styled.div``;
`
const EditorButtonsWrapper = styled.div``
// Inside EditorUsersSection
const EditorUserProfileWrapper = styled.div`
@ -295,14 +353,14 @@ const EditorUserProfileWrapper = styled.div`
svg {
border-radius: 7px;
}
`;
`
// Inside EditorInfoWrapper
//..todo
const EditorInfoLearnHouseLogo = styled(Image)`
border-radius: 6px;
margin-right: 0px;
`;
`
const EditorInfoDocName = styled.div`
font-size: 16px;
justify-content: center;
@ -317,8 +375,7 @@ const EditorInfoDocName = styled.div`
padding: 3px;
color: #353535;
}
`;
`
const EditorInfoThumbnail = styled.img`
height: 25px;
@ -331,7 +388,7 @@ const EditorInfoThumbnail = styled.img`
&:hover {
cursor: pointer;
}
`;
`
export const EditorContentWrapper = styled.div`
margin: 40px;
@ -344,9 +401,8 @@ export const EditorContentWrapper = styled.div`
// disable chrome outline
.ProseMirror {
h1 {
font-size: 30px;
font-size: 30px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
@ -393,72 +449,71 @@ export const EditorContentWrapper = styled.div`
// Code Block
pre {
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
iframe {
@ -472,15 +527,12 @@ export const EditorContentWrapper = styled.div`
outline: 0px solid transparent;
}
ul, ol {
ul,
ol {
padding: 0 1rem;
padding-left: 20px;
list-style-type: decimal;
}
`
`;
export default Editor;
export default Editor

View file

@ -1,61 +1,66 @@
'use client';
import { default as React, } from "react";
import * as Y from "yjs";
import Editor from "./Editor";
import { updateActivity } from "@services/courses/activities";
import { toast } from "react-hot-toast";
import Toast from "@components/StyledElements/Toast/Toast";
import { OrgProvider } from "@components/Contexts/OrgContext";
'use client'
import { default as React } from 'react'
import * as Y from 'yjs'
import Editor from './Editor'
import { updateActivity } from '@services/courses/activities'
import { toast } from 'react-hot-toast'
import Toast from '@components/StyledElements/Toast/Toast'
import { OrgProvider } from '@components/Contexts/OrgContext'
interface EditorWrapperProps {
content: string;
activity: any;
content: string
activity: any
course: any
org: any;
org: any
}
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
// A new Y document
const ydoc = new Y.Doc();
const [providerState, setProviderState] = React.useState<any>({});
const [ydocState, setYdocState] = React.useState<any>({});
const [isLoading, setIsLoading] = React.useState(true);
const ydoc = new Y.Doc()
const [providerState, setProviderState] = React.useState<any>({})
const [ydocState, setYdocState] = React.useState<any>({})
const [isLoading, setIsLoading] = React.useState(true)
function createRTCProvider() {
// const provider = new WebrtcProvider(props.activity.activity_id, ydoc);
// setYdocState(ydoc);
// setProviderState(provider);
setIsLoading(false);
setIsLoading(false)
}
async function setContent(content: any) {
let activity = props.activity;
activity.content = content;
let activity = props.activity
activity.content = content
toast.promise(
updateActivity(activity, activity.activity_uuid),
{
loading: 'Saving...',
success: <b>Activity saved!</b>,
error: <b>Could not save.</b>,
}
);
toast.promise(updateActivity(activity, activity.activity_uuid), {
loading: 'Saving...',
success: <b>Activity saved!</b>,
error: <b>Could not save.</b>,
})
}
if (isLoading) {
createRTCProvider();
return <div>Loading...</div>;
createRTCProvider()
return <div>Loading...</div>
} else {
return <>
<Toast></Toast>
<OrgProvider orgslug={props.org.slug}>
<Editor org={props.org} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
</OrgProvider>
</>
return (
<>
<Toast></Toast>
<OrgProvider orgslug={props.org.slug}>
<Editor
org={props.org}
course={props.course}
activity={props.activity}
content={props.content}
setContent={setContent}
provider={providerState}
ydoc={ydocState}
></Editor>
;
</OrgProvider>
</>
)
}
}
export default EditorWrapper;
export default EditorWrapper

View file

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

View file

@ -1,35 +1,38 @@
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import { AlertCircle } from "lucide-react";
import React from "react";
import styled from "styled-components";
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import { AlertCircle } from 'lucide-react'
import React from 'react'
import styled from 'styled-components'
function InfoCalloutComponent(props: any) {
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
return (
<NodeViewWrapper>
<InfoCalloutWrapper className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner" contentEditable={isEditable}>
<AlertCircle /> <NodeViewContent contentEditable={isEditable} className="content" />
<InfoCalloutWrapper
className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner"
contentEditable={isEditable}
>
<AlertCircle />{' '}
<NodeViewContent contentEditable={isEditable} className="content" />
</InfoCalloutWrapper>
</NodeViewWrapper>
);
)
}
const InfoCalloutWrapper = styled.div`
svg{
svg {
padding: 3px;
}
.content {
margin: 5px;
padding: 0.5rem;
border: ${(props) => (props.contentEditable ? "2px dashed #1f3a8a12" : "none")};
border: ${(props) =>
props.contentEditable ? '2px dashed #1f3a8a12' : 'none'};
border-radius: 0.5rem;
}
`;
`
export default InfoCalloutComponent;
export default InfoCalloutComponent

View file

@ -1,29 +1,29 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import WarningCalloutComponent from "./WarningCalloutComponent";
import WarningCalloutComponent from './WarningCalloutComponent'
export default Node.create({
name: "calloutWarning",
group: "block",
name: 'calloutWarning',
group: 'block',
draggable: true,
content: "text*",
content: 'text*',
// TODO : multi line support
parseHTML() {
return [
{
tag: "callout-warning",
tag: 'callout-warning',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(WarningCalloutComponent);
return ReactNodeViewRenderer(WarningCalloutComponent)
},
});
})

View file

@ -1,25 +1,27 @@
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import { AlertTriangle } from "lucide-react";
import React from "react";
import styled from "styled-components";
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import { AlertTriangle } from 'lucide-react'
import React from 'react'
import styled from 'styled-components'
function WarningCalloutComponent(props: any) {
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
return (
<NodeViewWrapper>
<CalloutWrapper className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner" contentEditable={isEditable}>
<AlertTriangle /> <NodeViewContent contentEditable={isEditable} className="content" />
<CalloutWrapper
className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner"
contentEditable={isEditable}
>
<AlertTriangle />{' '}
<NodeViewContent contentEditable={isEditable} className="content" />
</CalloutWrapper>
</NodeViewWrapper>
);
)
}
const CalloutWrapper = styled.div`
svg {
padding: 3px;
}
@ -27,10 +29,11 @@ const CalloutWrapper = styled.div`
.content {
margin: 5px;
padding: 0.5rem;
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
border: ${(props) =>
props.contentEditable ? '2px dashed #713f1117' : 'none'};
border-radius: 0.5rem;
}
`;
`
const DragHandle = styled.div`
position: absolute;
@ -40,6 +43,6 @@ const DragHandle = styled.div`
height: 100%;
cursor: move;
z-index: 1;
`;
`
export default WarningCalloutComponent;
export default WarningCalloutComponent

View file

@ -1,11 +1,11 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import ImageBlockComponent from "./ImageBlockComponent";
import ImageBlockComponent from './ImageBlockComponent'
export default Node.create({
name: "blockImage",
group: "block",
name: 'blockImage',
group: 'block',
atom: true,
@ -17,22 +17,22 @@ export default Node.create({
size: {
width: 300,
},
};
}
},
parseHTML() {
return [
{
tag: "block-image",
tag: 'block-image',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-image", mergeAttributes(HTMLAttributes), 0];
return ['block-image', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(ImageBlockComponent);
return ReactNodeViewRenderer(ImageBlockComponent)
},
});
})

View file

@ -1,95 +1,136 @@
import { NodeViewWrapper } from "@tiptap/react";
import React, { useEffect } from "react";
import styled from "styled-components";
import { Resizable } from 're-resizable';
import { AlertTriangle, Image, Loader } from "lucide-react";
import { uploadNewImageFile } from "../../../../../services/blocks/Image/images";
import { UploadIcon } from "@radix-ui/react-icons";
import { getActivityBlockMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { useCourse } from "@components/Contexts/CourseContext";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { Resizable } from 're-resizable'
import { AlertTriangle, Image, Loader } from 'lucide-react'
import { uploadNewImageFile } from '../../../../../services/blocks/Image/images'
import { UploadIcon } from '@radix-ui/react-icons'
import { getActivityBlockMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function ImageBlockComponent(props: any) {
const org = useOrg() as any;
const course = useCourse() as any;
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const [image, setImage] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300 });
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
const org = useOrg() as any
const course = useCourse() as any
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const [image, setImage] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
props.node.attrs.blockObject
)
const [imageSize, setImageSize] = React.useState({
width: props.node.attrs.size ? props.node.attrs.size.width : 300,
})
const fileId = blockObject
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
: null
const handleImageChange = (event: React.ChangeEvent<any>) => {
setImage(event.target.files[0]);
};
setImage(event.target.files[0])
}
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
let object = await uploadNewImageFile(image, props.extension.options.activity.activity_uuid);
setIsLoading(false);
setblockObject(object);
e.preventDefault()
setIsLoading(true)
let object = await uploadNewImageFile(
image,
props.extension.options.activity.activity_uuid
)
setIsLoading(false)
setblockObject(object)
props.updateAttributes({
blockObject: object,
size: imageSize,
});
};
useEffect(() => {
})
}
, [course, org]);
useEffect(() => {}, [course, org])
return (
<NodeViewWrapper className="block-image">
{!blockObject && isEditable && (
<BlockImageWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
<BlockImageWrapper
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
contentEditable={isEditable}
>
{isLoading ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
<Loader
className="animate-spin animate-pulse text-gray-200"
size={50}
/>
) : (
<>
<div>
<Image className="text-gray-200" size={50} />
</div>
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleImageChange} type="file" name="" id="" />
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
<input
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
onChange={handleImageChange}
type="file"
name=""
id=""
/>
<button
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
onClick={handleSubmit}
>
<UploadIcon></UploadIcon>
<p>Submit</p>
</button>
</>
)}
</BlockImageWrapper>
)}
{blockObject && (
<Resizable defaultSize={{ width: imageSize.width, height: "100%" }}
<Resizable
defaultSize={{ width: imageSize.width, height: '100%' }}
handleStyles={{
right: { position: 'unset', width: 7, height: 30, borderRadius: 20, cursor: 'col-resize', backgroundColor: 'black', opacity: '0.3', margin: 'auto', marginLeft: 5 },
right: {
position: 'unset',
width: 7,
height: 30,
borderRadius: 20,
cursor: 'col-resize',
backgroundColor: 'black',
opacity: '0.3',
margin: 'auto',
marginLeft: 5,
},
}}
style={{
margin: 'auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
style={{ margin: "auto", display: "flex", justifyContent: "center", alignItems: "center", height: "100%" }}
maxWidth={1000}
minWidth={200}
onResizeStop={(e, direction, ref, d) => {
props.updateAttributes({
size: {
width: imageSize.width + d.width,
}
});
},
})
setImageSize({
width: imageSize.width + d.width,
});
})
}}
>
<img
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
src={`${getActivityBlockMediaDirectory(
org?.org_uuid,
course?.courseStructure.course_uuid,
props.extension.options.activity.activity_uuid,
blockObject.block_uuid,
blockObject ? fileId : ' ', 'imageBlock')}`}
blockObject ? fileId : ' ',
'imageBlock'
)}`}
alt=""
className="rounded-lg shadow "
/>
</Resizable>
)}
{isLoading && (
@ -98,29 +139,24 @@ function ImageBlockComponent(props: any) {
</div>
)}
</NodeViewWrapper>
);
)
}
export default ImageBlockComponent;
export default ImageBlockComponent
const BlockImageWrapper = styled.div`
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`
const BlockImage = styled.div`
display: flex;
// center
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`

View file

@ -1,35 +1,35 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import MathEquationBlockComponent from "./MathEquationBlockComponent";
import MathEquationBlockComponent from './MathEquationBlockComponent'
export default Node.create({
name: "blockMathEquation",
group: "block",
name: 'blockMathEquation',
group: 'block',
atom: true,
addAttributes() {
return {
math_equation: {
default: "",
default: '',
},
};
}
},
parseHTML() {
return [
{
tag: "block-math-equation",
tag: 'block-math-equation',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-math-equation", mergeAttributes(HTMLAttributes), 0];
return ['block-math-equation', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(MathEquationBlockComponent);
return ReactNodeViewRenderer(MathEquationBlockComponent)
},
});
})

View file

@ -1,31 +1,31 @@
import { NodeViewWrapper } from "@tiptap/react";
import React from "react";
import styled from "styled-components";
import "katex/dist/katex.min.css";
import { BlockMath } from "react-katex";
import { Save } from "lucide-react";
import Link from "next/link";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import React from 'react'
import styled from 'styled-components'
import 'katex/dist/katex.min.css'
import { BlockMath } from 'react-katex'
import { Save } from 'lucide-react'
import Link from 'next/link'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function MathEquationBlockComponent(props: any) {
const [equation, setEquation] = React.useState(props.node.attrs.math_equation);
const [isEditing, setIsEditing] = React.useState(true);
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const [equation, setEquation] = React.useState(props.node.attrs.math_equation)
const [isEditing, setIsEditing] = React.useState(true)
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const handleEquationChange = (event: React.ChangeEvent<any>) => {
setEquation(event.target.value);
setEquation(event.target.value)
props.updateAttributes({
math_equation: equation,
});
};
})
}
const saveEquation = () => {
props.updateAttributes({
math_equation: equation,
});
})
//setIsEditing(false);
};
}
return (
<NodeViewWrapper className="block-math-equation">
@ -34,24 +34,38 @@ function MathEquationBlockComponent(props: any) {
{isEditing && isEditable && (
<>
<EditBar>
<input value={equation} onChange={handleEquationChange} placeholder="Insert a Math Equation (LaTeX) " type="text" />
<input
value={equation}
onChange={handleEquationChange}
placeholder="Insert a Math Equation (LaTeX) "
type="text"
/>
<button className="opacity-1" onClick={() => saveEquation()}>
<Save size={15}></Save>
</button>
</EditBar>
<span className="pt-2 text-zinc-500 text-sm">Please refer to this <Link className="text-zinc-900 after:content-['↗']" href="https://katex.org/docs/supported.html" target="_blank"> guide</Link> for supported TeX functions </span>
<span className="pt-2 text-zinc-500 text-sm">
Please refer to this{' '}
<Link
className="text-zinc-900 after:content-['↗']"
href="https://katex.org/docs/supported.html"
target="_blank"
>
{' '}
guide
</Link>{' '}
for supported TeX functions{' '}
</span>
</>
)}
</MathEqWrapper>
</NodeViewWrapper>
);
)
}
export default MathEquationBlockComponent;
export default MathEquationBlockComponent
const MathEqWrapper = styled.div`
`;
const MathEqWrapper = styled.div``
const EditBar = styled.div`
display: flex;
@ -82,7 +96,7 @@ const EditBar = styled.div`
font-size: 14px;
color: #494949;
width: 100%;
font-family: "DM Sans", sans-serif;
font-family: 'DM Sans', sans-serif;
padding-left: 10px;
&:focus {
outline: none;
@ -92,4 +106,4 @@ const EditBar = styled.div`
color: #49494936;
}
}
`;
`

View file

@ -1,21 +1,29 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
export const NoTextInput = Extension.create({
name: 'noTextInput',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('noTextInput'),
filterTransaction: (transaction) => {
// If the transaction is adding text, stop it
return !transaction.docChanged || transaction.steps.every((step) => {
const { slice } = step.toJSON();
return !slice || !slice.content.some((node: { type: string; }) => node.type === 'text');
});
},
}),
];
},
});
name: 'noTextInput',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('noTextInput'),
filterTransaction: (transaction) => {
// If the transaction is adding text, stop it
return (
!transaction.docChanged ||
transaction.steps.every((step) => {
const { slice } = step.toJSON()
return (
!slice ||
!slice.content.some(
(node: { type: string }) => node.type === 'text'
)
)
})
)
},
}),
]
},
})

View file

@ -1,11 +1,11 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import PDFBlockComponent from "./PDFBlockComponent";
import PDFBlockComponent from './PDFBlockComponent'
export default Node.create({
name: "blockPDF",
group: "block",
name: 'blockPDF',
group: 'block',
atom: true,
@ -14,22 +14,22 @@ export default Node.create({
blockObject: {
default: null,
},
};
}
},
parseHTML() {
return [
{
tag: "block-pdf",
tag: 'block-pdf',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-pdf", mergeAttributes(HTMLAttributes), 0];
return ['block-pdf', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(PDFBlockComponent);
return ReactNodeViewRenderer(PDFBlockComponent)
},
});
})

View file

@ -1,56 +1,79 @@
import { NodeViewWrapper } from "@tiptap/react";
import React, { useEffect } from "react";
import styled from "styled-components";
import { AlertTriangle, FileText, Loader } from "lucide-react";
import { uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
import { UploadIcon } from "@radix-ui/react-icons";
import { getActivityBlockMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { useCourse } from "@components/Contexts/CourseContext";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { AlertTriangle, FileText, Loader } from 'lucide-react'
import { uploadNewPDFFile } from '../../../../../services/blocks/Pdf/pdf'
import { UploadIcon } from '@radix-ui/react-icons'
import { getActivityBlockMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function PDFBlockComponent(props: any) {
const org = useOrg() as any;
const course = useCourse() as any;
const [pdf, setPDF] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const org = useOrg() as any
const course = useCourse() as any
const [pdf, setPDF] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
props.node.attrs.blockObject
)
const fileId = blockObject
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
: null
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const handlePDFChange = (event: React.ChangeEvent<any>) => {
setPDF(event.target.files[0]);
};
setPDF(event.target.files[0])
}
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
let object = await uploadNewPDFFile(pdf, props.extension.options.activity.activity_uuid);
setIsLoading(false);
setblockObject(object);
e.preventDefault()
setIsLoading(true)
let object = await uploadNewPDFFile(
pdf,
props.extension.options.activity.activity_uuid
)
setIsLoading(false)
setblockObject(object)
props.updateAttributes({
blockObject: object,
});
};
useEffect(() => {
})
}
, [course, org]);
useEffect(() => {}, [course, org])
return (
<NodeViewWrapper className="block-pdf">
{!blockObject && (
<BlockPDFWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
<BlockPDFWrapper
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
contentEditable={isEditable}
>
{isLoading ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
<Loader
className="animate-spin animate-pulse text-gray-200"
size={50}
/>
) : (
<>
<div>
<FileText className="text-gray-200" size={50} />
</div>
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handlePDFChange} type="file" name="" id="" />
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
<input
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
onChange={handlePDFChange}
type="file"
name=""
id=""
/>
<button
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
onClick={handleSubmit}
>
<UploadIcon></UploadIcon>
<p>Submit</p>
</button>
</>
)}
</BlockPDFWrapper>
@ -59,11 +82,14 @@ function PDFBlockComponent(props: any) {
<BlockPDF>
<iframe
className="shadow rounded-lg h-96 w-full object-scale-down bg-black"
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
src={`${getActivityBlockMediaDirectory(
org?.org_uuid,
course?.courseStructure.course_uuid,
props.extension.options.activity.activity_uuid,
blockObject.block_uuid,
blockObject ? fileId : ' ', 'pdfBlock')}`}
blockObject ? fileId : ' ',
'pdfBlock'
)}`}
/>
</BlockPDF>
)}
@ -73,19 +99,18 @@ function PDFBlockComponent(props: any) {
</div>
)}
</NodeViewWrapper>
);
)
}
export default PDFBlockComponent;
export default PDFBlockComponent
const BlockPDFWrapper = styled.div`
// center
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`
const BlockPDF = styled.div`
display: flex;
@ -97,5 +122,5 @@ const BlockPDF = styled.div`
// cover
object-fit: cover;
}
`;
const PDFNotFound = styled.div``;
`
const PDFNotFound = styled.div``

View file

@ -1,11 +1,11 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import QuizBlockComponent from "./QuizBlockComponent";
import QuizBlockComponent from './QuizBlockComponent'
export default Node.create({
name: "blockQuiz",
group: "block",
name: 'blockQuiz',
group: 'block',
atom: true,
addAttributes() {
@ -16,22 +16,22 @@ export default Node.create({
questions: {
default: [],
},
};
}
},
parseHTML() {
return [
{
tag: "block-quiz",
tag: 'block-quiz',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-quiz", mergeAttributes(HTMLAttributes), 0];
return ['block-quiz', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(QuizBlockComponent);
return ReactNodeViewRenderer(QuizBlockComponent)
},
});
})

View file

@ -1,188 +1,206 @@
import { NodeViewWrapper } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
import { NodeViewWrapper } from '@tiptap/react'
import { v4 as uuidv4 } from 'uuid'
import { twMerge } from 'tailwind-merge'
import React from "react";
import { BadgeHelp, Check, Minus, Plus, RefreshCcw } from "lucide-react";
import ReactConfetti from "react-confetti";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import React from 'react'
import { BadgeHelp, Check, Minus, Plus, RefreshCcw } from 'lucide-react'
import ReactConfetti from 'react-confetti'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
interface Answer {
answer_id: string;
answer: string;
correct: boolean;
answer_id: string
answer: string
correct: boolean
}
interface Question {
question_id: string;
question: string;
type: "multiple_choice" | 'custom_answer'
answers: Answer[];
question_id: string
question: string
type: 'multiple_choice' | 'custom_answer'
answers: Answer[]
}
function QuizBlockComponent(props: any) {
const [questions, setQuestions] = React.useState(props.node.attrs.questions) as [Question[], any];
const [userAnswers, setUserAnswers] = React.useState([]) as [any[], any];
const [submitted, setSubmitted] = React.useState(false) as [boolean, any];
const [submissionMessage, setSubmissionMessage] = React.useState("") as [string, any];
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const [questions, setQuestions] = React.useState(
props.node.attrs.questions
) as [Question[], any]
const [userAnswers, setUserAnswers] = React.useState([]) as [any[], any]
const [submitted, setSubmitted] = React.useState(false) as [boolean, any]
const [submissionMessage, setSubmissionMessage] = React.useState('') as [
string,
any,
]
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const handleAnswerClick = (question_id: string, answer_id: string) => {
// if the quiz is submitted, do nothing
if (submitted) {
return;
return
}
const userAnswer = {
question_id: question_id,
answer_id: answer_id
answer_id: answer_id,
}
const newAnswers = [...userAnswers, userAnswer];
const newAnswers = [...userAnswers, userAnswer]
// only accept one answer per question
const filteredAnswers = newAnswers.filter((answer: any) => answer.question_id !== question_id);
setUserAnswers([...filteredAnswers, userAnswer]);
const filteredAnswers = newAnswers.filter(
(answer: any) => answer.question_id !== question_id
)
setUserAnswers([...filteredAnswers, userAnswer])
}
const refreshUserSubmission = () => {
setUserAnswers([]);
setSubmitted(false);
setUserAnswers([])
setSubmitted(false)
}
const handleUserSubmission = () => {
if (userAnswers.length === 0) {
setSubmissionMessage("Please answer at least one question!");
return;
setSubmissionMessage('Please answer at least one question!')
return
}
setSubmitted(true);
setSubmitted(true)
// check if all submitted answers are correct
const correctAnswers = questions.map((question: Question) => {
const correctAnswer: any = question.answers.find((answer: Answer) => answer.correct);
const userAnswer = userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id);
const correctAnswer: any = question.answers.find(
(answer: Answer) => answer.correct
)
const userAnswer = userAnswers.find(
(userAnswer: any) => userAnswer.question_id === question.question_id
)
if (correctAnswer.answer_id === userAnswer.answer_id) {
return true;
return true
} else {
return false;
return false
}
});
})
// check if all answers are correct
const allCorrect = correctAnswers.every((answer: boolean) => answer === true);
const allCorrect = correctAnswers.every(
(answer: boolean) => answer === true
)
if (allCorrect) {
setSubmissionMessage("All answers are correct!");
console.log("All answers are correct!");
setSubmissionMessage('All answers are correct!')
console.log('All answers are correct!')
} else {
setSubmissionMessage('Some answers are incorrect!')
console.log('Some answers are incorrect!')
}
else {
setSubmissionMessage("Some answers are incorrect!");
console.log("Some answers are incorrect!");
}
}
const getAnswerID = (answerIndex: number, questionId : string) => {
const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode('A'.charCodeAt(0) + i));
let alphabetID = alphabet[answerIndex];
const getAnswerID = (answerIndex: number, questionId: string) => {
const alphabet = Array.from({ length: 26 }, (_, i) =>
String.fromCharCode('A'.charCodeAt(0) + i)
)
let alphabetID = alphabet[answerIndex]
// Get question index
const questionIndex = questions.findIndex((question: Question) => question.question_id === questionId);
let questionID = questionIndex + 1;
const questionIndex = questions.findIndex(
(question: Question) => question.question_id === questionId
)
let questionID = questionIndex + 1
return `${alphabetID}`;
return `${alphabetID}`
}
const saveQuestions = (questions: any) => {
props.updateAttributes({
questions: questions,
});
setQuestions(questions);
};
})
setQuestions(questions)
}
const addSampleQuestion = () => {
const newQuestion = {
question_id: uuidv4(),
question: "",
type: "multiple_choice",
question: '',
type: 'multiple_choice',
answers: [
{
answer_id: uuidv4(),
answer: "",
correct: false
answer: '',
correct: false,
},
]
],
}
setQuestions([...questions, newQuestion]);
setQuestions([...questions, newQuestion])
}
const addAnswer = (question_id: string) => {
const newAnswer = {
answer_id: uuidv4(),
answer: "",
correct: false
answer: '',
correct: false,
}
// check if there is already more thqn 5 answers
const question: any = questions.find((question: Question) => question.question_id === question_id);
const question: any = questions.find(
(question: Question) => question.question_id === question_id
)
if (question.answers.length >= 5) {
return;
return
}
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers.push(newAnswer);
question.answers.push(newAnswer)
}
return question;
});
return question
})
saveQuestions(newQuestions);
saveQuestions(newQuestions)
}
const changeAnswerValue = (question_id: string, answer_id: string, value: string) => {
const changeAnswerValue = (
question_id: string,
answer_id: string,
value: string
) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers.map((answer: Answer) => {
if (answer.answer_id === answer_id) {
answer.answer = value;
answer.answer = value
}
return answer;
});
return answer
})
}
return question;
});
saveQuestions(newQuestions);
return question
})
saveQuestions(newQuestions)
}
const changeQuestionValue = (question_id: string, value: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.question = value;
question.question = value
}
return question;
});
saveQuestions(newQuestions);
return question
})
saveQuestions(newQuestions)
}
const deleteQuestion = (question_id: string) => {
const newQuestions = questions.filter((question: Question) => question.question_id !== question_id);
saveQuestions(newQuestions);
const newQuestions = questions.filter(
(question: Question) => question.question_id !== question_id
)
saveQuestions(newQuestions)
}
const deleteAnswer = (question_id: string, answer_id: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers = question.answers.filter((answer: Answer) => answer.answer_id !== answer_id);
question.answers = question.answers.filter(
(answer: Answer) => answer.answer_id !== answer_id
)
}
return question;
});
saveQuestions(newQuestions);
return question
})
saveQuestions(newQuestions)
}
const markAnswerCorrect = (question_id: string, answer_id: string) => {
@ -190,54 +208,68 @@ function QuizBlockComponent(props: any) {
if (question.question_id === question_id) {
question.answers.map((answer: Answer) => {
if (answer.answer_id === answer_id) {
answer.correct = true;
answer.correct = true
} else {
answer.correct = false;
answer.correct = false
}
return answer;
});
return answer
})
}
return question;
});
saveQuestions(newQuestions);
return question
})
saveQuestions(newQuestions)
}
return (
<NodeViewWrapper className="block-quiz">
<div
//style={{ background: "radial-gradient(152.15% 150.08% at 56.45% -6.67%, rgba(180, 255, 250, 0.10) 5.53%, rgba(202, 201, 255, 0.10) 66.76%)" }}
className="rounded-xl px-5 py-2 bg-slate-100 transition-all ease-linear"
>
<div className="flex space-x-2 pt-1 items-center text-sm overflow-hidden">
{(submitted && submissionMessage == "All answers are correct!") &&
{submitted && submissionMessage == 'All answers are correct!' && (
<ReactConfetti
numberOfPieces={submitted ? 1400 : 0}
recycle={false}
className="w-full h-screen"
/>
}
)}
<div className="flex space-x-2 items-center text-sm">
<BadgeHelp className='text-slate-400' size={15} />
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">Quiz</p>
<BadgeHelp className="text-slate-400" size={15} />
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">
Quiz
</p>
</div>
<div className="grow flex items-center justify-center">
</div>
{isEditable ?
<div className="grow flex items-center justify-center"></div>
{isEditable ? (
<div>
<button onClick={addSampleQuestion} className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs">Add Question</button>
<button
onClick={addSampleQuestion}
className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs"
>
Add Question
</button>
</div>
:
) : (
<div className="flex space-x-1 items-center">
<div onClick={() => refreshUserSubmission()} className="cursor-pointer px-2">
<RefreshCcw className='text-slate-400 cursor-pointer' size={15} />
<div
onClick={() => refreshUserSubmission()}
className="cursor-pointer px-2"
>
<RefreshCcw
className="text-slate-400 cursor-pointer"
size={15}
/>
</div>
<button onClick={() => handleUserSubmission()} className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs">Submit</button>
<button
onClick={() => handleUserSubmission()}
className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs"
>
Submit
</button>
</div>
}
)}
</div>
{questions.map((question: Question) => (
@ -245,20 +277,32 @@ function QuizBlockComponent(props: any) {
<div className="question">
<div className="flex space-x-2 items-center">
<div className="flex-grow">
{isEditable ?
<input value={question.question} placeholder="Your Question" onChange={(e) => changeQuestionValue(question.question_id, e.target.value)} className="text-slate-800 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-md font-bold w-full"></input>
:
<p className="text-slate-800 bg-[#00008b00] rounded-md text-md font-bold w-full">{question.question}</p>
}
{isEditable ? (
<input
value={question.question}
placeholder="Your Question"
onChange={(e) =>
changeQuestionValue(
question.question_id,
e.target.value
)
}
className="text-slate-800 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-md font-bold w-full"
></input>
) : (
<p className="text-slate-800 bg-[#00008b00] rounded-md text-md font-bold w-full">
{question.question}
</p>
)}
</div>
{isEditable &&
{isEditable && (
<div
onClick={() => deleteQuestion(question.question_id)}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer">
<Minus
className="mx-auto text-slate-400" size={12} />
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
>
<Minus className="mx-auto text-slate-400" size={12} />
</div>
}
)}
</div>
<div className="answers flex py-2 space-x-3">
{question.answers.map((answer: Answer) => (
@ -266,60 +310,119 @@ function QuizBlockComponent(props: any) {
key={answer.answer_id}
className={twMerge(
'outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] bg-opacity-50 hover:bg-opacity-100 hover:shadow-md rounded-s rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear',
answer.correct && isEditable ? 'outline-lime-300' : 'outline-white',
userAnswers.find((userAnswer: any) => (userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) && !isEditable) ? 'outline-slate-300' : '',
(submitted && answer.correct) ? 'outline-lime-300 text-lime' : '',
(submitted && !answer.correct) && userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) ? 'outline-red-400' : '',
)
answer.correct && isEditable
? 'outline-lime-300'
: 'outline-white',
userAnswers.find(
(userAnswer: any) =>
userAnswer.question_id === question.question_id &&
userAnswer.answer_id === answer.answer_id &&
!isEditable
)
? 'outline-slate-300'
: '',
submitted && answer.correct
? 'outline-lime-300 text-lime'
: '',
submitted &&
!answer.correct &&
userAnswers.find(
(userAnswer: any) =>
userAnswer.question_id === question.question_id &&
userAnswer.answer_id === answer.answer_id
)
? 'outline-red-400'
: ''
)}
onClick={() =>
handleAnswerClick(question.question_id, answer.answer_id)
}
onClick={() => handleAnswerClick(question.question_id, answer.answer_id)}
>
<div className={twMerge(
"bg-white font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800",
answer.correct && isEditable ? 'bg-lime-300 text-lime-800 outline-none' : 'bg-white',
(submitted && answer.correct) ? 'bg-lime-300 text-lime-800 outline-none' : '',
(submitted && !answer.correct) && userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) ? 'bg-red-400 text-red-800 outline-none' : '',
)}>
<p className="mx-auto font-bold text-sm ">{getAnswerID(question.answers.indexOf(answer),question.question_id)}</p>
<div
className={twMerge(
'bg-white font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800',
answer.correct && isEditable
? 'bg-lime-300 text-lime-800 outline-none'
: 'bg-white',
submitted && answer.correct
? 'bg-lime-300 text-lime-800 outline-none'
: '',
submitted &&
!answer.correct &&
userAnswers.find(
(userAnswer: any) =>
userAnswer.question_id === question.question_id &&
userAnswer.answer_id === answer.answer_id
)
? 'bg-red-400 text-red-800 outline-none'
: ''
)}
>
<p className="mx-auto font-bold text-sm ">
{getAnswerID(
question.answers.indexOf(answer),
question.question_id
)}
</p>
</div>
{isEditable ?
<input value={answer.answer} onChange={(e) => changeAnswerValue(question.question_id, answer.answer_id, e.target.value)} placeholder="Answer" className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"></input>
:
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] rounded-md ext-sm font-bold">{answer.answer}</p>
}
{isEditable &&
{isEditable ? (
<input
value={answer.answer}
onChange={(e) =>
changeAnswerValue(
question.question_id,
answer.answer_id,
e.target.value
)
}
placeholder="Answer"
className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
></input>
) : (
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] rounded-md ext-sm font-bold">
{answer.answer}
</p>
)}
{isEditable && (
<div className="flex space-x-1 items-center">
<div
onClick={() => markAnswerCorrect(question.question_id, answer.answer_id)}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-lime-300 hover:bg-lime-400 transition-all ease-linear text-sm cursor-pointer ">
<Check
className="mx-auto text-lime-800" size={12} />
onClick={() =>
markAnswerCorrect(
question.question_id,
answer.answer_id
)
}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-lime-300 hover:bg-lime-400 transition-all ease-linear text-sm cursor-pointer "
>
<Check className="mx-auto text-lime-800" size={12} />
</div>
<div
onClick={() => deleteAnswer(question.question_id, answer.answer_id)}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer">
<Minus
className="mx-auto text-slate-400" size={12} />
onClick={() =>
deleteAnswer(question.question_id, answer.answer_id)
}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
>
<Minus className="mx-auto text-slate-400" size={12} />
</div>
</div>
}
)}
</div>
))}
{isEditable &&
<div onClick={() => addAnswer(question.question_id)} className="outline outline-3 w-[30px] flex-none flex items-center h-[30px] outline-white hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear">
{isEditable && (
<div
onClick={() => addAnswer(question.question_id)}
className="outline outline-3 w-[30px] flex-none flex items-center h-[30px] outline-white hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear"
>
<Plus className="mx-auto text-slate-800" size={15} />
</div>
}
)}
</div>
</div>
</div>
))}
</div>
</NodeViewWrapper>
);
)
}
export default QuizBlockComponent;
export default QuizBlockComponent

View file

@ -1,34 +1,34 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import VideoBlockComponent from "./VideoBlockComponent";
import VideoBlockComponent from './VideoBlockComponent'
export default Node.create({
name: "blockVideo",
group: "block",
name: 'blockVideo',
group: 'block',
atom: true,
addAttributes() {
return {
blockObject: {
default: null,
},
};
}
},
parseHTML() {
return [
{
tag: "block-video",
tag: 'block-video',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-video", mergeAttributes(HTMLAttributes), 0];
return ['block-video', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(VideoBlockComponent);
return ReactNodeViewRenderer(VideoBlockComponent)
},
});
})

View file

@ -1,56 +1,79 @@
import { NodeViewWrapper } from "@tiptap/react";
import { Loader, Video } from "lucide-react";
import React, { useEffect } from "react";
import styled from "styled-components";
import { uploadNewVideoFile } from "../../../../../services/blocks/Video/video";
import { getActivityBlockMediaDirectory } from "@services/media/media";
import { UploadIcon } from "@radix-ui/react-icons";
import { useOrg } from "@components/Contexts/OrgContext";
import { useCourse } from "@components/Contexts/CourseContext";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import { Loader, Video } from 'lucide-react'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { uploadNewVideoFile } from '../../../../../services/blocks/Video/video'
import { getActivityBlockMediaDirectory } from '@services/media/media'
import { UploadIcon } from '@radix-ui/react-icons'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function VideoBlockComponents(props: any) {
const org = useOrg() as any;
const course = useCourse() as any;
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const [video, setVideo] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
const org = useOrg() as any
const course = useCourse() as any
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const [video, setVideo] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
props.node.attrs.blockObject
)
const fileId = blockObject
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
: null
const handleVideoChange = (event: React.ChangeEvent<any>) => {
setVideo(event.target.files[0]);
};
setVideo(event.target.files[0])
}
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
let object = await uploadNewVideoFile(video, props.extension.options.activity.activity_uuid);
setIsLoading(false);
setblockObject(object);
e.preventDefault()
setIsLoading(true)
let object = await uploadNewVideoFile(
video,
props.extension.options.activity.activity_uuid
)
setIsLoading(false)
setblockObject(object)
props.updateAttributes({
blockObject: object,
});
};
useEffect(() => {
})
}
, [course, org]);
useEffect(() => {}, [course, org])
return (
<NodeViewWrapper className="block-video">
{!blockObject && (
<BlockVideoWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
<BlockVideoWrapper
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
contentEditable={isEditable}
>
{isLoading ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
<Loader
className="animate-spin animate-pulse text-gray-200"
size={50}
/>
) : (
<>
<div>
<Video className="text-gray-200" size={50} />
</div>
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleVideoChange} type="file" name="" id="" />
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
<input
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
onChange={handleVideoChange}
type="file"
name=""
id=""
/>
<button
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
onClick={handleSubmit}
>
<UploadIcon></UploadIcon>
<p>Submit</p>
</button>
</>
)}
</BlockVideoWrapper>
@ -60,31 +83,33 @@ function VideoBlockComponents(props: any) {
<video
controls
className="rounded-lg shadow h-96 w-full object-scale-down bg-black"
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
src={`${getActivityBlockMediaDirectory(
org?.org_uuid,
course?.courseStructure.course_uuid,
props.extension.options.activity.activity_uuid,
blockObject.block_uuid,
blockObject ? fileId : ' ', 'videoBlock')}`}
blockObject ? fileId : ' ',
'videoBlock'
)}`}
></video>
</BlockVideo>
)}
</NodeViewWrapper>
);
)
}
const BlockVideoWrapper = styled.div`
//border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
//border: ${(props) =>
props.contentEditable ? '2px dashed #713f1117' : 'none'};
// center
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`
const BlockVideo = styled.div`
display: flex;
flex-direction: column;
`;
export default VideoBlockComponents;
`
export default VideoBlockComponents

View file

@ -1,26 +1,43 @@
import styled from "styled-components";
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, DividerVerticalIcon, ListBulletIcon } from "@radix-ui/react-icons";
import { AlertCircle, AlertTriangle, BadgeHelp, Code, FileText, ImagePlus, Sigma, Video, Youtube } from "lucide-react";
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
import styled from 'styled-components'
import {
FontBoldIcon,
FontItalicIcon,
StrikethroughIcon,
ArrowLeftIcon,
ArrowRightIcon,
DividerVerticalIcon,
ListBulletIcon,
} from '@radix-ui/react-icons'
import {
AlertCircle,
AlertTriangle,
BadgeHelp,
Code,
FileText,
ImagePlus,
Sigma,
Video,
Youtube,
} from 'lucide-react'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
export const ToolbarButtons = ({ editor, props }: any) => {
if (!editor) {
return null;
return null
}
// YouTube extension
const addYoutubeVideo = () => {
const url = prompt("Enter YouTube URL");
const url = prompt('Enter YouTube URL')
if (url) {
editor.commands.setYoutubeVideo({
src: url,
width: 640,
height: 480,
});
})
}
};
}
return (
<ToolButtonsWrapper>
@ -30,16 +47,28 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<ToolBtn onClick={() => editor.chain().focus().redo().run()}>
<ArrowRightIcon />
</ToolBtn>
<ToolBtn onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive("bold") ? "is-active" : ""}>
<ToolBtn
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''}
>
<FontBoldIcon />
</ToolBtn>
<ToolBtn onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive("italic") ? "is-active" : ""}>
<ToolBtn
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''}
>
<FontItalicIcon />
</ToolBtn>
<ToolBtn onClick={() => editor.chain().focus().toggleStrike().run()} className={editor.isActive("strike") ? "is-active" : ""}>
<ToolBtn
onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive('strike') ? 'is-active' : ''}
>
<StrikethroughIcon />
</ToolBtn>
<ToolBtn onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive('orderedList') ? 'is-active' : ''}>
<ToolBtn
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''}
>
<ListBulletIcon />
</ToolBtn>
<ToolSelect
@ -59,25 +88,33 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<option value="6">Heading 6</option>
</ToolSelect>
{/* TODO: fix this : toggling only works one-way */}
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey" }} />
<ToolTip content={"Info Callout"}>
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutInfo").run()}>
<DividerVerticalIcon
style={{ marginTop: 'auto', marginBottom: 'auto', color: 'grey' }}
/>
<ToolTip content={'Info Callout'}>
<ToolBtn
onClick={() => editor.chain().focus().toggleNode('calloutInfo').run()}
>
<AlertCircle size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"Warning Callout"}>
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutWarning").run()}>
<ToolTip content={'Warning Callout'}>
<ToolBtn
onClick={() =>
editor.chain().focus().toggleNode('calloutWarning').run()
}
>
<AlertTriangle size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"Image"}>
<ToolTip content={'Image'}>
<ToolBtn
onClick={() =>
editor
.chain()
.focus()
.insertContent({
type: "blockImage",
type: 'blockImage',
})
.run()
}
@ -85,15 +122,14 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<ImagePlus size={15} />
</ToolBtn>
</ToolTip>
<ToolTip
content={"Video"}>
<ToolTip content={'Video'}>
<ToolBtn
onClick={() =>
editor
.chain()
.focus()
.insertContent({
type: "blockVideo",
type: 'blockVideo',
})
.run()
}
@ -101,19 +137,19 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<Video size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"YouTube video"}>
<ToolTip content={'YouTube video'}>
<ToolBtn onClick={() => addYoutubeVideo()}>
<Youtube size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"Math Equation (LaTeX)"}>
<ToolTip content={'Math Equation (LaTeX)'}>
<ToolBtn
onClick={() =>
editor
.chain()
.focus()
.insertContent({
type: "blockMathEquation",
type: 'blockMathEquation',
})
.run()
}
@ -121,14 +157,14 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<Sigma size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"PDF Document"}>
<ToolTip content={'PDF Document'}>
<ToolBtn
onClick={() =>
editor
.chain()
.focus()
.insertContent({
type: "blockPDF",
type: 'blockPDF',
})
.run()
}
@ -136,14 +172,14 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<FileText size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"Interactive Quiz"}>
<ToolTip content={'Interactive Quiz'}>
<ToolBtn
onClick={() =>
editor
.chain()
.focus()
.insertContent({
type: "blockQuiz",
type: 'blockQuiz',
})
.run()
}
@ -151,7 +187,7 @@ export const ToolbarButtons = ({ editor, props }: any) => {
<BadgeHelp size={15} />
</ToolBtn>
</ToolTip>
<ToolTip content={"Code Block"}>
<ToolTip content={'Code Block'}>
<ToolBtn
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''}
@ -160,15 +196,15 @@ export const ToolbarButtons = ({ editor, props }: any) => {
</ToolBtn>
</ToolTip>
</ToolButtonsWrapper>
);
};
)
}
const ToolButtonsWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: left;
justify-content: left;
`;
`
const ToolBtn = styled.div`
display: flex;
@ -197,7 +233,7 @@ const ToolBtn = styled.div`
background: rgba(217, 217, 217, 0.48);
cursor: pointer;
}
`;
`
const ToolSelect = styled.select`
display: flex;
@ -208,6 +244,6 @@ const ToolSelect = styled.select`
height: 25px;
padding: 5px;
font-size: 11px;
font-family: "DM Sans";
font-family: 'DM Sans';
margin-right: 5px;
`;
`