diff --git a/front/components/Canva/Canva.tsx b/front/components/Canva/Canva.tsx index 97a466c1..01eda31e 100644 --- a/front/components/Canva/Canva.tsx +++ b/front/components/Canva/Canva.tsx @@ -7,6 +7,7 @@ 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; @@ -31,6 +32,10 @@ function Canva(props: Editor) { editable: isEditable, element: props.element, }), + VideoBlock.configure({ + editable: true, + element: props.element, + }), Youtube.configure({ controls: true, modestBranding: true, diff --git a/front/components/Editor/Editor.tsx b/front/components/Editor/Editor.tsx index 54a12543..c5ba7ea6 100644 --- a/front/components/Editor/Editor.tsx +++ b/front/components/Editor/Editor.tsx @@ -17,6 +17,7 @@ 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 { content: string; @@ -48,6 +49,10 @@ function Editor(props: Editor) { editable: true, element: props.element, }), + VideoBlock.configure({ + editable: true, + element: props.element, + }), Youtube.configure({ controls: true, modestBranding: true, diff --git a/front/components/Editor/Extensions/Video/VideoBlock.ts b/front/components/Editor/Extensions/Video/VideoBlock.ts index bb81be3f..1f7d727f 100644 --- a/front/components/Editor/Extensions/Video/VideoBlock.ts +++ b/front/components/Editor/Extensions/Video/VideoBlock.ts @@ -4,23 +4,28 @@ import { ReactNodeViewRenderer } from "@tiptap/react"; import VideoBlockComponent from "./VideoBlockComponent"; export default Node.create({ - name: "calloutWarning", + name: "blockVideo", group: "block", - draggable: true, - content: "inline*", - - // TODO : multi line support + atom: true, + + addAttributes() { + return { + fileObject: { + default: null, + }, + }; + }, parseHTML() { return [ { - tag: "callout-warning", + tag: "block-video", }, ]; }, renderHTML({ HTMLAttributes }) { - return ["callout-info", mergeAttributes(HTMLAttributes), 0]; + return ["block-video", mergeAttributes(HTMLAttributes), 0]; }, addNodeView() { diff --git a/front/components/Editor/Extensions/Video/VideoBlockComponent.tsx b/front/components/Editor/Extensions/Video/VideoBlockComponent.tsx new file mode 100644 index 00000000..671bf023 --- /dev/null +++ b/front/components/Editor/Extensions/Video/VideoBlockComponent.tsx @@ -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) => { + 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 ( + + {!fileObject && ( + +
+
+ +
+ +
+ )} + {fileObject && ( + + + + )} + {isLoading && ( +
+ +
+ )} +
+ ); +} +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; diff --git a/front/components/Editor/Extensions/Video/VideoBlockComponents.tsx b/front/components/Editor/Extensions/Video/VideoBlockComponents.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/front/components/Editor/Toolbar/ToolbarButtons.tsx b/front/components/Editor/Toolbar/ToolbarButtons.tsx index e793779a..87ea1c0d 100644 --- a/front/components/Editor/Toolbar/ToolbarButtons.tsx +++ b/front/components/Editor/Toolbar/ToolbarButtons.tsx @@ -1,6 +1,6 @@ import styled from "styled-components"; import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon } from "@radix-ui/react-icons"; -import { AlertCircle, AlertTriangle, ImagePlus, Info, Youtube } from "lucide-react"; +import { AlertCircle, AlertTriangle, ImagePlus, Info, Video, Youtube } from "lucide-react"; export const ToolbarButtons = ({ editor }: any) => { if (!editor) { @@ -74,6 +74,19 @@ export const ToolbarButtons = ({ editor }: any) => { > + + editor + .chain() + .focus() + .insertContent({ + type: "blockVideo", + }) + .run() + } + > + addYoutubeVideo()}> diff --git a/front/components/Security/AuthProvider.tsx b/front/components/Security/AuthProvider.tsx index 3eb88197..11905747 100644 --- a/front/components/Security/AuthProvider.tsx +++ b/front/components/Security/AuthProvider.tsx @@ -39,7 +39,7 @@ const AuthProvider = (props: any) => { } } else { setAuth({ access_token, isAuthenticated: false, userInfo, isLoading }); - router.push("/login"); + //router.push("/login"); } } catch (error) { router.push("/"); diff --git a/front/services/files/video.ts b/front/services/files/video.ts new file mode 100644 index 00000000..19210d7c --- /dev/null +++ b/front/services/files/video.ts @@ -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)); +} \ No newline at end of file diff --git a/src/services/files/videos.py b/src/services/files/videos.py index 6acec817..47b5a563 100644 --- a/src/services/files/videos.py +++ b/src/services/files/videos.py @@ -55,7 +55,8 @@ async def create_video_file(video_file: UploadFile, element_id: str): ) # create folder for element - os.mkdir(f"content/uploads/files/videos/{element_id}") + 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: