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 styled from "styled-components";
|
||||
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";
|
||||
// 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 {
|
||||
content: string;
|
||||
|
|
@ -26,23 +32,43 @@ function Editor(props: Editor) {
|
|||
const auth: any = React.useContext(AuthContext);
|
||||
|
||||
const editor: any = useEditor({
|
||||
editable: true,
|
||||
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
// 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
|
||||
Collaboration.configure({
|
||||
document: props.ydoc,
|
||||
}),
|
||||
// Collaboration.configure({
|
||||
// document: props.ydoc,
|
||||
// }),
|
||||
// Register the collaboration cursor extension
|
||||
CollaborationCursor.configure({
|
||||
provider: props.provider,
|
||||
user: {
|
||||
name: auth.userInfo.username,
|
||||
color: "#f783ac",
|
||||
},
|
||||
}),
|
||||
// CollaborationCursor.configure({
|
||||
// provider: props.provider,
|
||||
// user: {
|
||||
// name: auth.userInfo.username,
|
||||
// color: "#f783ac",
|
||||
// },
|
||||
// }),
|
||||
],
|
||||
|
||||
content: props.content,
|
||||
|
|
@ -65,15 +91,13 @@ function Editor(props: Editor) {
|
|||
<EditorTop>
|
||||
<EditorDocSection>
|
||||
<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>
|
||||
<EditorInfoDocName>
|
||||
{" "}
|
||||
<b>{props.course.course.name}</b> <SlashIcon /> {props.element.name}{" "}
|
||||
</EditorInfoDocName>
|
||||
<EditorSaveButton onClick={() => props.setContent(editor.getJSON())}>
|
||||
Save
|
||||
</EditorSaveButton>
|
||||
<EditorSaveButton onClick={() => props.setContent(editor.getJSON())}>Save</EditorSaveButton>
|
||||
</EditorInfoWrapper>
|
||||
<EditorButtonsWrapper>
|
||||
<ToolbarButtons editor={editor} />
|
||||
|
|
@ -90,7 +114,6 @@ function Editor(props: Editor) {
|
|||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.99 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
key="modal"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 360,
|
||||
|
|
@ -106,6 +129,7 @@ function Editor(props: Editor) {
|
|||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
const Page = styled.div`
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
|
|
@ -113,13 +137,13 @@ const Page = styled.div`
|
|||
min-width: 100vw;
|
||||
padding-top: 30px;
|
||||
|
||||
// dots background
|
||||
// dots background
|
||||
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`
|
||||
background-color: #ffffffb8;
|
||||
|
|
@ -224,7 +248,7 @@ const EditorInfoThumbnail = styled.img`
|
|||
}
|
||||
`;
|
||||
|
||||
const EditorContentWrapper = styled.div`
|
||||
export const EditorContentWrapper = styled.div`
|
||||
margin: 40px;
|
||||
margin-top: 90px;
|
||||
background-color: white;
|
||||
|
|
@ -236,7 +260,8 @@ const EditorContentWrapper = styled.div`
|
|||
.ProseMirror {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 6px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
padding-top: 1px;
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
|
@ -244,6 +269,17 @@ const EditorContentWrapper = styled.div`
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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 { 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) => {
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// YouTube extension
|
||||
|
||||
const addYoutubeVideo = () => {
|
||||
const url = prompt("Enter YouTube URL");
|
||||
|
||||
if (url) {
|
||||
editor.commands.setYoutubeVideo({
|
||||
src: url,
|
||||
width: 640,
|
||||
height: 480,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolButtonsWrapper>
|
||||
<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" : ""}>
|
||||
<StrikethroughIcon />
|
||||
</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="2">Heading 2</option>
|
||||
<option value="3">Heading 3</option>
|
||||
|
|
@ -31,6 +54,42 @@ export const ToolbarButtons = ({ editor }: any) => {
|
|||
<option value="5">Heading 5</option>
|
||||
<option value="6">Heading 6</option>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
@ -49,10 +108,13 @@ const ToolBtn = styled.div`
|
|||
width: 25px;
|
||||
height: 25px;
|
||||
padding: 5px;
|
||||
font-size: 5px;
|
||||
margin-right: 5px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(176, 176, 176, 0.5);
|
||||
|
||||
|
|
|
|||
|
|
@ -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("/");
|
||||
|
|
|
|||
35
front/package-lock.json
generated
35
front/package-lock.json
generated
|
|
@ -12,11 +12,13 @@
|
|||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@tiptap/extension-collaboration": "^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/react": "^2.0.0-beta.199",
|
||||
"@tiptap/starter-kit": "^2.0.0-beta.199",
|
||||
"avvvatars-react": "^0.4.2",
|
||||
"framer-motion": "^7.3.6",
|
||||
"lucide-react": "^0.104.1",
|
||||
"next": "12.3.1",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
|
|
@ -1296,6 +1298,18 @@
|
|||
"@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": {
|
||||
"version": "2.0.0-beta.202",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-2.0.0-beta.202.tgz",
|
||||
|
|
@ -3532,6 +3546,15 @@
|
|||
"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": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
|
|
@ -5961,6 +5984,12 @@
|
|||
"integrity": "sha512-ntOqEhkBjDHrdzxvpPe4U1JB5GgE9/yyWqWdgzSL9lpSndRTJN1xQLOmyuv0qsLqOgBHn1YITHvaxPb3t8FrFw==",
|
||||
"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": {
|
||||
"version": "2.0.0-beta.202",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/html/-/html-2.0.0-beta.202.tgz",
|
||||
|
|
@ -7577,6 +7606,12 @@
|
|||
"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": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@
|
|||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@tiptap/extension-collaboration": "^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/react": "^2.0.0-beta.199",
|
||||
"@tiptap/starter-kit": "^2.0.0-beta.199",
|
||||
"avvvatars-react": "^0.4.2",
|
||||
"framer-motion": "^7.3.6",
|
||||
"lucide-react": "^0.104.1",
|
||||
"next": "12.3.1",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ import styled from "styled-components";
|
|||
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { PreAlphaLabel } from "../components//UI/Layout";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<HomePage>
|
||||
<PreAlphaLabel>🚧 Pre-Alpha</PreAlphaLabel>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
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 React, { useMemo } from "react";
|
||||
import Layout from "../../../../../../../components//UI/Layout";
|
||||
import { getElement } from "../../../../../../../services/courses/elements";
|
||||
import { getBackendUrl } from "../../../../../../../services/config";
|
||||
import Canva from "../../../../../../../components/Canva/Canva";
|
||||
|
||||
function ElementPage() {
|
||||
const router = useRouter();
|
||||
|
|
@ -30,35 +25,6 @@ function ElementPage() {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 (
|
||||
<Layout>
|
||||
{isLoading ? (
|
||||
|
|
@ -69,7 +35,7 @@ function ElementPage() {
|
|||
<h1>{element.name} </h1>
|
||||
<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 */}
|
||||
{element.type == "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
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"paths": {
|
||||
"@components/*": ["components/*"],
|
||||
"@public/*": ["public/*"],
|
||||
"@images/*": ["public/img/*"],
|
||||
"@services/*": ["services/*"],
|
||||
"@editor/*": ["components/Editor/*"]
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
||||
|
|
@ -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(orgs.router, prefix="/orgs", tags=["orgs"])
|
||||
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(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||
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
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
|
|
|
|||
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 os
|
||||
from typing import List
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.services.courses.elements.elements import ElementInDB
|
||||
from src.services.uploads import upload_thumbnail
|
||||
from src.services.users import PublicUser, User
|
||||
from src.services.database import create_config_collection, check_database, create_database, learnhouseDB
|
||||
from src.services.courses.thumbnails import upload_thumbnail
|
||||
from src.services.users import PublicUser
|
||||
from src.services.database import check_database, learnhouseDB
|
||||
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
|
||||
|
||||
#### Classes ####################################################
|
||||
|
|
@ -79,7 +78,6 @@ async def get_course_meta(course_id: str, current_user: PublicUser):
|
|||
course = courses.find_one({"course_id": course_id})
|
||||
elements = learnhouseDB["elements"]
|
||||
|
||||
|
||||
# verify course rights
|
||||
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(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist")
|
||||
|
||||
|
||||
coursechapters = coursechapters.find(
|
||||
{"course_id": course_id}).sort("name", 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,7 @@
|
|||
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()
|
||||
|
||||
|
||||
async def upload_video(video_file, name_in_disk, element_id):
|
||||
async def upload_video(video_file, element_id):
|
||||
contents = video_file.file.read()
|
||||
video_format = video_file.filename.split(".")[-1]
|
||||
# create folder
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
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.uploads import upload_video
|
||||
from src.services.users import PublicUser, User
|
||||
from src.services.courses.elements.elements import ElementInDB, Element
|
||||
from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File
|
||||
from src.services.courses.elements.uploads.videos import upload_video
|
||||
from src.services.users import PublicUser
|
||||
from src.services.courses.elements.elements import ElementInDB
|
||||
from fastapi import HTTPException, status, UploadFile
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ async def create_video_element(name: str, coursechapter_id: str, current_user:
|
|||
print("uploading video")
|
||||
# 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
|
||||
# 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