Refined media handling in tiptap, handle upload action in backend
This commit is contained in:
parent
d450ac3cb4
commit
50808b27b0
@ -96,5 +96,5 @@ class MediaSerializer(serializers.Serializer):
|
||||
media = serializers.ListField(
|
||||
child=serializers.FileField(max_length=100000, allow_empty_file=False, use_url=False)
|
||||
)
|
||||
resource_type = serializers.CharField(allow_blank=False)
|
||||
resource_id = serializers.CharField(allow_blank=False)
|
||||
resource_type = serializers.CharField(max_length=255, allow_blank=False)
|
||||
resource_id = serializers.CharField(max_length=255, allow_blank=False)
|
||||
@ -1,17 +1,10 @@
|
||||
#######################Django related imports####################
|
||||
import os
|
||||
import subprocess
|
||||
import ast
|
||||
import shutil
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponseRedirect
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework import generics, permissions, views, serializers, status
|
||||
from django.core.files.storage import default_storage
|
||||
from rest_framework import generics, status
|
||||
import random
|
||||
#################################################################
|
||||
#API related imports
|
||||
from .models import (
|
||||
@ -110,24 +103,22 @@ class BlogDeleteAPIView(generics.DestroyAPIView):
|
||||
lookup_field = 'blog_id'
|
||||
####################################################################
|
||||
|
||||
'''
|
||||
class MediaView(APIView):
|
||||
|
||||
class MediaUpload(APIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
file_serializer = FileSerializer(data=request.data)
|
||||
file_serializer = MediaSerializer(data=request.data)
|
||||
if file_serializer.is_valid():
|
||||
files = dict((f, f) for f in request.FILES.getlist('file'))
|
||||
nodeName = file_serializer.validated_data['nodeName']
|
||||
preferredFormat = file_serializer.validated_data['preferredFormat']
|
||||
for f in files.values():
|
||||
fileHandlerObject = FileHandler(f, preferredFormat, nodeName)
|
||||
fileProcessed = fileHandlerObject.handleUploadedFile()
|
||||
if not fileProcessed[0]:
|
||||
return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(file_serializer.data, status=status.HTTP_201_CREATED)
|
||||
files = request.FILES.getlist('media')
|
||||
resource_type = file_serializer.validated_data['resource_type']
|
||||
resource_id = file_serializer.validated_data['resource_id']
|
||||
file_path_base = f'static/rangolio_data'
|
||||
|
||||
for f in files:
|
||||
file_unique_slug = ''.join(random.choices('ABCDEabcde1234', k=5))
|
||||
file_path = f"{file_path_base}/{resource_type}/{resource_id}/media/{file_unique_slug+resource_id+f.name}"
|
||||
default_storage.save(file_path, f)
|
||||
|
||||
return Response(file_serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
@ -135,6 +126,7 @@ class MediaView(APIView):
|
||||
|
||||
|
||||
|
||||
'''
|
||||
class ETLFunctions(GenericAPIView):
|
||||
|
||||
serializer_class = ETLData
|
||||
|
||||
@ -32,6 +32,7 @@ from apimanager.views import (
|
||||
BlogRetrieveAPIView,
|
||||
BlogDeleteAPIView,
|
||||
BlogsByCategoryAPIView,
|
||||
MediaUpload
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@ -49,4 +50,5 @@ urlpatterns = [
|
||||
path('data/blog/<slug:blog_id>/', BlogRetrieveAPIView.as_view(), name='blog-retrieve-view'),
|
||||
path('data/blog/update/<slug:blog_id>/', BlogUpdateAPIView.as_view(), name='blog-update-view'),
|
||||
path('data/blog/delete/<slug:blog_id>/', BlogDeleteAPIView.as_view(), name='blog-delete-view'),
|
||||
path('data/upload/', MediaUpload.as_view(), name='media-upload'),
|
||||
]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 593 KiB |
@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "72e4d550-a19b-4b62-bf5a-13f98813d31a",
|
||||
"name": "Blog 1",
|
||||
"description": "A subtitle for Blog 1",
|
||||
"tagLine": "Read blog",
|
||||
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||
"contentBody": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec non ipsum in nunc pretium gravida id ut lectus. Duis ligula nisl, egestas a tortor nec, scelerisque hendrerit urna. Nullam gravida, ante id aliquet ultrices, justo metus aliquet augue, vestibulum posuere massa lacus at est. Cras vitae dolor euismod, volutpat quam eu, cursus enim. Maecenas in magna ut augue ultrices laoreet. Maecenas sapien sem, mollis sed ipsum nec, viverra vehicula quam. Sed et pulvinar justo. Quisque et vestibulum dui. Aliquam laoreet tempus neque, et eleifend nulla vestibulum eget. Cras tempus justo at nunc facilisis auctor. Duis facilisis tortor eu risus aliquam dapibus nec sit amet est. Maecenas ante lectus, sagittis eu facilisis sit amet, convallis eu ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean imperdiet vulputate ipsum sed scelerisque. Donec sit amet rutrum est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam gravida augue sem, quis aliquet justo varius non. Nam tortor nulla, bibendum sit amet finibus sed, ultricies non tellus. Aliquam condimentum risus ut felis porta iaculis. Nullam gravida mauris lacinia finibus hendrerit. Sed nec consectetur erat, sed tincidunt velit. Donec sit amet nulla at sem blandit imperdiet ut eu nisl. Sed condimentum, lectus quis sodales commodo, arcu turpis sodales ex, ac placerat mi turpis sit amet mi. Nullam lorem velit, porta eu quam eu, hendrerit egestas nibh. Sed non pellentesque arcu. Nam felis lectus, scelerisque in semper a, faucibus sit amet nibh. Cras est ligula, pretium id maximus nec, hendrerit eu ante. Maecenas tincidunt est ante, sed vestibulum mauris dignissim nec. Phasellus varius varius leo in pharetra. Aenean vestibulum id dui cursus lobortis. Mauris vel orci massa. Nullam vitae lorem mattis, lobortis dui in, pharetra velit. Aenean non urna ac felis volutpat consequat sit amet sed nisi. Quisque rutrum nisi ac erat ultricies tempor. Etiam nec pellentesque metus. Nulla tempus mi a ex rutrum, ut luctus tellus porta. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus et quam sit amet arcu mattis commodo quis non risus. Sed luctus non dui ullamcorper consectetur. Duis eros magna, tempus ut aliquam sit amet, tempor vel nibh. Vestibulum hendrerit odio convallis elit pretium dictum. Proin ligula dolor, finibus eget lacus sed, facilisis fringilla lorem. Quisque quis lacus sit amet massa blandit fringilla vel vitae tortor. Vivamus dictum nibh vel justo ullamcorper faucibus. Nullam vitae augue pretium erat semper rhoncus. Etiam tempus, arcu feugiat hendrerit pretium, nisi justo sollicitudin velit, sagittis elementum ante metus sed ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam quis ligula euismod, venenatis arcu vitae, condimentum arcu. Donec vitae nisl aliquam, rutrum arcu vel, sollicitudin justo. Vestibulum nec sagittis massa. Etiam maximus, erat vitae dapibus vulputate, velit lectus imperdiet est, sed lobortis ante erat lobortis arcu. Maecenas mollis nunc ut nisi tristique tempus. Nulla tempor est non dui scelerisque, eget semper augue consequat."
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e",
|
||||
"name": "Blog 2",
|
||||
"description": "A subtitle for Blog 2",
|
||||
"tagLine": "Read blog",
|
||||
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||
"contentBody": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec non ipsum in nunc pretium gravida id ut lectus. Duis ligula nisl, egestas a tortor nec, scelerisque hendrerit urna. Nullam gravida, ante id aliquet ultrices, justo metus aliquet augue, vestibulum posuere massa lacus at est. Cras vitae dolor euismod, volutpat quam eu, cursus enim. Maecenas in magna ut augue ultrices laoreet. Maecenas sapien sem, mollis sed ipsum nec, viverra vehicula quam. Sed et pulvinar justo. Quisque et vestibulum dui. Aliquam laoreet tempus neque, et eleifend nulla vestibulum eget. Cras tempus justo at nunc facilisis auctor. Duis facilisis tortor eu risus aliquam dapibus nec sit amet est. Maecenas ante lectus, sagittis eu facilisis sit amet, convallis eu ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean imperdiet vulputate ipsum sed scelerisque. Donec sit amet rutrum est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam gravida augue sem, quis aliquet justo varius non. Nam tortor nulla, bibendum sit amet finibus sed, ultricies non tellus. Aliquam condimentum risus ut felis porta iaculis. Nullam gravida mauris lacinia finibus hendrerit. Sed nec consectetur erat, sed tincidunt velit. Donec sit amet nulla at sem blandit imperdiet ut eu nisl. Sed condimentum, lectus quis sodales commodo, arcu turpis sodales ex, ac placerat mi turpis sit amet mi. Nullam lorem velit, porta eu quam eu, hendrerit egestas nibh. Sed non pellentesque arcu. Nam felis lectus, scelerisque in semper a, faucibus sit amet nibh. Cras est ligula, pretium id maximus nec, hendrerit eu ante. Maecenas tincidunt est ante, sed vestibulum mauris dignissim nec. Phasellus varius varius leo in pharetra. Aenean vestibulum id dui cursus lobortis. Mauris vel orci massa. Nullam vitae lorem mattis, lobortis dui in, pharetra velit. Aenean non urna ac felis volutpat consequat sit amet sed nisi. Quisque rutrum nisi ac erat ultricies tempor. Etiam nec pellentesque metus. Nulla tempus mi a ex rutrum, ut luctus tellus porta. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus et quam sit amet arcu mattis commodo quis non risus. Sed luctus non dui ullamcorper consectetur. Duis eros magna, tempus ut aliquam sit amet, tempor vel nibh. Vestibulum hendrerit odio convallis elit pretium dictum. Proin ligula dolor, finibus eget lacus sed, facilisis fringilla lorem. Quisque quis lacus sit amet massa blandit fringilla vel vitae tortor. Vivamus dictum nibh vel justo ullamcorper faucibus. Nullam vitae augue pretium erat semper rhoncus. Etiam tempus, arcu feugiat hendrerit pretium, nisi justo sollicitudin velit, sagittis elementum ante metus sed ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam quis ligula euismod, venenatis arcu vitae, condimentum arcu. Donec vitae nisl aliquam, rutrum arcu vel, sollicitudin justo. Vestibulum nec sagittis massa. Etiam maximus, erat vitae dapibus vulputate, velit lectus imperdiet est, sed lobortis ante erat lobortis arcu. Maecenas mollis nunc ut nisi tristique tempus. Nulla tempor est non dui scelerisque, eget semper augue consequat."
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "72e4d550-a19b-4b62-bf5a-13f98813d31a",
|
||||
"name": "Blog 1",
|
||||
"description": "A subtitle for Blog 1",
|
||||
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||
"tagLine": "Read more",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
|
||||
},
|
||||
{
|
||||
"id": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e",
|
||||
"name": "Blog 2",
|
||||
"description": "A subtitle for Blog 2",
|
||||
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||
"tagLine": "Read more",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
|
||||
}
|
||||
]
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"id": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||
"name": "Technology",
|
||||
"coverImage": "category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png",
|
||||
"tagLine": "Read articles about tech",
|
||||
"description": "I have been working in tech for long, and here are my thoughts of random stuff",
|
||||
"featuredBlog": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e"
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
[{
|
||||
|
||||
}]
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"id": "b9e0d686-351d-49af-8e3d-b62023f44dbe",
|
||||
"name": "Gaming",
|
||||
"coverImage": "category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png",
|
||||
"tagLine": "Read articles about games",
|
||||
"description": "I like to game, and here are my thoughts on games"
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||
"name": "Technology",
|
||||
"coverImage": "category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png",
|
||||
"tagLine": "Read articles about tech",
|
||||
"description": "I have been working in tech for long, and here are my thoughts of random stuff"
|
||||
},
|
||||
{
|
||||
"id": "b9e0d686-351d-49af-8e3d-b62023f44dbe",
|
||||
"name": "Gaming",
|
||||
"coverImage": "category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png",
|
||||
"tagLine": "Read articles about games",
|
||||
"description": "I like to game, and here are my thoughts on games"
|
||||
}
|
||||
]
|
||||
@ -1,38 +0,0 @@
|
||||
{
|
||||
"darkTheme": {
|
||||
"theme": "Dark Mode",
|
||||
"background": "bg-dark",
|
||||
"textColor": "text-white",
|
||||
"captionColor": "#8a8a8a",
|
||||
"fontAwesomeIcon": "faSun",
|
||||
"borderColor": "white",
|
||||
"categoryNavigator": "light",
|
||||
"navBar": {
|
||||
"navBarTheme": "navbar-dark",
|
||||
"background": "bg-secondary",
|
||||
"buttonColor": "light"
|
||||
},
|
||||
"footer": {
|
||||
"background": "bg-secondary",
|
||||
"text": "bg-white"
|
||||
}
|
||||
},
|
||||
"lightTheme":{
|
||||
"theme": "Light Mode",
|
||||
"background": "bg-light",
|
||||
"textColor": "text-black",
|
||||
"captionColor": "#605f5f",
|
||||
"fontAwesomeIcon": "faMoon",
|
||||
"borderColor": "black",
|
||||
"categoryNavigator": "dark",
|
||||
"navBar": {
|
||||
"navBarTheme": "navbar-light",
|
||||
"background": "bg-secondary",
|
||||
"buttonColor": "light"
|
||||
},
|
||||
"footer": {
|
||||
"background": "bg-secondary",
|
||||
"text": "bg-white"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "John Doe",
|
||||
"greetingLine": "Hi! My name is",
|
||||
"tagLine": "Me like tech. Checkout my blog where I have nice stuff!",
|
||||
"profilePhoto": "homepage/media/profile.png",
|
||||
"links": {
|
||||
"instagram": ""
|
||||
},
|
||||
"contact":{
|
||||
"email":"",
|
||||
"phone": ""
|
||||
}
|
||||
}
|
||||
@ -138,7 +138,7 @@ function Blog(props) {
|
||||
<Col xs="3" className="d-none d-md-block"></Col>
|
||||
|
||||
<Col className={`blogContent ${ThemeConfig[GlobalTheme].textColor}`} style={{marginBottom: '25px'}}>
|
||||
<EditorComponent setContent={setBlogContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig} content={blogContent}/>
|
||||
<EditorComponent notificationToggler={props.notificationToggler} setContent={setBlogContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig} content={blogContent} resourceType='blog' resourceId={blogData.id}/>
|
||||
<ButtonGroup className='mt-4'>
|
||||
<Button onClick={(event) => setInfo(event)} color={ThemeConfig[GlobalTheme].buttonColor} outline>Save Data</Button>
|
||||
<Button color={ThemeConfig[GlobalTheme].buttonColor} outline>Publish Data</Button>
|
||||
|
||||
46
frontend/src/components/editable/shared/file-component.jsx
Normal file
46
frontend/src/components/editable/shared/file-component.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Label, Input } from 'reactstrap';
|
||||
import EditableDataService from '../../../services/editable-data-service';
|
||||
|
||||
function FileComponent(props) {
|
||||
const [file, setFile] = useState(null);
|
||||
const [resourceType, setResourceType] = useState('');
|
||||
const [resourceId, setResourceId] = useState('');
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
setFile(event.target.files[0]); // Assuming single file upload
|
||||
};
|
||||
|
||||
const handleUpload = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('media', file);
|
||||
formData.append('resource_type', props.resourceType);
|
||||
formData.append('resource_id', props.resourceId);
|
||||
|
||||
try {
|
||||
const response = await EditableDataService.createData('/data/upload/', formData);
|
||||
props.notificationToggler('Media uploaded successfully')
|
||||
} catch (error) {
|
||||
props.notificationToggler('Media upload failed', 'danger')
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleUpload}>
|
||||
<Label for="exampleFile">
|
||||
File
|
||||
</Label>
|
||||
<Input
|
||||
id="exampleFile"
|
||||
name="file"
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Button className='mt-2' type="submit">Upload</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileComponent;
|
||||
59
frontend/src/components/editable/shared/media-upload.jsx
Normal file
59
frontend/src/components/editable/shared/media-upload.jsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { useState } from 'react';
|
||||
import FileComponent from './file-component.jsx';
|
||||
import { Button, ButtonGroup, Modal, ModalHeader, ModalBody, ModalFooter} from 'reactstrap';
|
||||
|
||||
function MediaUpload(props) {
|
||||
|
||||
const [action, setAction] = useState('insert')
|
||||
const toggleAction = () =>{
|
||||
if (action === 'insert')
|
||||
setAction('upload')
|
||||
if (action === 'upload')
|
||||
setAction('insert')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal isOpen={props.modal} toggle={props.toggle}>
|
||||
<ModalHeader toggle={props.toggle}>{props.modalTitle}</ModalHeader>
|
||||
<ModalBody>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
outline
|
||||
active={action === 'insert'}
|
||||
onClick={() => toggleAction()}
|
||||
>
|
||||
Insert Media
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
active={action === 'upload'}
|
||||
onClick={() => toggleAction()}
|
||||
>
|
||||
Upload Media
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div className="mt-3">
|
||||
{ action === 'insert' ?
|
||||
<div>
|
||||
<h4>
|
||||
Choose media to insert
|
||||
</h4>
|
||||
</div>:
|
||||
<div>
|
||||
<FileComponent notificationToggler={props.notificationToggler} resourceType={props.resourceType} resourceId={props.resourceId} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={props.toggle}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaUpload;
|
||||
@ -0,0 +1,52 @@
|
||||
/*
|
||||
|
||||
extension credits: Angelika Tyborska: https://angelika.me/2023/02/26/how-to-add-editing-image-alt-text-tiptap/
|
||||
|
||||
*/
|
||||
|
||||
import Image from '@tiptap/extension-image'
|
||||
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import {
|
||||
Button
|
||||
} from 'reactstrap';
|
||||
|
||||
function ImageNode(props) {
|
||||
const { src, alt } = props.node.attrs
|
||||
const { updateAttributes } = props
|
||||
const onEditAlt = () => {
|
||||
const newAlt = prompt('Set alt text:', alt || '')
|
||||
updateAttributes({ alt: newAlt })
|
||||
}
|
||||
|
||||
let className = 'image'
|
||||
if (props.selected) { className += ' ProseMirror-selectednode'}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={className} data-drag-handle>
|
||||
<div className="image-container">
|
||||
<img onClick={() => onEditAlt()} className='mx-auto d-block' src={src} alt={alt} />
|
||||
<div className="image-overlay">
|
||||
<span className="image-text mx-auto d-block">
|
||||
{ alt ?
|
||||
<span>✔</span> :
|
||||
<span>!</span>
|
||||
}
|
||||
{ alt ?
|
||||
<span className="text">Alt text: "{alt}".</span>:
|
||||
<span className="text">Alt text missing.</span>
|
||||
}
|
||||
<Button className="edit" type="button" onClick={onEditAlt}>
|
||||
Edit
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Image.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ImageNode)
|
||||
}
|
||||
})
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button, ButtonGroup, Label, Input } from 'reactstrap';
|
||||
import { Color } from '@tiptap/extension-color'
|
||||
@ -8,7 +8,6 @@ import Highlight from '@tiptap/extension-highlight'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
import Blockquote from '@tiptap/extension-blockquote'
|
||||
import Image from '@tiptap/extension-image'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
@ -21,7 +20,11 @@ import { faBold, faItalic,
|
||||
faListUl, faLink,
|
||||
faListOl, faQuoteLeft,
|
||||
faQuoteRight, faRulerHorizontal,
|
||||
faRotateLeft, faRotateRight } from '@fortawesome/free-solid-svg-icons';
|
||||
faRotateLeft, faRotateRight, faImage } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
import CustomImageExtension from './tiptap-custom-extensions/custom-image-extension.jsx'
|
||||
import MediaUpload from './media-upload.jsx'
|
||||
|
||||
const MenuBar = (props) => {
|
||||
const { editor } = useCurrentEditor()
|
||||
@ -310,6 +313,14 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faRotateRight}/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
className='mt-2 ms-2'
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => props.toggle()}
|
||||
outline
|
||||
>
|
||||
<FontAwesomeIcon icon={faImage}/>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -329,12 +340,7 @@ const extensions = [
|
||||
}),
|
||||
Underline,
|
||||
Blockquote,
|
||||
Image.configure({
|
||||
allowBase64: true,
|
||||
HTMLAttributes: {
|
||||
class: 'mx-auto d-block',
|
||||
},
|
||||
}),
|
||||
CustomImageExtension,
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
@ -349,8 +355,14 @@ export default (props) => {
|
||||
const GlobalTheme = props.GlobalTheme;
|
||||
const ThemeConfig = props.ThemeConfig;
|
||||
|
||||
const [modal, setModal] = useState(false);
|
||||
const toggle = () => setModal(!modal);
|
||||
|
||||
if (props.content && GlobalTheme && ThemeConfig)
|
||||
return (
|
||||
<EditorProvider slotBefore={<MenuBar setContent={props.setContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>} extensions={extensions} content={props.content}></EditorProvider>
|
||||
<>
|
||||
<MediaUpload notificationToggler={props.notificationToggler} modal={modal} toggle={toggle} resourceType={props.resourceType} resourceId={props.resourceId}></MediaUpload>
|
||||
<EditorProvider slotBefore={<MenuBar modal={modal} toggle={toggle} setContent={props.setContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>} extensions={extensions} content={props.content}></EditorProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
a {
|
||||
text-decoration: none !important; /* Removes underline */
|
||||
color: inherit !important; /* Inherits color from parent */
|
||||
border: none !important; /* Removes any borders */
|
||||
text-decoration: none !important;
|
||||
color: inherit !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto; /* Header size, flexible content, footer size */
|
||||
grid-template-rows: auto 1fr auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@ -42,7 +42,27 @@ a {
|
||||
|
||||
.blogContent img {
|
||||
display: flex;
|
||||
justify-content: center; /* Center horizontally */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
}
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.image-text {
|
||||
display: block;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user