diff --git a/backend/apimanager/views.py b/backend/apimanager/views.py index fa559b2..29ec7fb 100644 --- a/backend/apimanager/views.py +++ b/backend/apimanager/views.py @@ -93,7 +93,6 @@ class BlogsByCategoryAPIView(APIView): class BlogCreateAPIView(generics.CreateAPIView): queryset = Blog.objects.all() serializer_class = BlogSerializer - lookup_field = 'blog_id' class BlogUpdateAPIView(generics.RetrieveUpdateAPIView): queryset = Blog.objects.all() @@ -105,16 +104,6 @@ class BlogRetrieveAPIView(generics.RetrieveAPIView): serializer_class = BlogSerializer lookup_field = 'blog_id' -class CategoryDetailView(APIView): - def get(self, request, category_id): - try: - category = Category.objects.get(category_id=category_id) - except Category.DoesNotExist: - return Response({'message': 'Category not found'}, status=404) - - serializer = UnifiedCategoryBlogSerializer(category) - return Response(serializer.data) - class BlogDeleteAPIView(generics.DestroyAPIView): queryset = Blog.objects.all() serializer_class = BlogSerializer diff --git a/backend/backend/urls.py b/backend/backend/urls.py index a0cf6bc..4073d56 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -45,8 +45,8 @@ urlpatterns = [ path('data/category//', BlogsByCategoryAPIView.as_view(), name='blogs-by-category-view'), path('data/category/update//', CategoryUpdateAPIView.as_view(), name='category-update-view'), path('data/category/delete//', CategoryDeleteAPIView.as_view(), name='category-delete-view'), + path('data/blog/create/', BlogCreateAPIView.as_view(), name='blog-create-view'), path('data/blog//', BlogRetrieveAPIView.as_view(), name='blog-retrieve-view'), - path('data/blog/create//', BlogCreateAPIView.as_view(), name='blog-create-view'), path('data/blog/update//', BlogUpdateAPIView.as_view(), name='blog-update-view'), path('data/blog/delete//', BlogDeleteAPIView.as_view(), name='blog-delete-view'), ] \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1961e0c..a8e925f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,7 +14,7 @@ "@tiptap/extension-blockquote": "^2.3.2", "@tiptap/extension-color": "^2.3.2", "@tiptap/extension-highlight": "^2.3.2", - "@tiptap/extension-image": "^2.3.2", + "@tiptap/extension-image": "^2.4.0", "@tiptap/extension-link": "^2.3.2", "@tiptap/extension-list-item": "^2.3.2", "@tiptap/extension-text-align": "^2.3.2", @@ -25,8 +25,10 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "html-react-parser": "^5.1.10", + "interactjs": "^1.10.27", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-resizable": "^3.0.5", "react-router-dom": "^6.22.3", "reactstrap": "^9.2.2", "sass": "^1.75.0", @@ -920,6 +922,11 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@interactjs/types": { + "version": "1.10.27", + "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", + "integrity": "sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1453,9 +1460,9 @@ } }, "node_modules/@tiptap/extension-image": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.3.2.tgz", - "integrity": "sha512-otkhqToHnjjpWOIswuotfK/PTPEOhhKRFPf1NuXvqHpMNulz+J1uIuA9R/B1m+bXkxZzCMKkWQi50vjqH9idVg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.4.0.tgz", + "integrity": "sha512-NIVhRPMO/ONo8OywEd+8zh0Q6Q7EbFHtBxVsvfOKj9KtZkaXQfUO4MzONTyptkvAchTpj9pIzeaEY5fyU87gFA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2211,6 +2218,14 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3501,6 +3516,14 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" }, + "node_modules/interactjs": { + "version": "1.10.27", + "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", + "integrity": "sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==", + "dependencies": { + "@interactjs/types": "1.10.27" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -4706,6 +4729,19 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -4744,6 +4780,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-router": { "version": "6.22.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7dd7046..7711352 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,7 @@ "@tiptap/extension-blockquote": "^2.3.2", "@tiptap/extension-color": "^2.3.2", "@tiptap/extension-highlight": "^2.3.2", - "@tiptap/extension-image": "^2.3.2", + "@tiptap/extension-image": "^2.4.0", "@tiptap/extension-link": "^2.3.2", "@tiptap/extension-list-item": "^2.3.2", "@tiptap/extension-text-align": "^2.3.2", @@ -30,8 +30,10 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "html-react-parser": "^5.1.10", + "interactjs": "^1.10.27", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-resizable": "^3.0.5", "react-router-dom": "^6.22.3", "reactstrap": "^9.2.2", "sass": "^1.75.0", diff --git a/frontend/src/components/editable/blog-list.jsx b/frontend/src/components/editable/blog-list.jsx index c382709..7326391 100755 --- a/frontend/src/components/editable/blog-list.jsx +++ b/frontend/src/components/editable/blog-list.jsx @@ -11,56 +11,80 @@ import { Card, Row, Col, + Button, CardImg, CardTitle, CardText, CardBody } from 'reactstrap'; -import { Link, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; +import { faLeftLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; function BlogList(props) { const { categoryID } = useParams(); + let navigate = useNavigate(); const GlobalTheme = props.GlobalTheme; const ThemeConfig = props.ThemeConfig; const [categoryData, setCategoryData] = useState('loading'); - const [currentPage, setCurrentPage] = useState('loading'); - useEffect(() => { + const loadBlogs = () => { EditableDataService.getData(`/data/category/${categoryID}/`).then(response => { let responseData = response.data let blogMetadata = [] console.log(responseData) let localCategoryData = { - "id": responseData["category_id"], - "name": responseData["name"], - "coverImage": responseData["cover_image"], - "tagLine": responseData["tagline"], - "description": responseData["description"], - "featuredBlog": responseData["featured_id"], - "blogMetadata": responseData["blog_metadata"] - } + "id": responseData["category_id"], + "name": responseData["name"], + "coverImage": responseData["cover_image"], + "tagLine": responseData["tagline"], + "description": responseData["description"], + "featuredBlog": responseData["featured_id"], + "blogMetadata": responseData["blog_metadata"] + } for (let eachBlog of responseData["blog_metadata"]){ - blogMetadata.push({ - "id": eachBlog["blog_id"], - "name": eachBlog["name"], - "description": eachBlog["description"], - "tagLine": eachBlog["tagline"], - "coverImage": eachBlog["cover_image"], - "parentCategory": eachBlog["parent_category"] - }) + blogMetadata.push({ + "id": eachBlog["blog_id"], + "name": eachBlog["name"], + "description": eachBlog["description"], + "tagLine": eachBlog["tagline"], + "coverImage": eachBlog["cover_image"], + "parentCategory": eachBlog["parent_category"] + }) } localCategoryData.blogMetadata = blogMetadata setCategoryData(localCategoryData) } - ); + ); + } + + useEffect(() => { + loadBlogs() }, [categoryID]); + const addNewBlog = () => { + EditableDataService.createData(`/data/blog/create/`, { + "name": "Enter a blog name", + "description": "Enter a description", + "tagline": "Enter a tagline", + "cover_image": "", + "content_body": "

", + "parent_category": categoryID + }).then(response => { + props.notificationToggler("New blog created") + loadBlogs() + }).catch(error => { + props.notificationToggler('Failed to add a new blog', 'danger'); + }); + } + if (GlobalTheme && ThemeConfig) { return ( + @@ -69,6 +93,7 @@ return ( {`Blogs in ${categoryData.name}`} + diff --git a/frontend/src/components/editable/blog.jsx b/frontend/src/components/editable/blog.jsx index c3a29b3..862d62c 100755 --- a/frontend/src/components/editable/blog.jsx +++ b/frontend/src/components/editable/blog.jsx @@ -4,14 +4,20 @@ import EditableDataService from '../../services/editable-data-service'; import MediaService from '../../services/media-service' import CategoryBar from './shared/category-bar'; import EditorComponent from './shared/tiptap'; +import ModalComponent from './shared/modal-component'; + +import { faLeftLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Container,Row, Col,Spinner, UncontrolledCollapse, Button, ButtonGroup, Card, CardBody, Input, InputGroup, InputGroupText } from 'reactstrap'; -import { Link, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; function Blog(props) { + let navigate = useNavigate(); + const nameField = useRef(null); const descriptionField = useRef(null); const tagLineField = useRef(null); @@ -23,6 +29,11 @@ function Blog(props) { const [blogData, setBlogData] = useState([]); const [blogContent, setBlogContent] = useState(); + const [modal, setModal] = useState(false); + const [modalText, setModalText] = useState(false); + const [modalTitle, setModalTitle] = useState(false); + + const toggle = () => setModal(!modal); const setInfo = (event) => { EditableDataService.updateData(`/data/blog/update/${blogID}/`,{ @@ -38,6 +49,22 @@ function Blog(props) { }); } + const showModal = () => { + setModalTitle('Confirm') + setModalText('Are you sure that you wish to delete this blog?') + toggle() + } + + const deleteResource = () => { + EditableDataService.deleteData(`/data/blog/delete/${blogData.id}/`).then(response => { + props.notificationToggler('Blog successfully deleted') + navigate(`/categories/${blogData.parentCategory}`); + }).catch(error => { + props.notificationToggler('Failed to delete blog', 'danger'); + }); + toggle() + } + const getInfo = () => { EditableDataService.getData(`/data/blog/${blogID}/`).then(response => { let responseData = response.data @@ -61,19 +88,25 @@ function Blog(props) { if (GlobalTheme && ThemeConfig && blogData) { return ( - + + + - Banner + { + blogData.coverImage !== "" ? + Banner:"" + } = 765 ? '6':''}`}> + Name diff --git a/frontend/src/components/editable/category-list.jsx b/frontend/src/components/editable/category-list.jsx index 7b153c4..c9dfd8c 100755 --- a/frontend/src/components/editable/category-list.jsx +++ b/frontend/src/components/editable/category-list.jsx @@ -19,9 +19,13 @@ import { Button, ButtonGroup } from 'reactstrap'; -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { faLeftLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; function Blogs(props) { + + let navigate = useNavigate(); const GlobalTheme = props.GlobalTheme; const ThemeConfig = props.ThemeConfig; @@ -65,7 +69,7 @@ function Blogs(props) { const deleteResource = (id) => { EditableDataService.deleteData(`/data/category/delete/${id}/`).then(response => { - props.notificationToggler('Category delete successfully') + props.notificationToggler('Category successfully deleted') setCategoryData() }).catch(error => { props.notificationToggler('Failed to delete category', 'danger'); @@ -74,10 +78,10 @@ function Blogs(props) { const addNewCategory = () => { EditableDataService.createData('/data/category/create/', { - "name": "Enter a blog name", + "name": "Enter name", "featured_blog": "", "description": "Enter description", - "tagline": "Enter category tagline", + "tagline": "Enter tagline", "cover_image": "" }).then(response => { props.notificationToggler('Category created successfully') @@ -99,9 +103,10 @@ function Blogs(props) { setCategoryData() } - if (GlobalTheme && ThemeConfig && categoryMetadata.length > 0) { + if (GlobalTheme && ThemeConfig) { return ( +
diff --git a/frontend/src/components/editable/shared/card-list-viewer.jsx b/frontend/src/components/editable/shared/card-list-viewer.jsx index ee918d6..6376930 100755 --- a/frontend/src/components/editable/shared/card-list-viewer.jsx +++ b/frontend/src/components/editable/shared/card-list-viewer.jsx @@ -116,7 +116,7 @@ function CardListViewer(props) { Open this resource - + @@ -128,13 +128,13 @@ function CardListViewer(props) { {itemObject.coverImage !== "" ? : ""} - + {itemObject.name} - + {itemObject.description} - + {itemObject.tagLine} diff --git a/frontend/src/components/editable/shared/tiptap.jsx b/frontend/src/components/editable/shared/tiptap.jsx index c8e47d8..25ca9dc 100755 --- a/frontend/src/components/editable/shared/tiptap.jsx +++ b/frontend/src/components/editable/shared/tiptap.jsx @@ -8,6 +8,7 @@ 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' @@ -18,7 +19,7 @@ import { faBold, faItalic, faAlignJustify, faHighlighter, faStrikethrough, faCode, faListUl, faLink, - faListOl, faQuoteLeft, + faListOl, faQuoteLeft, faQuoteRight, faRulerHorizontal, faRotateLeft, faRotateRight } from '@fortawesome/free-solid-svg-icons'; @@ -328,6 +329,12 @@ const extensions = [ }), Underline, Blockquote, + Image.configure({ + allowBase64: true, + HTMLAttributes: { + class: 'mx-auto d-block', + }, + }), TextAlign.configure({ types: ['heading', 'paragraph'], }), diff --git a/frontend/src/components/viewable/blog-list.jsx b/frontend/src/components/viewable/blog-list.jsx index 2a25c34..e7fb025 100755 --- a/frontend/src/components/viewable/blog-list.jsx +++ b/frontend/src/components/viewable/blog-list.jsx @@ -11,15 +11,18 @@ import { Card, Row, Col, - CardImg, + Button, CardTitle, - CardText, CardBody } from 'reactstrap'; -import { Link, useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; +import { faLeftLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; function BlogList(props) { + let navigate = useNavigate(); + const { categoryID } = useParams(); const GlobalTheme = props.GlobalTheme; @@ -47,7 +50,8 @@ function BlogList(props) { if (GlobalTheme && ThemeConfig) { return ( - + +
diff --git a/frontend/src/components/viewable/blog.jsx b/frontend/src/components/viewable/blog.jsx index 5c0171b..a4c0e50 100755 --- a/frontend/src/components/viewable/blog.jsx +++ b/frontend/src/components/viewable/blog.jsx @@ -8,10 +8,13 @@ import CategoryBar from './shared/category-bar'; import { Container,Row, Col,Spinner, UncontrolledCollapse, Button, ButtonGroup, Card, CardBody } from 'reactstrap'; -import { Link, useParams } from 'react-router-dom'; +import { Link, useParams, useNavigate } from 'react-router-dom'; +import { faLeftLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; function Blog(props) { + let navigate = useNavigate(); const { blogID } = useParams(); const GlobalTheme = props.GlobalTheme; @@ -56,14 +59,18 @@ function Blog(props) { if (GlobalTheme && ThemeConfig) { return ( - + + - Banner + { + blogData.coverImage !== "" ? + Banner:"" + } @@ -142,13 +149,13 @@ function Blog(props) { - +
{blogContent}
- +
); diff --git a/frontend/src/components/viewable/category-list.jsx b/frontend/src/components/viewable/category-list.jsx index ff460c7..5b69e39 100755 --- a/frontend/src/components/viewable/category-list.jsx +++ b/frontend/src/components/viewable/category-list.jsx @@ -15,11 +15,15 @@ import { CardImg, CardTitle, CardText, - CardBody + CardBody, + Button } from 'reactstrap'; -import { Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { faLeftLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; function Blogs(props) { + let navigate = useNavigate(); const GlobalTheme = props.GlobalTheme; const ThemeConfig = props.ThemeConfig; @@ -34,10 +38,12 @@ function Blogs(props) { if (GlobalTheme && ThemeConfig) { return ( + {/* Top Section - Categories */}
+ diff --git a/frontend/src/index.css b/frontend/src/index.css index c121572..dc1dac1 100755 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -38,4 +38,11 @@ a { border: solid grey; border-radius: 10px; padding: 1em; +} + +.blogContent img { + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; + width: 50%; } \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 12538f0..c85c263 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -13,4 +13,4 @@ export default defineConfig({ } } } -}) +}) \ No newline at end of file