Skip to content

Commit

Permalink
Added book uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
umerfaruk committed Nov 23, 2023
1 parent 29d90cc commit de6488d
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 139 deletions.
14 changes: 13 additions & 1 deletion src/components/books/bookInfo.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
// ---------------------------------------------
Expand Down Expand Up @@ -44,6 +45,17 @@ const BookInfo = ({ libraryId, book, t }) => {
<IconText icon={FiLayers} text={t("book.chapterCount", { count: book.chapterCount })} />
<IconText icon={AiOutlineCopy} text={t("book.pageCount", { count: book.pageCount })} />
<IconText icon={BookStatusIcon({status : book.status, render:false })} text={t(`bookStatus.${book.status}`)} />
<Progress percent={book.progress} size="small" status="active" />
<Divider />
{book.pageStatus.map(s => (
<Space direction="vertical" key={`status-${s.status}`} style={{ width: "100%" }}>
<IconText
icon={EditingStatusIcon({status : s.status, render:false })}
text={t(`editingStatus.${s.status}`)}
secondaryText={ s.count }/>
<Progress percent={s.percentage} size="small" />
</Space>
))}
</Space>
</>
);
Expand Down
48 changes: 48 additions & 0 deletions src/components/books/files/fileDeleteButton.jsx
Original file line number Diff line number Diff line change
@@ -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: <ExclamationCircleFilled />,
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 (
<Tooltip title={t('actions.delete')}>
<Button
type={type}
onClick={showConfirm}
icon={<FaTrash />}
/>
</Tooltip>
);
}
82 changes: 82 additions & 0 deletions src/components/books/files/fileListItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Link } from "react-router-dom";

// 3rd Party Libraries
import { Avatar, Button, List, Tooltip, Typography, Upload } from "antd";
import { FaFileDownload, FaFileUpload } from "react-icons/fa";

// Local Import
import { useUpdateBookContentMutation } from "../../../features/api/booksSlice";
import FileDeleteButton from "./fileDeleteButton";
import FileTypeIcon from "./fileTypeIcon";

// ------------------------------------------------------

function FileListItem({
libraryId,
bookId,
content,
t,
message
}) {
const [updateBookContent, { isLoading: isUpdating }] = useUpdateBookContentMutation();

const title = (<Link
to={`/libraries/${libraryId}/books/${bookId}/contents/${content.id}`}
>
<Typography.Text>
{content.fileName}
</Typography.Text>
</Link>);

const uploadFile = (file) => {
const isAllowed = ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"].includes(file.type);
if (!isAllowed) {
message.error(t("errors.imageRequired"));
return;
}

updateBookContent({ content: content, payload: file }).unwrap()
.then(() => message.success(t("book.actions.addFile.success")))
.catch((_) => message.error(t("book.actions.addFile.error")));
}

return (<List.Item
actions={[
content && content.links.update && (
<Tooltip title={t('book.actions.addFile.title')}>
<Upload beforeUpload={uploadFile} maxCount={1} showUploadList={false} >
<Button icon={<FaFileUpload />} disabled={isUpdating} />
</Upload>
</Tooltip>
),
content && content.links.download && (
<Tooltip title={t('book.actions.downloadFile.title')}>
<a href={content.links.download} target="_blank" rel="noreferrer">
<Button icon={<FaFileDownload />} />
</a>
</Tooltip>
),
content && content.links.delete && (
<FileDeleteButton
content={content}
t={t}
type="text"
/>
),
]}
>
<List.Item.Meta
title={title}
avatar={
<Avatar>
<FileTypeIcon
type={content.mimeType}
/>
</Avatar>
}
/>
</List.Item>
);
}

export default FileListItem;
15 changes: 15 additions & 0 deletions src/components/books/files/fileTypeIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FileOutlined, FilePdfOutlined, FileWordOutlined } from "@ant-design/icons";

const FileTypeIcon = ({ type }) => {
switch (type) {
case 'application/pdf':
return (<FilePdfOutlined />)
case "application/msword":
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return (<FileWordOutlined />);
default:
return (<FileOutlined />)
}
};

export default FileTypeIcon;
158 changes: 41 additions & 117 deletions src/components/books/files/filesList.jsx
Original file line number Diff line number Diff line change
@@ -1,142 +1,66 @@
// 3rd party libraries
import { App, Button, Col, List, Row, Skeleton, Typography } from "antd";
import { FaBook } from "react-icons/fa";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { App, Button, List, Upload } from "antd";
import { FaBook, FaFileUpload } from "react-icons/fa";

// Internal Imports
import {
useGetBookChaptersQuery,
useUpdateChapterSequenceMutation,
} from "../../../features/api/booksSlice";
import ChapterListItem from "../chapters/chapterListItem";
import ChapterEditor from "../chapters/chapterEditor";
import { useAddBookContentMutation } from "../../../features/api/booksSlice";
import FileListItem from "../files/fileListItem";
import DataContainer from "../../layout/dataContainer";

// ------------------------------------------------------
// ----------------------------------------------

const FilesList = ({
libraryId,
bookId,
book,
t,
selectedChapterNumber = null,
size = "default",
hideTitle = false,
size = "default"
}) => {
const { message } = App.useApp();

const {
refetch,
data: chapters,
error,
isFetching,
} = useGetBookChaptersQuery(
{ libraryId, bookId },
{ skip: !libraryId || !bookId }
);
const [updateChapterSequence, { isLoading: isUpdating }] =
useUpdateChapterSequenceMutation();

const title = hideTitle ? null : <div>{t("book.chapters.title")}</div>;

if (isFetching) return <Skeleton />;

const onDragDrop = (result) => {
const fromIndex = result.source.index;
const toIndex = result.destination.index;
let payload = [...chapters.data];
if (fromIndex !== toIndex) {
const element = payload[fromIndex];
payload.splice(fromIndex, 1);
payload.splice(toIndex, 0, element);
const [addBookContent, { isLoading: isAdding }] = useAddBookContentMutation();

payload = payload.map((item, index) => ({
id: item.id,
chapterNumber: index + 1,
}));

return updateChapterSequence({ libraryId, bookId, payload })
.unwrap()
.then(() =>
message.success(t("chapter.actions.reorder.success"))
)
.catch((_) =>
message.error(t("chapter.actions.reorder.error"))
);
const uploadFile = (file) => {
const isAllowed = ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"].includes(file.type);
if (!isAllowed) {
message.error(t("errors.imageRequired"));
return;
}
};

const header = (
<Row>
<Col flex="auto">
<Typography level={3}>{title}</Typography>
</Col>
<Col>
<ChapterEditor
libraryId={libraryId}
bookId={bookId}
t={t}
buttonType="dashed"
/>
</Col>
</Row>
);
addBookContent({ book: book, payload: file }).unwrap()
.then(() => message.success(t("book.actions.addFile.success")))
.catch((_) => message.error(t("book.actions.addFile.error")));
}

return (
<>
<DataContainer
busy={isFetching | isUpdating}
error={error}
errorTitle={t("chapters.errors.loading.title")}
errorSubTitle={t("chapters.errors.loading.subTitle")}
errorAction={
<Button type="default" onClick={refetch}>
{t("actions.retry")}
</Button>
}
busy={isAdding}
emptyImage={<FaBook size="5em" />}
emptyDescription={t("chapters.empty.title")}
emptyDescription={t("book.empty.title")}
emptyContent={
<ChapterEditor
libraryId={libraryId}
bookId={bookId}
t={t}
buttonType="dashed"
/>
<Upload beforeUpload={uploadFile} maxCount={1} showUploadList={false} >
<Button icon={FaFileUpload}>a{t('book.actions.addFile')}</Button>
</Upload>
}
empty={chapters && chapters.data && chapters.data.length < 1}
empty={!book || (book.contents && book.contents.length < 1)}
bordered={false}
>
<DragDropContext onDragEnd={onDragDrop}>
<Droppable droppableId={`Droppable_${bookId}`}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
<List
size={size}
itemLayout="horizontal"
dataSource={chapters ? chapters.data : []}
header={header}
renderItem={(chapter) => (
<ChapterListItem
key={chapter.id}
t={t}
selected={
selectedChapterNumber ===
chapter.chapterNumber
}
libraryId={libraryId}
bookId={bookId}
chapter={chapter}
/>
)}
>
{provided.placeholder}
</List>
</div>
>
<List
size={size}
itemLayout="horizontal"
dataSource={book && book.contents ? book.contents : []}
header={<Upload beforeUpload={uploadFile} maxCount={1} showUploadList={false} >
<Button icon={FaFileUpload}>Upload</Button>
</Upload>}
renderItem={(c) => (
<FileListItem key={`file-${c.id}`}
libraryId={libraryId}
bookId={book.id}
content={c}
t={t}
message={message} />
)}
</Droppable>
</DragDropContext>
>
</List>
</DataContainer>
</>
);
Expand Down
5 changes: 3 additions & 2 deletions src/components/common/iconText.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Space onClick={onClick} style={{ cursor : link ? 'pointer' : 'default' }}>
{React.createElement(icon)}
{text}
{secondaryText && <Typography.Text type="secondary">{secondaryText}</Typography.Text>}
</Space>
);
}
Loading

0 comments on commit de6488d

Please sign in to comment.