mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #15 from learnhouse/feat/custom-editor-inputs
Feat/custom editor inputs
This commit is contained in:
commit
eebaee42a5
28 changed files with 1005 additions and 93 deletions
55
front/components/Canva/Canva.tsx
Normal file
55
front/components/Canva/Canva.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useEditor, EditorContent } from "@tiptap/react";
|
||||||
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
// Custom Extensions
|
||||||
|
import InfoCallout from "../Editor/Extensions/Callout/Info/InfoCallout";
|
||||||
|
import WarningCallout from "../Editor/Extensions/Callout/Warning/WarningCallout";
|
||||||
|
import ImageBlock from "../Editor/Extensions/Image/ImageBlock";
|
||||||
|
import Youtube from "@tiptap/extension-youtube";
|
||||||
|
import { EditorContentWrapper } from "../Editor/Editor";
|
||||||
|
import VideoBlock from "../Editor/Extensions/Video/VideoBlock";
|
||||||
|
|
||||||
|
interface Editor {
|
||||||
|
content: string;
|
||||||
|
element: any;
|
||||||
|
//course: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Canva(props: Editor) {
|
||||||
|
const isEditable = false;
|
||||||
|
const editor: any = useEditor({
|
||||||
|
editable: isEditable,
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
// Custom Extensions
|
||||||
|
InfoCallout.configure({
|
||||||
|
editable: isEditable,
|
||||||
|
}),
|
||||||
|
WarningCallout.configure({
|
||||||
|
editable: isEditable,
|
||||||
|
}),
|
||||||
|
ImageBlock.configure({
|
||||||
|
editable: isEditable,
|
||||||
|
element: props.element,
|
||||||
|
}),
|
||||||
|
VideoBlock.configure({
|
||||||
|
editable: true,
|
||||||
|
element: props.element,
|
||||||
|
}),
|
||||||
|
Youtube.configure({
|
||||||
|
controls: true,
|
||||||
|
modestBranding: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
content: props.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorContentWrapper>
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</EditorContentWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Canva;
|
||||||
|
|
@ -10,8 +10,14 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { getBackendUrl } from "../../services/config";
|
import { getBackendUrl } from "../../services/config";
|
||||||
import { GlobeIcon, PaperPlaneIcon, SlashIcon } from "@radix-ui/react-icons";
|
import { SlashIcon } from "@radix-ui/react-icons";
|
||||||
import Avvvatars from "avvvatars-react";
|
import Avvvatars from "avvvatars-react";
|
||||||
|
// extensions
|
||||||
|
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
|
||||||
|
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";
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -26,23 +32,43 @@ function Editor(props: Editor) {
|
||||||
const auth: any = React.useContext(AuthContext);
|
const auth: any = React.useContext(AuthContext);
|
||||||
|
|
||||||
const editor: any = useEditor({
|
const editor: any = useEditor({
|
||||||
|
editable: true,
|
||||||
|
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
// The Collaboration extension comes with its own history handling
|
// The Collaboration extension comes with its own history handling
|
||||||
history: false,
|
// history: false,
|
||||||
|
}),
|
||||||
|
InfoCallout.configure({
|
||||||
|
editable: true,
|
||||||
|
}),
|
||||||
|
WarningCallout.configure({
|
||||||
|
editable: true,
|
||||||
|
}),
|
||||||
|
ImageBlock.configure({
|
||||||
|
editable: true,
|
||||||
|
element: props.element,
|
||||||
|
}),
|
||||||
|
VideoBlock.configure({
|
||||||
|
editable: true,
|
||||||
|
element: props.element,
|
||||||
|
}),
|
||||||
|
Youtube.configure({
|
||||||
|
controls: true,
|
||||||
|
modestBranding: true,
|
||||||
}),
|
}),
|
||||||
// Register the document with Tiptap
|
// Register the document with Tiptap
|
||||||
Collaboration.configure({
|
// Collaboration.configure({
|
||||||
document: props.ydoc,
|
// document: props.ydoc,
|
||||||
}),
|
// }),
|
||||||
// Register the collaboration cursor extension
|
// Register the collaboration cursor extension
|
||||||
CollaborationCursor.configure({
|
// CollaborationCursor.configure({
|
||||||
provider: props.provider,
|
// provider: props.provider,
|
||||||
user: {
|
// user: {
|
||||||
name: auth.userInfo.username,
|
// name: auth.userInfo.username,
|
||||||
color: "#f783ac",
|
// color: "#f783ac",
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
],
|
],
|
||||||
|
|
||||||
content: props.content,
|
content: props.content,
|
||||||
|
|
@ -65,15 +91,13 @@ function Editor(props: Editor) {
|
||||||
<EditorTop>
|
<EditorTop>
|
||||||
<EditorDocSection>
|
<EditorDocSection>
|
||||||
<EditorInfoWrapper>
|
<EditorInfoWrapper>
|
||||||
<EditorInfoLearnHouseLogo width={23} height={23} src={learnhouseIcon} alt="" />
|
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
||||||
<EditorInfoThumbnail src={`${getBackendUrl()}content/uploads/img/${props.course.course.thumbnail}`} alt=""></EditorInfoThumbnail>
|
<EditorInfoThumbnail src={`${getBackendUrl()}content/uploads/img/${props.course.course.thumbnail}`} alt=""></EditorInfoThumbnail>
|
||||||
<EditorInfoDocName>
|
<EditorInfoDocName>
|
||||||
{" "}
|
{" "}
|
||||||
<b>{props.course.course.name}</b> <SlashIcon /> {props.element.name}{" "}
|
<b>{props.course.course.name}</b> <SlashIcon /> {props.element.name}{" "}
|
||||||
</EditorInfoDocName>
|
</EditorInfoDocName>
|
||||||
<EditorSaveButton onClick={() => props.setContent(editor.getJSON())}>
|
<EditorSaveButton onClick={() => props.setContent(editor.getJSON())}>Save</EditorSaveButton>
|
||||||
Save
|
|
||||||
</EditorSaveButton>
|
|
||||||
</EditorInfoWrapper>
|
</EditorInfoWrapper>
|
||||||
<EditorButtonsWrapper>
|
<EditorButtonsWrapper>
|
||||||
<ToolbarButtons editor={editor} />
|
<ToolbarButtons editor={editor} />
|
||||||
|
|
@ -90,7 +114,6 @@ function Editor(props: Editor) {
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.99 }}
|
initial={{ opacity: 0, scale: 0.99 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
key="modal"
|
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 360,
|
stiffness: 360,
|
||||||
|
|
@ -106,6 +129,7 @@ function Editor(props: Editor) {
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = styled.div`
|
const Page = styled.div`
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -119,7 +143,7 @@ const Page = styled.div`
|
||||||
background-size: 50px 50px;
|
background-size: 50px 50px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
`
|
`;
|
||||||
|
|
||||||
const EditorTop = styled.div`
|
const EditorTop = styled.div`
|
||||||
background-color: #ffffffb8;
|
background-color: #ffffffb8;
|
||||||
|
|
@ -224,7 +248,7 @@ const EditorInfoThumbnail = styled.img`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorContentWrapper = styled.div`
|
export const EditorContentWrapper = styled.div`
|
||||||
margin: 40px;
|
margin: 40px;
|
||||||
margin-top: 90px;
|
margin-top: 90px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
@ -236,7 +260,8 @@ const EditorContentWrapper = styled.div`
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 20px;
|
||||||
|
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|
@ -244,6 +269,17 @@ const EditorContentWrapper = styled.div`
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
min-width: 200px;
|
||||||
|
width: 100%;
|
||||||
|
height: 440px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: block;
|
||||||
|
outline: 0px solid transparent;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { mergeAttributes, Node } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
import InfoCalloutComponent from "./InfoCalloutComponent";
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "calloutInfo",
|
||||||
|
group: "block",
|
||||||
|
draggable: true,
|
||||||
|
content: "text*",
|
||||||
|
|
||||||
|
// TODO : multi line support
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "callout-info",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(InfoCalloutComponent);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
function InfoCalloutComponent(props: any) {
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
<InfoCalloutWrapper contentEditable={props.extension.options.editable}>
|
||||||
|
<AlertCircle /> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
|
||||||
|
</InfoCalloutWrapper>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoCalloutWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
color: #1f3a8a;
|
||||||
|
background-color: #dbe9fe;
|
||||||
|
border: 1px solid #c1d9fb;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 1rem 0;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 15px;
|
||||||
|
|
||||||
|
svg{
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: ${(props) => (props.contentEditable ? "2px dashed #1f3a8a12" : "none")};
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DragHandle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
cursor: move;
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default InfoCalloutComponent;
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { mergeAttributes, Node } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
import WarningCalloutComponent from "./WarningCalloutComponent";
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "calloutWarning",
|
||||||
|
group: "block",
|
||||||
|
draggable: true,
|
||||||
|
content: "text*",
|
||||||
|
marks: "",
|
||||||
|
defining: true,
|
||||||
|
|
||||||
|
// TODO : multi line support
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "callout-warning",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(WarningCalloutComponent);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
function WarningCalloutComponent(props: any) {
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
<CalloutWrapper contentEditable={props.extension.options.editable}>
|
||||||
|
<AlertTriangle/> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
|
||||||
|
</CalloutWrapper>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalloutWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: #fefce8;
|
||||||
|
color: #713f11;
|
||||||
|
border: 1px solid #fff103;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 1rem 0;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 15px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DragHandle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
cursor: move;
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default WarningCalloutComponent;
|
||||||
35
front/components/Editor/Extensions/Image/ImageBlock.ts
Normal file
35
front/components/Editor/Extensions/Image/ImageBlock.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { mergeAttributes, Node } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
import ImageBlockComponent from "./ImageBlockComponent";
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "blockImage",
|
||||||
|
group: "block",
|
||||||
|
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
fileObject: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "block-image",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["block-image", mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(ImageBlockComponent);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { AlertCircle, AlertTriangle, Image, ImagePlus, Info } from "lucide-react";
|
||||||
|
import { getImageFile, uploadNewImageFile } from "../../../../services/files/images";
|
||||||
|
import { getBackendUrl } from "../../../../services/config";
|
||||||
|
|
||||||
|
function ImageBlockComponent(props: any) {
|
||||||
|
const [image, setImage] = React.useState(null);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [fileObject, setfileObject] = React.useState(props.node.attrs.fileObject);
|
||||||
|
|
||||||
|
const handleImageChange = (event: React.ChangeEvent<any>) => {
|
||||||
|
setImage(event.target.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
let object = await uploadNewImageFile(image, props.extension.options.element.element_id);
|
||||||
|
setIsLoading(false);
|
||||||
|
setfileObject(object);
|
||||||
|
props.updateAttributes({
|
||||||
|
fileObject: object,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className="block-image">
|
||||||
|
{!fileObject && (
|
||||||
|
<BlockImageWrapper contentEditable={props.extension.options.editable}>
|
||||||
|
<div>
|
||||||
|
<Image color="#e1e0e0" size={50} />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<input onChange={handleImageChange} type="file" name="" id="" />
|
||||||
|
<br />
|
||||||
|
<button onClick={handleSubmit}>Submit</button>
|
||||||
|
</BlockImageWrapper>
|
||||||
|
)}
|
||||||
|
{fileObject && (
|
||||||
|
<BlockImage>
|
||||||
|
<img
|
||||||
|
src={`${getBackendUrl()}content/uploads/files/pictures/${props.extension.options.element.element_id}/${fileObject.file_id}.${
|
||||||
|
fileObject.file_format
|
||||||
|
}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</BlockImage>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<div>
|
||||||
|
<AlertTriangle color="#e1e0e0" size={50} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageBlockComponent;
|
||||||
|
|
||||||
|
const BlockImageWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 74px;
|
||||||
|
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||||
|
|
||||||
|
// center
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlockImage = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 300px;
|
||||||
|
// cover
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const ImageNotFound = styled.div``;
|
||||||
34
front/components/Editor/Extensions/Video/VideoBlock.ts
Normal file
34
front/components/Editor/Extensions/Video/VideoBlock.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { mergeAttributes, Node } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
import VideoBlockComponent from "./VideoBlockComponent";
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "blockVideo",
|
||||||
|
group: "block",
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
fileObject: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "block-video",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["block-video", mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(VideoBlockComponent);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import { AlertTriangle, Image, Video } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { getBackendUrl } from "../../../../services/config";
|
||||||
|
import { uploadNewVideoFile } from "../../../../services/files/video";
|
||||||
|
|
||||||
|
function VideoBlockComponents(props: any) {
|
||||||
|
const [video, setVideo] = React.useState(null);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [fileObject, setfileObject] = React.useState(props.node.attrs.fileObject);
|
||||||
|
|
||||||
|
const handleVideoChange = (event: React.ChangeEvent<any>) => {
|
||||||
|
setVideo(event.target.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
let object = await uploadNewVideoFile(video, props.extension.options.element.element_id);
|
||||||
|
setIsLoading(false);
|
||||||
|
setfileObject(object);
|
||||||
|
props.updateAttributes({
|
||||||
|
fileObject: object,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className="block-video">
|
||||||
|
{!fileObject && (
|
||||||
|
<BlockVideoWrapper contentEditable={props.extension.options.editable}>
|
||||||
|
<div>
|
||||||
|
<Video color="#e1e0e0" size={50} />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<input onChange={handleVideoChange} type="file" name="" id="" />
|
||||||
|
<br />
|
||||||
|
<button onClick={handleSubmit}>Submit</button>
|
||||||
|
</BlockVideoWrapper>
|
||||||
|
)}
|
||||||
|
{fileObject && (
|
||||||
|
<BlockVideo>
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
src={`${getBackendUrl()}content/uploads/files/videos/${props.extension.options.element.element_id}/${fileObject.file_id}.${
|
||||||
|
fileObject.file_format
|
||||||
|
}`}
|
||||||
|
></video>
|
||||||
|
</BlockVideo>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<div>
|
||||||
|
<AlertTriangle color="#e1e0e0" size={50} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const BlockVideoWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 74px;
|
||||||
|
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;
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 300px;
|
||||||
|
// cover
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export default VideoBlockComponents;
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons";
|
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon } from "@radix-ui/react-icons";
|
||||||
|
import { AlertCircle, AlertTriangle, ImagePlus, Info, Video, Youtube } from "lucide-react";
|
||||||
|
|
||||||
export const ToolbarButtons = ({ editor }: any) => {
|
export const ToolbarButtons = ({ editor }: any) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// YouTube extension
|
||||||
|
|
||||||
|
const addYoutubeVideo = () => {
|
||||||
|
const url = prompt("Enter YouTube URL");
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
editor.commands.setYoutubeVideo({
|
||||||
|
src: url,
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButtonsWrapper>
|
<ToolButtonsWrapper>
|
||||||
<ToolBtn onClick={() => editor.chain().focus().undo().run()}>
|
<ToolBtn onClick={() => editor.chain().focus().undo().run()}>
|
||||||
|
|
@ -23,7 +38,15 @@ export const ToolbarButtons = ({ editor }: any) => {
|
||||||
<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 />
|
<StrikethroughIcon />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
<ToolSelect onChange={(e) => editor.chain().focus().toggleHeading({ level: parseInt(e.target.value) }).run() }>
|
<ToolSelect
|
||||||
|
onChange={(e) =>
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.toggleHeading({ level: parseInt(e.target.value) })
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
<option value="1">Heading 1</option>
|
<option value="1">Heading 1</option>
|
||||||
<option value="2">Heading 2</option>
|
<option value="2">Heading 2</option>
|
||||||
<option value="3">Heading 3</option>
|
<option value="3">Heading 3</option>
|
||||||
|
|
@ -31,6 +54,42 @@ export const ToolbarButtons = ({ editor }: any) => {
|
||||||
<option value="5">Heading 5</option>
|
<option value="5">Heading 5</option>
|
||||||
<option value="6">Heading 6</option>
|
<option value="6">Heading 6</option>
|
||||||
</ToolSelect>
|
</ToolSelect>
|
||||||
|
{/* TODO: fix this : toggling only works one-way */}
|
||||||
|
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutWarning").run()}>
|
||||||
|
<AlertTriangle size={15} />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutInfo").run()}>
|
||||||
|
<AlertCircle size={15} />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
onClick={() =>
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContent({
|
||||||
|
type: "blockImage",
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ImagePlus size={15} />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
onClick={() =>
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContent({
|
||||||
|
type: "blockVideo",
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Video size={15} />
|
||||||
|
</ToolBtn>
|
||||||
|
<ToolBtn onClick={() => addYoutubeVideo()}>
|
||||||
|
<Youtube size={15} />
|
||||||
|
</ToolBtn>
|
||||||
</ToolButtonsWrapper>
|
</ToolButtonsWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -49,10 +108,13 @@ const ToolBtn = styled.div`
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
font-size: 5px;
|
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
background: rgba(176, 176, 176, 0.5);
|
background: rgba(176, 176, 176, 0.5);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const AuthProvider = (props: any) => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading });
|
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading });
|
||||||
router.push("/login");
|
//router.push("/login");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
|
|
|
||||||
35
front/package-lock.json
generated
35
front/package-lock.json
generated
|
|
@ -12,11 +12,13 @@
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
"@tiptap/extension-collaboration": "^2.0.0-beta.199",
|
"@tiptap/extension-collaboration": "^2.0.0-beta.199",
|
||||||
"@tiptap/extension-collaboration-cursor": "^2.0.0-beta.199",
|
"@tiptap/extension-collaboration-cursor": "^2.0.0-beta.199",
|
||||||
|
"@tiptap/extension-youtube": "^2.0.0-beta.207",
|
||||||
"@tiptap/html": "^2.0.0-beta.202",
|
"@tiptap/html": "^2.0.0-beta.202",
|
||||||
"@tiptap/react": "^2.0.0-beta.199",
|
"@tiptap/react": "^2.0.0-beta.199",
|
||||||
"@tiptap/starter-kit": "^2.0.0-beta.199",
|
"@tiptap/starter-kit": "^2.0.0-beta.199",
|
||||||
"avvvatars-react": "^0.4.2",
|
"avvvatars-react": "^0.4.2",
|
||||||
"framer-motion": "^7.3.6",
|
"framer-motion": "^7.3.6",
|
||||||
|
"lucide-react": "^0.104.1",
|
||||||
"next": "12.3.1",
|
"next": "12.3.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
@ -1296,6 +1298,18 @@
|
||||||
"@tiptap/core": "^2.0.0-beta.193"
|
"@tiptap/core": "^2.0.0-beta.193"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-youtube": {
|
||||||
|
"version": "2.0.0-beta.207",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-2.0.0-beta.207.tgz",
|
||||||
|
"integrity": "sha512-fx3adbWZWCysl2Pbw2NNbOVJ+mZT1wJ8YImwXjlM976Z0AWlWY4+O4H2avHzIHQP+t9U/jaV4K6FdcHo6EjJuQ==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^2.0.0-beta.193"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/html": {
|
"node_modules/@tiptap/html": {
|
||||||
"version": "2.0.0-beta.202",
|
"version": "2.0.0-beta.202",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-2.0.0-beta.202.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-2.0.0-beta.202.tgz",
|
||||||
|
|
@ -3532,6 +3546,15 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.104.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.104.1.tgz",
|
||||||
|
"integrity": "sha512-BKvhulnLKmBj+6pqUN5ViYk4a5fabMgc4B0a4ZLUnbRqkDDWH3h7Iet6U4WbesJzjWauQrXUlEvQCe5XpFuRnw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
|
@ -5961,6 +5984,12 @@
|
||||||
"integrity": "sha512-ntOqEhkBjDHrdzxvpPe4U1JB5GgE9/yyWqWdgzSL9lpSndRTJN1xQLOmyuv0qsLqOgBHn1YITHvaxPb3t8FrFw==",
|
"integrity": "sha512-ntOqEhkBjDHrdzxvpPe4U1JB5GgE9/yyWqWdgzSL9lpSndRTJN1xQLOmyuv0qsLqOgBHn1YITHvaxPb3t8FrFw==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"@tiptap/extension-youtube": {
|
||||||
|
"version": "2.0.0-beta.207",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-2.0.0-beta.207.tgz",
|
||||||
|
"integrity": "sha512-fx3adbWZWCysl2Pbw2NNbOVJ+mZT1wJ8YImwXjlM976Z0AWlWY4+O4H2avHzIHQP+t9U/jaV4K6FdcHo6EjJuQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"@tiptap/html": {
|
"@tiptap/html": {
|
||||||
"version": "2.0.0-beta.202",
|
"version": "2.0.0-beta.202",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-2.0.0-beta.202.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-2.0.0-beta.202.tgz",
|
||||||
|
|
@ -7577,6 +7606,12 @@
|
||||||
"yallist": "^4.0.0"
|
"yallist": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lucide-react": {
|
||||||
|
"version": "0.104.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.104.1.tgz",
|
||||||
|
"integrity": "sha512-BKvhulnLKmBj+6pqUN5ViYk4a5fabMgc4B0a4ZLUnbRqkDDWH3h7Iet6U4WbesJzjWauQrXUlEvQCe5XpFuRnw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"memoize-one": {
|
"memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,13 @@
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
"@tiptap/extension-collaboration": "^2.0.0-beta.199",
|
"@tiptap/extension-collaboration": "^2.0.0-beta.199",
|
||||||
"@tiptap/extension-collaboration-cursor": "^2.0.0-beta.199",
|
"@tiptap/extension-collaboration-cursor": "^2.0.0-beta.199",
|
||||||
|
"@tiptap/extension-youtube": "^2.0.0-beta.207",
|
||||||
"@tiptap/html": "^2.0.0-beta.202",
|
"@tiptap/html": "^2.0.0-beta.202",
|
||||||
"@tiptap/react": "^2.0.0-beta.199",
|
"@tiptap/react": "^2.0.0-beta.199",
|
||||||
"@tiptap/starter-kit": "^2.0.0-beta.199",
|
"@tiptap/starter-kit": "^2.0.0-beta.199",
|
||||||
"avvvatars-react": "^0.4.2",
|
"avvvatars-react": "^0.4.2",
|
||||||
"framer-motion": "^7.3.6",
|
"framer-motion": "^7.3.6",
|
||||||
|
"lucide-react": "^0.104.1",
|
||||||
"next": "12.3.1",
|
"next": "12.3.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,10 @@ import styled from "styled-components";
|
||||||
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
|
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PreAlphaLabel } from "../components//UI/Layout";
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<HomePage>
|
<HomePage>
|
||||||
<PreAlphaLabel>🚧 Pre-Alpha</PreAlphaLabel>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import Bold from "@tiptap/extension-bold";
|
|
||||||
import Document from "@tiptap/extension-document";
|
|
||||||
import Paragraph from "@tiptap/extension-paragraph";
|
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
|
||||||
import Text from "@tiptap/extension-text";
|
|
||||||
import { generateHTML } from "@tiptap/html";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import Layout from "../../../../../../../components//UI/Layout";
|
import Layout from "../../../../../../../components//UI/Layout";
|
||||||
import { getElement } from "../../../../../../../services/courses/elements";
|
import { getElement } from "../../../../../../../services/courses/elements";
|
||||||
import { getBackendUrl } from "../../../../../../../services/config";
|
import { getBackendUrl } from "../../../../../../../services/config";
|
||||||
|
import Canva from "../../../../../../../components/Canva/Canva";
|
||||||
|
|
||||||
function ElementPage() {
|
function ElementPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -30,35 +25,6 @@ function ElementPage() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [router.isReady]);
|
}, [router.isReady]);
|
||||||
|
|
||||||
const output = useMemo(() => {
|
|
||||||
if (router.isReady && !isLoading) {
|
|
||||||
console.log(element);
|
|
||||||
|
|
||||||
if (element.type == "dynamic") {
|
|
||||||
let content =
|
|
||||||
Object.keys(element.content).length > 0
|
|
||||||
? element.content
|
|
||||||
: {
|
|
||||||
type: "doc",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: "Hello world, this is a example Canva ⚡️",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
console.log("element", content);
|
|
||||||
|
|
||||||
return generateHTML(content, [Document, StarterKit, Paragraph, Text, Bold]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [element.content]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
@ -69,7 +35,7 @@ function ElementPage() {
|
||||||
<h1>{element.name} </h1>
|
<h1>{element.name} </h1>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
{element.type == "dynamic" && <div dangerouslySetInnerHTML={{ __html: output } as any}></div>}
|
{element.type == "dynamic" && <Canva content= {element.content} element={element}/>}
|
||||||
{/* todo : use apis & streams instead of this */}
|
{/* todo : use apis & streams instead of this */}
|
||||||
{element.type == "video" && (
|
{element.type == "video" && (
|
||||||
<video controls src={`${getBackendUrl()}content/uploads/video/${element.content.video.element_id}/${element.content.video.filename}`}></video>
|
<video controls src={`${getBackendUrl()}content/uploads/video/${element.content.video.element_id}/${element.content.video.filename}`}></video>
|
||||||
|
|
|
||||||
38
front/services/files/images.ts
Normal file
38
front/services/files/images.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { getAPIUrl } from "../config";
|
||||||
|
|
||||||
|
export async function uploadNewImageFile(file: any, element_id: string) {
|
||||||
|
const HeadersConfig = new Headers();
|
||||||
|
|
||||||
|
// Send file thumbnail as form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file_object", file);
|
||||||
|
formData.append("element_id", element_id);
|
||||||
|
|
||||||
|
const requestOptions: any = {
|
||||||
|
method: "POST",
|
||||||
|
headers: HeadersConfig,
|
||||||
|
redirect: "follow",
|
||||||
|
credentials: "include",
|
||||||
|
body: formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(`${getAPIUrl()}files/picture`, requestOptions)
|
||||||
|
.then((result) => result.json())
|
||||||
|
.catch((error) => console.log("error", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImageFile(file_id: string) {
|
||||||
|
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
|
||||||
|
|
||||||
|
const requestOptions: any = {
|
||||||
|
method: "GET",
|
||||||
|
headers: HeadersConfig,
|
||||||
|
redirect: "follow",
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
|
||||||
|
// todo : add course id to url
|
||||||
|
return fetch(`${getAPIUrl()}files/picture?file_id=${file_id}`, requestOptions)
|
||||||
|
.then((result) => result.json())
|
||||||
|
.catch((error) => console.log("error", error));
|
||||||
|
}
|
||||||
38
front/services/files/video.ts
Normal file
38
front/services/files/video.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { getAPIUrl } from "../config";
|
||||||
|
|
||||||
|
export async function uploadNewVideoFile(file: any, element_id: string) {
|
||||||
|
const HeadersConfig = new Headers();
|
||||||
|
|
||||||
|
// Send file thumbnail as form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file_object", file);
|
||||||
|
formData.append("element_id", element_id);
|
||||||
|
|
||||||
|
const requestOptions: any = {
|
||||||
|
method: "POST",
|
||||||
|
headers: HeadersConfig,
|
||||||
|
redirect: "follow",
|
||||||
|
credentials: "include",
|
||||||
|
body: formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(`${getAPIUrl()}files/video`, requestOptions)
|
||||||
|
.then((result) => result.json())
|
||||||
|
.catch((error) => console.log("error", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVideoFile(file_id: string) {
|
||||||
|
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
|
||||||
|
|
||||||
|
const requestOptions: any = {
|
||||||
|
method: "GET",
|
||||||
|
headers: HeadersConfig,
|
||||||
|
redirect: "follow",
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return fetch(`${getAPIUrl()}files/video?file_id=${file_id}`, requestOptions)
|
||||||
|
.then((result) => result.json())
|
||||||
|
.catch((error) => console.log("error", error));
|
||||||
|
}
|
||||||
|
|
@ -16,5 +16,12 @@
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"paths": {
|
||||||
|
"@components/*": ["components/*"],
|
||||||
|
"@public/*": ["public/*"],
|
||||||
|
"@images/*": ["public/img/*"],
|
||||||
|
"@services/*": ["services/*"],
|
||||||
|
"@editor/*": ["components/Editor/*"]
|
||||||
|
},
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from src.routers import users, auth, houses, orgs, roles
|
from src.routers import users, auth, houses, orgs, roles, files
|
||||||
from src.routers.courses import chapters, collections, courses,elements
|
from src.routers.courses import chapters, collections, courses,elements
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ global_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
global_router.include_router(houses.router, prefix="/houses", tags=["houses"])
|
global_router.include_router(houses.router, prefix="/houses", tags=["houses"])
|
||||||
global_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
global_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
||||||
global_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
global_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||||
|
global_router.include_router(files.router, prefix="/files", tags=["files"])
|
||||||
global_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
global_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||||
global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||||
global_router.include_router(elements.router, prefix="/elements", tags=["elements"])
|
global_router.include_router(elements.router, prefix="/elements", tags=["elements"])
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ async def api_delete_element(element_id: str, current_user: PublicUser = Depends
|
||||||
|
|
||||||
# Video Element
|
# Video Element
|
||||||
|
|
||||||
|
|
||||||
@router.post("/video")
|
@router.post("/video")
|
||||||
async def api_create_video_element(name: str = Form() , coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None):
|
async def api_create_video_element(name: str = Form() , coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
41
src/routers/files.py
Normal file
41
src/routers/files.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from fastapi import APIRouter, Depends, UploadFile, Form
|
||||||
|
from src.dependencies.auth import get_current_user
|
||||||
|
from fastapi import HTTPException, status, UploadFile
|
||||||
|
|
||||||
|
from src.services.files.pictures import create_picture_file, get_picture_file
|
||||||
|
from src.services.files.videos import create_video_file, get_video_file
|
||||||
|
from src.services.users import PublicUser
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/picture")
|
||||||
|
async def api_create_picture_file(file_object: UploadFile, element_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Create new picture file
|
||||||
|
"""
|
||||||
|
return await create_picture_file(file_object, element_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/video")
|
||||||
|
async def api_create_video_file(file_object: UploadFile,element_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Create new video file
|
||||||
|
"""
|
||||||
|
return await create_video_file(file_object, element_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/picture")
|
||||||
|
async def api_get_picture_file(file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Get picture file
|
||||||
|
"""
|
||||||
|
return await get_picture_file(file_id, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/video")
|
||||||
|
async def api_get_video_file(file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Get video file
|
||||||
|
"""
|
||||||
|
return await get_video_file(file_id, current_user)
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from src.services.courses.elements.elements import ElementInDB
|
from src.services.courses.elements.elements import ElementInDB
|
||||||
from src.services.uploads import upload_thumbnail
|
from src.services.courses.thumbnails import upload_thumbnail
|
||||||
from src.services.users import PublicUser, User
|
from src.services.users import PublicUser
|
||||||
from src.services.database import create_config_collection, check_database, create_database, learnhouseDB
|
from src.services.database import check_database, learnhouseDB
|
||||||
from src.services.security import *
|
from src.services.security import *
|
||||||
from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File
|
from fastapi import HTTPException, status, UploadFile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
#### Classes ####################################################
|
#### Classes ####################################################
|
||||||
|
|
@ -79,7 +78,6 @@ async def get_course_meta(course_id: str, current_user: PublicUser):
|
||||||
course = courses.find_one({"course_id": course_id})
|
course = courses.find_one({"course_id": course_id})
|
||||||
elements = learnhouseDB["elements"]
|
elements = learnhouseDB["elements"]
|
||||||
|
|
||||||
|
|
||||||
# verify course rights
|
# verify course rights
|
||||||
await verify_rights(course_id, current_user, "read")
|
await verify_rights(course_id, current_user, "read")
|
||||||
|
|
||||||
|
|
@ -87,7 +85,6 @@ async def get_course_meta(course_id: str, current_user: PublicUser):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist")
|
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist")
|
||||||
|
|
||||||
|
|
||||||
coursechapters = coursechapters.find(
|
coursechapters = coursechapters.find(
|
||||||
{"course_id": course_id}).sort("name", 1)
|
{"course_id": course_id}).sort("name", 1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
async def upload_thumbnail(thumbnail_file, name_in_disk):
|
async def upload_video(video_file, element_id):
|
||||||
contents = thumbnail_file.file.read()
|
|
||||||
try:
|
|
||||||
with open(f"content/uploads/img/{name_in_disk}", 'wb') as f:
|
|
||||||
f.write(contents)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return {"message": "There was an error uploading the file"}
|
|
||||||
finally:
|
|
||||||
thumbnail_file.file.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def upload_video(video_file, name_in_disk, element_id):
|
|
||||||
contents = video_file.file.read()
|
contents = video_file.file.read()
|
||||||
video_format = video_file.filename.split(".")[-1]
|
video_format = video_file.filename.split(".")[-1]
|
||||||
# create folder
|
# create folder
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from src.services.database import create_config_collection, check_database, create_database, learnhouseDB
|
from src.services.database import check_database, learnhouseDB
|
||||||
from src.services.security import verify_user_rights_with_roles
|
from src.services.security import verify_user_rights_with_roles
|
||||||
from src.services.uploads import upload_video
|
from src.services.courses.elements.uploads.videos import upload_video
|
||||||
from src.services.users import PublicUser, User
|
from src.services.users import PublicUser
|
||||||
from src.services.courses.elements.elements import ElementInDB, Element
|
from src.services.courses.elements.elements import ElementInDB
|
||||||
from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File
|
from fastapi import HTTPException, status, UploadFile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ async def create_video_element(name: str, coursechapter_id: str, current_user:
|
||||||
print("uploading video")
|
print("uploading video")
|
||||||
# get videofile format
|
# get videofile format
|
||||||
|
|
||||||
await upload_video(video_file, video_file.filename, element_id)
|
await upload_video(video_file, element_id)
|
||||||
|
|
||||||
# todo : choose whether to update the chapter or not
|
# todo : choose whether to update the chapter or not
|
||||||
# update chapter
|
# update chapter
|
||||||
|
|
|
||||||
15
src/services/courses/thumbnails.py
Normal file
15
src/services/courses/thumbnails.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
async def upload_thumbnail(thumbnail_file, name_in_disk):
|
||||||
|
contents = thumbnail_file.file.read()
|
||||||
|
try:
|
||||||
|
with open(f"content/uploads/img/{name_in_disk}", 'wb') as f:
|
||||||
|
f.write(contents)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return {"message": "There was an error uploading the file"}
|
||||||
|
finally:
|
||||||
|
thumbnail_file.file.close()
|
||||||
116
src/services/files/pictures.py
Normal file
116
src/services/files/pictures.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from src.services.database import check_database, learnhouseDB, learnhouseDB
|
||||||
|
from fastapi import HTTPException, status, UploadFile
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from src.services.users import PublicUser
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoFile(BaseModel):
|
||||||
|
file_id: str
|
||||||
|
file_format: str
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
file_type: str
|
||||||
|
element_id: str
|
||||||
|
|
||||||
|
|
||||||
|
async def create_picture_file(picture_file: UploadFile, element_id: str):
|
||||||
|
await check_database()
|
||||||
|
photos = learnhouseDB["files"]
|
||||||
|
|
||||||
|
# generate file_id
|
||||||
|
file_id = str(f"file_{uuid4()}")
|
||||||
|
|
||||||
|
# get file format
|
||||||
|
file_format = picture_file.filename.split(".")[-1]
|
||||||
|
|
||||||
|
# validate file format
|
||||||
|
if file_format not in ["jpg", "jpeg", "png", "gif"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Picture file format not supported")
|
||||||
|
|
||||||
|
# create file
|
||||||
|
file = await picture_file.read()
|
||||||
|
|
||||||
|
|
||||||
|
# get file size
|
||||||
|
file_size = len(file)
|
||||||
|
|
||||||
|
# get file type
|
||||||
|
file_type = picture_file.content_type
|
||||||
|
|
||||||
|
# get file name
|
||||||
|
file_name = picture_file.filename
|
||||||
|
|
||||||
|
# create file object
|
||||||
|
uploadable_file = PhotoFile(
|
||||||
|
file_id=file_id,
|
||||||
|
file_format=file_format,
|
||||||
|
file_name=file_name,
|
||||||
|
file_size=file_size,
|
||||||
|
file_type=file_type,
|
||||||
|
element_id=element_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# create folder for element
|
||||||
|
if not os.path.exists(f"content/uploads/files/pictures/{element_id}"):
|
||||||
|
os.mkdir(f"content/uploads/files/pictures/{element_id}")
|
||||||
|
|
||||||
|
# upload file to server
|
||||||
|
with open(f"content/uploads/files/pictures/{element_id}/{file_id}.{file_format}", 'wb') as f:
|
||||||
|
f.write(file)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# insert file object into database
|
||||||
|
photo_file_in_db = photos.insert_one(uploadable_file.dict())
|
||||||
|
|
||||||
|
if not photo_file_in_db:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Photo file could not be created")
|
||||||
|
|
||||||
|
return uploadable_file
|
||||||
|
|
||||||
|
|
||||||
|
async def get_picture_object(file_id: str):
|
||||||
|
await check_database()
|
||||||
|
photos = learnhouseDB["files"]
|
||||||
|
|
||||||
|
photo_file = photos.find_one({"file_id": file_id})
|
||||||
|
|
||||||
|
if photo_file:
|
||||||
|
photo_file = PhotoFile(**photo_file)
|
||||||
|
return photo_file
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Photo file does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_picture_file(file_id: str, current_user: PublicUser):
|
||||||
|
await check_database()
|
||||||
|
photos = learnhouseDB["files"]
|
||||||
|
|
||||||
|
photo_file = photos.find_one({"file_id": file_id})
|
||||||
|
|
||||||
|
# check media type
|
||||||
|
if photo_file.format not in ["jpg", "jpeg", "png", "gif"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Photo file format not supported")
|
||||||
|
|
||||||
|
# TODO : check if user has access to file
|
||||||
|
|
||||||
|
if photo_file:
|
||||||
|
# stream file
|
||||||
|
photo_file = PhotoFile(**photo_file)
|
||||||
|
file_format = photo_file.file_format
|
||||||
|
element_id = photo_file.element_id
|
||||||
|
file = open(
|
||||||
|
f"content/uploads/files/pictures/{element_id}/{file_id}.{file_format}", 'rb')
|
||||||
|
return StreamingResponse(file, media_type=photo_file.file_type)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Photo file does not exist")
|
||||||
118
src/services/files/videos.py
Normal file
118
src/services/files/videos.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import os
|
||||||
|
from src.services.database import check_database, learnhouseDB, learnhouseDB
|
||||||
|
from fastapi import HTTPException, status, UploadFile
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from src.services.users import PublicUser
|
||||||
|
|
||||||
|
|
||||||
|
class VideoFile(BaseModel):
|
||||||
|
file_id: str
|
||||||
|
file_format: str
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
file_type: str
|
||||||
|
element_id: str
|
||||||
|
|
||||||
|
|
||||||
|
async def create_video_file(video_file: UploadFile, element_id: str):
|
||||||
|
await check_database()
|
||||||
|
files = learnhouseDB["files"]
|
||||||
|
|
||||||
|
# generate file_id
|
||||||
|
file_id = str(f"file_{uuid4()}")
|
||||||
|
|
||||||
|
# get file format
|
||||||
|
file_format = video_file.filename.split(".")[-1]
|
||||||
|
|
||||||
|
# validate file format
|
||||||
|
if file_format not in ["mp4", "webm", "ogg"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Video file format not supported")
|
||||||
|
|
||||||
|
# create file
|
||||||
|
file = await video_file.read()
|
||||||
|
|
||||||
|
# get file size
|
||||||
|
file_size = len(file)
|
||||||
|
|
||||||
|
# get file type
|
||||||
|
file_type = video_file.content_type
|
||||||
|
|
||||||
|
# get file name
|
||||||
|
file_name = video_file.filename
|
||||||
|
|
||||||
|
# create file object
|
||||||
|
uploadable_file = VideoFile(
|
||||||
|
file_id=file_id,
|
||||||
|
file_format=file_format,
|
||||||
|
file_name=file_name,
|
||||||
|
file_size=file_size,
|
||||||
|
file_type=file_type,
|
||||||
|
element_id=element_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# create folder for element
|
||||||
|
if not os.path.exists(f"content/uploads/files/videos/{element_id}"):
|
||||||
|
os.mkdir(f"content/uploads/files/videos/{element_id}")
|
||||||
|
|
||||||
|
# upload file to server
|
||||||
|
with open(f"content/uploads/files/videos/{element_id}/{file_id}.{file_format}", 'wb') as f:
|
||||||
|
f.write(file)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# insert file object into database
|
||||||
|
video_file_in_db = files.insert_one(uploadable_file.dict())
|
||||||
|
|
||||||
|
if not video_file_in_db:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Video file could not be created")
|
||||||
|
|
||||||
|
return uploadable_file
|
||||||
|
|
||||||
|
|
||||||
|
async def get_video_object(file_id: str, current_user: PublicUser):
|
||||||
|
await check_database()
|
||||||
|
photos = learnhouseDB["files"]
|
||||||
|
|
||||||
|
video_file = photos.find_one({"file_id": file_id})
|
||||||
|
|
||||||
|
if video_file:
|
||||||
|
video_file = VideoFile(**video_file)
|
||||||
|
return video_file
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Photo file does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_video_file(file_id: str, current_user: PublicUser):
|
||||||
|
await check_database()
|
||||||
|
photos = learnhouseDB["files"]
|
||||||
|
|
||||||
|
video_file = photos.find_one({"file_id": file_id})
|
||||||
|
|
||||||
|
# check media type
|
||||||
|
if video_file.format not in ["mp4", "webm", "ogg"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Video file format not supported")
|
||||||
|
|
||||||
|
# TODO : check if user has access to file
|
||||||
|
|
||||||
|
if video_file:
|
||||||
|
# stream file
|
||||||
|
video_file = VideoFile(**video_file)
|
||||||
|
file_format = video_file.file_format
|
||||||
|
element_id = video_file.element_id
|
||||||
|
|
||||||
|
def iterfile(): #
|
||||||
|
#
|
||||||
|
with open(f"content/uploads/files/videos/{element_id}/{file_id}.{file_format}", mode="rb") as file_like:
|
||||||
|
yield from file_like
|
||||||
|
return StreamingResponse(iterfile(), media_type=video_file.file_type)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue