mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: format with prettier
This commit is contained in:
parent
03fb09c3d6
commit
a147ad6610
164 changed files with 11257 additions and 8154 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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``
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`;
|
||||
`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue