mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add pdf extension to editor
This commit is contained in:
parent
429a95c50a
commit
fdcee29a0d
7 changed files with 297 additions and 6 deletions
|
|
@ -20,6 +20,8 @@ import Youtube from "@tiptap/extension-youtube";
|
||||||
import VideoBlock from "./Extensions/Video/VideoBlock";
|
import VideoBlock from "./Extensions/Video/VideoBlock";
|
||||||
import { Save } from "lucide-react";
|
import { Save } from "lucide-react";
|
||||||
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
|
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
|
||||||
|
import PDFBlockComponent from "./Extensions/PDF/PDFBlockComponent";
|
||||||
|
import PDFBlock from "./Extensions/PDF/PDFBlock";
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -59,6 +61,10 @@ function Editor(props: Editor) {
|
||||||
editable: true,
|
editable: true,
|
||||||
lecture: props.lecture,
|
lecture: props.lecture,
|
||||||
}),
|
}),
|
||||||
|
PDFBlock.configure({
|
||||||
|
editable: true,
|
||||||
|
lecture: props.lecture,
|
||||||
|
}),
|
||||||
Youtube.configure({
|
Youtube.configure({
|
||||||
controls: true,
|
controls: true,
|
||||||
modestBranding: true,
|
modestBranding: true,
|
||||||
|
|
|
||||||
35
front/components/Editor/Extensions/PDF/PDFBlock.ts
Normal file
35
front/components/Editor/Extensions/PDF/PDFBlock.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { mergeAttributes, Node } from "@tiptap/core";
|
||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
|
||||||
|
import PDFBlockComponent from "./PDFBlockComponent";
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: "blockPDF",
|
||||||
|
group: "block",
|
||||||
|
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
fileObject: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "block-pdf",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["block-pdf", mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(PDFBlockComponent);
|
||||||
|
},
|
||||||
|
});
|
||||||
88
front/components/Editor/Extensions/PDF/PDFBlockComponent.tsx
Normal file
88
front/components/Editor/Extensions/PDF/PDFBlockComponent.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { AlertCircle, AlertTriangle, FileText, Image, ImagePlus, Info } from "lucide-react";
|
||||||
|
import { getPDFFile, uploadNewPDFFile } from "../../../../services/files/documents";
|
||||||
|
import { getBackendUrl } from "../../../../services/config";
|
||||||
|
|
||||||
|
function PDFBlockComponent(props: any) {
|
||||||
|
const [pdf, setPDF] = React.useState(null);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [fileObject, setfileObject] = React.useState(props.node.attrs.fileObject);
|
||||||
|
|
||||||
|
const handlePDFChange = (event: React.ChangeEvent<any>) => {
|
||||||
|
setPDF(event.target.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
let object = await uploadNewPDFFile(pdf, props.extension.options.lecture.lecture_id);
|
||||||
|
setIsLoading(false);
|
||||||
|
setfileObject(object);
|
||||||
|
props.updateAttributes({
|
||||||
|
fileObject: object,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className="block-pdf">
|
||||||
|
{!fileObject && (
|
||||||
|
<BlockPDFWrapper contentEditable={props.extension.options.editable}>
|
||||||
|
<div>
|
||||||
|
<FileText color="#e1e0e0" size={50} />
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<input onChange={handlePDFChange} type="file" name="" id="" />
|
||||||
|
<br />
|
||||||
|
<button onClick={handleSubmit}>Submit</button>
|
||||||
|
</BlockPDFWrapper>
|
||||||
|
)}
|
||||||
|
{fileObject && (
|
||||||
|
<BlockPDF>
|
||||||
|
<iframe
|
||||||
|
src={`${getBackendUrl()}content/uploads/files/documents/${props.extension.options.lecture.lecture_id}/${fileObject.file_id}.${
|
||||||
|
fileObject.file_format
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</BlockPDF>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<div>
|
||||||
|
<AlertTriangle color="#e1e0e0" size={50} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PDFBlockComponent;
|
||||||
|
|
||||||
|
const BlockPDFWrapper = 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 BlockPDF = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 300px;
|
||||||
|
// cover
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const PDFNotFound = styled.div``;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon } from "@radix-ui/react-icons";
|
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon } from "@radix-ui/react-icons";
|
||||||
import { AlertCircle, AlertTriangle, ImagePlus, Info, Sigma, Video, Youtube } from "lucide-react";
|
import { AlertCircle, AlertTriangle, FileText, ImagePlus, Info, Sigma, Video, Youtube } from "lucide-react";
|
||||||
|
|
||||||
export const ToolbarButtons = ({ editor }: any) => {
|
export const ToolbarButtons = ({ editor }: any) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
|
@ -103,6 +103,19 @@ export const ToolbarButtons = ({ editor }: any) => {
|
||||||
>
|
>
|
||||||
<Sigma size={15} />
|
<Sigma size={15} />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
|
<ToolBtn
|
||||||
|
onClick={() =>
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContent({
|
||||||
|
type: "blockPDF",
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FileText size={15} />
|
||||||
|
</ToolBtn>
|
||||||
</ToolButtonsWrapper>
|
</ToolButtonsWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
20
front/services/files/documents.ts
Normal file
20
front/services/files/documents.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { getAPIUrl } from "@services/config";
|
||||||
|
import { RequestBody, RequestBodyForm } from "@services/utils/requests";
|
||||||
|
|
||||||
|
export async function uploadNewPDFFile(file: any, lecture_id: string) {
|
||||||
|
// Send file thumbnail as form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file_object", file);
|
||||||
|
formData.append("lecture_id", lecture_id);
|
||||||
|
|
||||||
|
return fetch(`${getAPIUrl()}files/document`, RequestBodyForm("POST", formData))
|
||||||
|
.then((result) => result.json())
|
||||||
|
.catch((error) => console.log("error", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPDFFile(file_id: string) {
|
||||||
|
// todo : add course id to url
|
||||||
|
return fetch(`${getAPIUrl()}files/document?file_id=${file_id}`, RequestBody("GET", null))
|
||||||
|
.then((result) => result.json())
|
||||||
|
.catch((error) => console.log("error", error));
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||||
from src.dependencies.auth import get_current_user
|
from src.dependencies.auth import get_current_user
|
||||||
from fastapi import HTTPException, status, UploadFile
|
from fastapi import HTTPException, status, UploadFile
|
||||||
|
from src.services.files.documents import create_document_file, get_document_file
|
||||||
|
|
||||||
from src.services.files.pictures import create_picture_file, get_picture_file
|
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.files.videos import create_video_file, get_video_file
|
||||||
|
|
@ -10,15 +11,15 @@ router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/picture")
|
@router.post("/picture")
|
||||||
async def api_create_picture_file(request: Request,file_object: UploadFile, lecture_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
async def api_create_picture_file(request: Request, file_object: UploadFile, lecture_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
Create new picture file
|
Create new picture file
|
||||||
"""
|
"""
|
||||||
return await create_picture_file(request,file_object, lecture_id)
|
return await create_picture_file(request, file_object, lecture_id)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/video")
|
@router.post("/video")
|
||||||
async def api_create_video_file(request: Request,file_object: UploadFile,lecture_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
async def api_create_video_file(request: Request, file_object: UploadFile, lecture_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
Create new video file
|
Create new video file
|
||||||
"""
|
"""
|
||||||
|
|
@ -26,7 +27,7 @@ async def api_create_video_file(request: Request,file_object: UploadFile,lecture
|
||||||
|
|
||||||
|
|
||||||
@router.get("/picture")
|
@router.get("/picture")
|
||||||
async def api_get_picture_file(request: Request,file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
async def api_get_picture_file(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
Get picture file
|
Get picture file
|
||||||
"""
|
"""
|
||||||
|
|
@ -34,8 +35,24 @@ async def api_get_picture_file(request: Request,file_id: str, current_user: Publ
|
||||||
|
|
||||||
|
|
||||||
@router.get("/video")
|
@router.get("/video")
|
||||||
async def api_get_video_file(request: Request,file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
async def api_get_video_file(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
Get video file
|
Get video file
|
||||||
"""
|
"""
|
||||||
return await get_video_file(request, file_id, current_user)
|
return await get_video_file(request, file_id, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/document")
|
||||||
|
async def api_get_document_file(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Get document file
|
||||||
|
"""
|
||||||
|
return await get_document_file(request, file_id, current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/document")
|
||||||
|
async def api_create_document_file(request: Request, file_object: UploadFile, lecture_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Create new document file
|
||||||
|
"""
|
||||||
|
return await create_document_file(request, file_object, lecture_id)
|
||||||
|
|
|
||||||
112
src/services/files/documents.py
Normal file
112
src/services/files/documents.py
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi import HTTPException, status, UploadFile, Request
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from src.services.users import PublicUser
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentFile(BaseModel):
|
||||||
|
file_id: str
|
||||||
|
file_format: str
|
||||||
|
file_name: str
|
||||||
|
file_size: int
|
||||||
|
file_type: str
|
||||||
|
lecture_id: str
|
||||||
|
|
||||||
|
|
||||||
|
async def create_document_file(request: Request, document_file: UploadFile, lecture_id: str):
|
||||||
|
documents = request.app.db["files"]
|
||||||
|
|
||||||
|
# generate file_id
|
||||||
|
file_id = str(f"file_{uuid4()}")
|
||||||
|
|
||||||
|
# get file format
|
||||||
|
file_format = document_file.filename.split(".")[-1]
|
||||||
|
|
||||||
|
# validate file format
|
||||||
|
if file_format not in ["pdf"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Document file format not supported")
|
||||||
|
|
||||||
|
# create file
|
||||||
|
file = await document_file.read()
|
||||||
|
|
||||||
|
# get file size
|
||||||
|
file_size = len(file)
|
||||||
|
|
||||||
|
# get file type
|
||||||
|
file_type = document_file.content_type
|
||||||
|
|
||||||
|
# get file name
|
||||||
|
file_name = document_file.filename
|
||||||
|
|
||||||
|
# create file object
|
||||||
|
uploadable_file = DocumentFile(
|
||||||
|
file_id=file_id,
|
||||||
|
file_format=file_format,
|
||||||
|
file_name=file_name,
|
||||||
|
file_size=file_size,
|
||||||
|
file_type=file_type,
|
||||||
|
lecture_id=lecture_id
|
||||||
|
)
|
||||||
|
# TODO : this is probably not working as intended
|
||||||
|
# create folder for lecture
|
||||||
|
if not os.path.exists(f"content/uploads/files/documents/{lecture_id}"):
|
||||||
|
os.mkdir(f"content/uploads/files/documents/{lecture_id}")
|
||||||
|
|
||||||
|
# upload file to server
|
||||||
|
with open(f"content/uploads/files/documents/{lecture_id}/{file_id}.{file_format}", 'wb') as f:
|
||||||
|
f.write(file)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# insert file object into database
|
||||||
|
document_file_in_db = documents.insert_one(uploadable_file.dict())
|
||||||
|
|
||||||
|
if not document_file_in_db:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Document file could not be created")
|
||||||
|
|
||||||
|
return uploadable_file
|
||||||
|
|
||||||
|
|
||||||
|
async def get_document_object(request: Request, file_id: str):
|
||||||
|
documents = request.app.db["files"]
|
||||||
|
|
||||||
|
document_file = documents.find_one({"file_id": file_id})
|
||||||
|
|
||||||
|
if document_file:
|
||||||
|
document_file = DocumentFile(**document_file)
|
||||||
|
return document_file
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Document file does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_document_file(request: Request, file_id: str, current_user: PublicUser):
|
||||||
|
documents = request.app.db["files"]
|
||||||
|
|
||||||
|
document_file = documents.find_one({"file_id": file_id})
|
||||||
|
|
||||||
|
# TODO : check if user has access to file
|
||||||
|
|
||||||
|
if document_file:
|
||||||
|
|
||||||
|
# check media type
|
||||||
|
if document_file.format not in ["jpg", "jpeg", "png", "gif"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Document file format not supported")
|
||||||
|
|
||||||
|
# stream file
|
||||||
|
document_file = DocumentFile(**document_file)
|
||||||
|
file_format = document_file.file_format
|
||||||
|
lecture_id = document_file.lecture_id
|
||||||
|
file = open(
|
||||||
|
f"content/uploads/files/documents/{lecture_id}/{file_id}.{file_format}", 'rb')
|
||||||
|
return StreamingResponse(file, media_type=document_file.file_type)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Document file does not exist")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue