diff --git a/src/components/books/bookInfo.jsx b/src/components/books/bookInfo.jsx index 0bcf090..31e9e90 100644 --- a/src/components/books/bookInfo.jsx +++ b/src/components/books/bookInfo.jsx @@ -1,5 +1,5 @@ // 3rd party imports -import { Space, Typography } from "antd"; +import { Divider, Progress, Space, Typography } from "antd"; // Local imports import helpers from "../../helpers"; @@ -10,6 +10,7 @@ import { IconText } from "../common/iconText"; import BookStatusIcon from './BookStatusIcon'; import { AiOutlineCopy } from "react-icons/ai"; import { FiLayers } from "react-icons/fi"; +import EditingStatusIcon from "../editingStatusIcon"; // ----------------------------------------- const { Paragraph } = Typography; // --------------------------------------------- @@ -44,6 +45,17 @@ const BookInfo = ({ libraryId, book, t }) => { + + + {book.pageStatus.map(s => ( + + + + + ))} ); diff --git a/src/components/books/files/fileDeleteButton.jsx b/src/components/books/files/fileDeleteButton.jsx new file mode 100644 index 0000000..655df21 --- /dev/null +++ b/src/components/books/files/fileDeleteButton.jsx @@ -0,0 +1,48 @@ +// Third party libraries +import { App, Button, Modal, Tooltip } from "antd"; +import { FaTrash } from "react-icons/fa"; +import { ExclamationCircleFilled } from "@ant-design/icons"; +// Local imports +import { useDeleteBookContentMutation } from "../../../features/api/booksSlice"; + +// ------------------------------------------------------ + +const { confirm } = Modal; + +// ------------------------------------------------------ +export default function FileDeleteButton({ content, t, type }) { + const { message } = App.useApp(); + const [deleteBookContent, { isLoading: isDeleting }] = useDeleteBookContentMutation(); + + const showConfirm = () => { + confirm({ + title: t("book.actions.deleteFile.title"), + icon: , + content: t("book.actions.deleteFile.message", { + title: content.fileName, + }), + okButtonProps: { loading: isDeleting }, + okType: "danger", + cancelButtonProps: { disabled: isDeleting }, + closable: { isDeleting }, + onOk() { + if (content && content.links && content.links.delete) { + return deleteBookContent({ content }).unwrap() + .then(() => message.success(t("chapter.actions.delete.success"))) + .catch((_) => message.error(t("chapter.actions.delete.error")) + ); + } + } + }); + }; + + return ( + + - } + busy={isAdding} emptyImage={} - emptyDescription={t("chapters.empty.title")} + emptyDescription={t("book.empty.title")} emptyContent={ - + + + } - empty={chapters && chapters.data && chapters.data.length < 1} + empty={!book || (book.contents && book.contents.length < 1)} bordered={false} - > - - - {(provided) => ( -
- ( - - )} - > - {provided.placeholder} - -
+ > + + + } + renderItem={(c) => ( + )} -
-
+ > + ); diff --git a/src/components/common/iconText.jsx b/src/components/common/iconText.jsx index 5e548ca..8ebf43a 100644 --- a/src/components/common/iconText.jsx +++ b/src/components/common/iconText.jsx @@ -1,14 +1,15 @@ import React from 'react'; // 3rd party -import { Space } from 'antd'; +import { Space, Typography } from 'antd'; // ------------------------------------------------ -export function IconText({ icon, text, link = true ,onClick = () => {} }) { +export function IconText({ icon, text, secondaryText = null, link = true ,onClick = () => {} }) { return ( {React.createElement(icon)} {text} + {secondaryText && {secondaryText}} ); } diff --git a/src/components/editingStatusIcon.jsx b/src/components/editingStatusIcon.jsx index d9229fb..cf66754 100644 --- a/src/components/editingStatusIcon.jsx +++ b/src/components/editingStatusIcon.jsx @@ -4,24 +4,25 @@ import { FaFileAlt, FaCheck, FaGlasses, + FaRegFile, } from "react-icons/fa"; // ------------------------------------------------------ -const EditingStatusIcon = ({ status, style }) => { +const EditingStatusIcon = ({ status, style, render = true }) => { switch (status) { case "Available": - return ; + return render ? : FaFile; case "Typing": - return ; + return render ? : FaFileSignature; case "Typed": - return ; + return render ? : FaFileAlt; case "InReview": - return ; + return render ? : FaGlasses; case "Completed": - return ; + return render ? : FaCheck; default: - return ; + return render ? : FaRegFile; } }; diff --git a/src/features/api/booksSlice.js b/src/features/api/booksSlice.js index 9f047ce..e4cd803 100644 --- a/src/features/api/booksSlice.js +++ b/src/features/api/booksSlice.js @@ -97,7 +97,7 @@ export const booksApi = createApi({ getBook: builder.query({ query: ({ libraryId, bookId }) => ({ url: `/libraries/${libraryId}/books/${bookId}`, method: 'get' }), transformResponse: (response) => parseResponse(response), - providesTags: [ 'Books' ] + providesTags: [ 'Book' ] }), getBookChapters: builder.query({ query: ({ libraryId, bookId }) => ({ url: `/libraries/${libraryId}/books/${bookId}/chapters`, method: 'get' }), @@ -128,14 +128,14 @@ export const booksApi = createApi({ method: 'PUT', payload: removeLinks(payload) }), - invalidatesTags: [ 'Books' ] + invalidatesTags: [ 'Books', 'Book' ] }), deleteBook: builder.mutation({ query: ({ libraryId, bookId }) => ({ url: `/libraries/${libraryId}/books/${bookId}`, method: 'DELETE' }), - invalidatesTags: [ 'Books' ] + invalidatesTags: [ 'Books', 'Book' ] }), updateBookImage: builder.mutation({ query: ({ libraryId, bookId, payload }) => { @@ -151,7 +151,7 @@ export const booksApi = createApi({ } }); }, - invalidatesTags: [ 'Books' ] + invalidatesTags: [ 'Book' ] }), addChapter: builder.mutation({ query: ({ libraryId, bookId, payload }) => ({ @@ -289,7 +289,45 @@ export const booksApi = createApi({ }, invalidatesTags: [ 'BookPages' ] }), - + addBookContent: builder.mutation({ + query: ({ book, payload }) => { + const formData = new FormData(); + formData.append('file', payload, payload.fileName); + return ({ + url: book.links.add_file, + method: 'POST', + payload: formData, + formData: true, + headers: { + 'content-type': 'multipart/form-data' + } + }); + }, + invalidatesTags: [ 'Book' ] + }), + updateBookContent: builder.mutation({ + query: ({ content, payload }) => { + const formData = new FormData(); + formData.append('file', payload, payload.fileName); + return ({ + url: content.links.update, + method: 'PUT', + payload: formData, + formData: true, + headers: { + 'content-type': 'multipart/form-data' + } + }); + }, + invalidatesTags: [ 'Book' ] + }), + deleteBookContent: builder.mutation({ + query: ({ content }) => ({ + url: content.links.delete, + method: 'DELETE', + }), + invalidatesTags: [ 'Book' ] + }), }), }) @@ -320,4 +358,7 @@ export const { useUpdateBookPageImageMutation, useUpdateBookPageSequenceMutation, useCreateBookPageWithImageMutation, + useAddBookContentMutation, + useUpdateBookContentMutation, + useDeleteBookContentMutation, } = booksApi diff --git a/src/i18n/en.js b/src/i18n/en.js index 1e7d55f..fe04c7f 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -355,8 +355,29 @@ const en = { "message": "Are you sure you want to delete book '{{title}}'? It will remove all of its contents including chapters, pages and files.", "success" :"Book deleted successfully.", "error" :"Error deleting book." + }, + "addFile" : { + "title" : "Upload file", + "success" :"File uploadedsuccessfully.", + "error" :"Error uploading file." + }, + "deleteFile": { + "title": "Delete file?", + "message": "Are you sure you want to delete book '{{title}}'?.", + }, + "downloadFile": { + "title" : "Download file" } - } + }, + "errors": { + "loading": { + "title": "Error", + "subTitle": "Unable to load book.", + } + }, + "empty" : { + "title" : "No latest book found", + }, }, "chapters" : { "title": "Chapters", @@ -1015,7 +1036,8 @@ const en = { "Typing": "Being Typed", "Typed": "Typed", "InReview" : "In Review", - "Completed" : "Completed" + "Completed" : "Completed", + "All": "All" }, "sort": { "ascending": "Ascending", diff --git a/src/i18n/ur.js b/src/i18n/ur.js index 3c7846e..f26756a 100644 --- a/src/i18n/ur.js +++ b/src/i18n/ur.js @@ -334,7 +334,7 @@ const ur = { "title": "ابواب" }, "pages": { - "title" : "صفحات", + "title" : "صفحات" }, "files": { "title" : "دستاویزات", @@ -356,9 +356,30 @@ const ur = { "message": "کیا آپ کتاب '{{title}}' کو حذف کرنا چاہیں گے؟ ایسا کرنے سے اس کتاب سے متعلق تمام مواد بشمول ابواب، صفحات اور تمام دستاویزات حذف ہو جائیں گی۔", "success" :"کتاب حذف کر دی گئی ہے۔", "error" :"کتاب حذف کرنے میں ناکامی ہوئی۔" + }, + "addFile" : { + "title" : "فائل اپلوڈ کریں", + "success" :"فائل کامیابی سے اپلوڈ کر دی گئی ہے۔", + "error" :"فائل اپلوڈ کرنے میں نامامی ہوئی۔" + }, + "deleteFile": { + "title": "فایئل حذف کریں؟", + "message": "کیا آپ '{{title}}' فائل کو حذف کرنا چاہیں گے؟", + }, + "downloadFile": { + "title" : "فائل ڈائنلوڈ" } - } - }, + }, + "errors": { + "loading": { + "title": "Error", + "subTitle": "Unable to load book.", + } + }, + "empty" : { + "title" : "No latest book found", + }, + }, "chapters" : { "title": "ابواب", "errors": { @@ -1007,7 +1028,8 @@ const ur = { "Typing": "ٹائپنگ", "Typed": "ٹائپنگ مکمل", "InReview" : "پروف خوانی", - "Completed" : "مکمل" + "Completed" : "مکمل", + "All" : "تمام" }, "layouts" : { "normal" : { diff --git a/src/pages/articles/article.jsx b/src/pages/articles/article.jsx index 514a69f..cbf31da 100644 --- a/src/pages/articles/article.jsx +++ b/src/pages/articles/article.jsx @@ -17,6 +17,7 @@ import PageHeader from "../../components/layout/pageHeader"; import Loading from "../../components/common/loader"; import AuthorAvatar from "../../components/author/authorAvatar"; import ArticleDeleteButton from "../../components/articles/articleDeleteButton"; +import ReactMarkdown from "react-markdown"; const ArticlePage = () => { const navigate = useNavigate(); @@ -95,7 +96,7 @@ const ArticlePage = () => { }>
- {articleContents?.text} +
);