10 Commits

Author SHA1 Message Date
8eb5befb1d Update README.md 2021-07-24 22:43:21 +05:00
1d326ae69c Added page navigation with number 2021-07-24 20:42:52 +03:00
b7f5a0eb55 Added pages persistance (ugly, but temporary) 2021-07-24 20:28:47 +03:00
e13f37923d Merge branch 'main' of github.com:publite/frontend 2021-07-24 19:22:52 +03:00
76311cd4b6 Improved text styles 2021-07-24 18:47:40 +03:00
fd8e58367d Update TODO 2021-07-23 14:23:46 +05:00
d39511f736 Update README.md 2021-07-18 01:52:28 +05:00
3b88688c93 even more deployment hell 2021-07-17 23:26:08 +03:00
44973de80d Dokku deployment hell 2021-07-17 23:15:11 +03:00
87139bd358 Changed dockerfile 2021-07-17 22:40:36 +03:00
11 changed files with 139 additions and 23 deletions

View File

@ -12,5 +12,5 @@ FROM node:alpine
RUN npm install serve -g --silent
WORKDIR /app
COPY --from=builder /app/build .
EXPOSE 5000
CMD ["serve", "-p", "5000", "-s", "."]
EXPOSE 80
CMD ["serve", "-p", "80", "-s", "."]

View File

@ -30,7 +30,7 @@ Simple docker deployment
docker build . -t publite_frontend
# run it with docker
docker run -p <port>:5000 publite_frontend
docker run -p <port>:80 publite_frontend
```
Dokku deployment with image from Docker Hub
@ -43,5 +43,10 @@ dokku git:from-image publitefrontend publite/frontend:latest
# TODO
- Create ServiceWorker (make it PWA)
- Migrate from LocalStorage to IndexedDB
- Add page position persistance
- Add menu with book view setting
- Add move to page by number
- Optimize page spliting algorythm (rewrite it)
- Fix css modules bundling

View File

@ -1,10 +1,10 @@
import React from "react";
import { useLibrary, UseLibraryReturnTuple } from "./hooks/useLibrary";
import { IBook } from "./types/book";
import { BookT } from "./types/book";
export const BookListContext = React.createContext<UseLibraryReturnTuple>([
{},
(book: IBook) => {},
(book: BookT) => {},
[],
]);

48
src/hooks/useBookState.ts Normal file
View File

@ -0,0 +1,48 @@
import React, { useEffect, useState } from "react";
import { BookState } from "~/types/book";
import { loadBookState, saveBookState } from "~/utils/localStorage";
export const useBookState = (
pagesReady: boolean,
hash: string | undefined,
goToPage: (pageNum: number) => void,
currentPage: React.RefObject<number>
): [boolean, BookState | undefined] => {
const [state, setState] = useState<BookState>();
const [ready, setReady] = useState(false);
useEffect(() => {
if (hash)
loadBookState(
hash,
(obj) => setState(obj),
() => setState({ currentPage: 0 })
);
}, [hash]);
useEffect(() => {
console.log(Boolean(!ready && state?.currentPage && goToPage));
if (!ready && state?.currentPage && pagesReady) {
console.log("Go to", state.currentPage);
goToPage(state.currentPage);
console.log("Ready");
setReady(true);
} else if (hash && !ready && pagesReady && typeof state === "object") {
saveBookState(hash, { currentPage: 0 });
setReady(true);
}
}, [ready, state, goToPage]);
useEffect(
() => () => {
if (hash) {
console.log(currentPage);
if (ready && state) saveBookState(hash, state);
else saveBookState(hash, { currentPage: currentPage.current || 0 });
}
},
[]
);
return [ready, state];
};

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { IBook } from "~/types/book";
import { BookT } from "~/types/book";
import {
getBookHT,
getHashList,
@ -7,16 +7,16 @@ import {
updateHashList,
} from "~/utils/localStorage";
export type AddBookFT = (book: IBook) => void;
export type AddBookFT = (book: BookT) => void;
export type UseLibraryReturnTuple = [
Record<string, IBook> | null,
Record<string, BookT> | null,
AddBookFT,
string[]
];
export const useLibrary = (): UseLibraryReturnTuple => {
const [library, setLibrary] = useState<Record<string, IBook> | null>(null);
const [library, setLibrary] = useState<Record<string, BookT> | null>(null);
const [hashList, setHashList] = useState<string[]>([]);
const addBook: AddBookFT = (book) => {

View File

@ -31,13 +31,19 @@
.page {
height: 100%;
font-size: 1.25rem;
font-family: serif;
}
img {
.page img {
max-height: 90%;
max-width: 100%;
}
.page p {
margin-bottom: 1rem;
}
.pageIndicator {
position: fixed;
z-index: 1;
@ -50,6 +56,10 @@ img {
justify-content: center;
}
.pageNumber {
cursor: pointer;
}
.pageSwitchArrow {
background: none;
border: none;

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useRef } from "react";
import React, { MouseEventHandler, useContext, useEffect, useRef } from "react";
import { Redirect, useRoute } from "wouter";
import styles from "./BookView.module.css";
@ -6,6 +6,7 @@ import styles from "./BookView.module.css";
import { BookListContext } from "~/context";
import { usePagination } from "~/hooks/usePagination";
import { IPageProps } from "~/types/page";
import { useBookState } from "~/hooks/useBookState";
export const BookView = ({ setLoading, loading }: IPageProps) => {
useEffect(() => setLoading(true), []);
@ -17,7 +18,7 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
const pageContainerRef = useRef<HTMLDivElement>(null);
const pageRef = useRef<HTMLDivElement>(null);
const [ready, goToPage, currentPage, pagesNumber] = usePagination(
const [pagesReady, goToPage, currentPage, pagesNumber] = usePagination(
contentRef,
pageContainerRef,
pageRef,
@ -31,8 +32,15 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
currentPageRef.current = currentPage;
}, [currentPage]);
const [bookStateReady, bs] = useBookState(
pagesReady,
params?.hash,
goToPage,
currentPageRef
);
useEffect(() => {
if (ready) {
if (bookStateReady) {
setLoading(false);
const handleKey = ({ key }: KeyboardEvent) => {
@ -48,10 +56,17 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
window.addEventListener("keydown", handleKey);
}
}, [ready]);
}, [bookStateReady]);
const goPrev = () => goToPage(currentPage - 1);
const goNext = () => goToPage(currentPage + 1);
const insertNumber: MouseEventHandler<HTMLSpanElement> = (e) => {
const str = prompt("Page number");
if (str) {
const n = parseInt(str);
if (!isNaN(n) && n > 0) goToPage(n - 1);
}
};
if (books) {
if (params?.hash && params.hash in books)
@ -73,7 +88,7 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
<button className={styles.pageSwitchArrow} onClick={goPrev}>
{currentPage !== 0 && "←"}
</button>
<span>
<span className={styles.pageNumber} onClick={insertNumber}>
{currentPage + 1} / {pagesNumber}
</span>
<button className={styles.pageSwitchArrow} onClick={goNext}>

View File

@ -2,10 +2,10 @@ import React from "react";
import styles from "./BookItem.module.css";
import { IBook } from "~/types/book";
import { BookT } from "~/types/book";
import { Link } from "wouter";
interface IBookItemProps extends IBook {}
interface IBookItemProps extends BookT {}
export const BookItem = ({ author, title, cover, hash }: IBookItemProps) => {
return (

View File

@ -1,9 +1,13 @@
export const requiredBookProps = ["title", "author", "content"] as const;
export const optionalBookProps = ["cover", "hash"] as const;
export type IBook = {
export type BookT = {
[key in typeof requiredBookProps[number]]: string;
} &
{
[key in typeof optionalBookProps[number]]: string | undefined;
};
export type BookState = {
currentPage: number;
};

View File

@ -1,4 +1,4 @@
import { IBook, requiredBookProps } from "~/types/book";
import { BookT, requiredBookProps } from "~/types/book";
import { API_URL } from "~/constants";
@ -39,7 +39,7 @@ export const submitFile = async (
}
};
export const validateResponse = (content: unknown): content is IBook => {
export const validateResponse = (content: unknown): content is BookT => {
if (content && typeof content === "object")
for (const key of requiredBookProps)
if (!(key in content)) {

View File

@ -1,4 +1,4 @@
import { IBook } from "~/types/book";
import { BookState, BookT } from "~/types/book";
import { isArrOfStr } from "~/types/utils";
import { validateResponse } from "~/utils/api";
@ -17,7 +17,7 @@ export const getHashList = () => {
};
export const getBookHT = (hashList: string[]) => {
const bookHT: Record<string, IBook> = {};
const bookHT: Record<string, BookT> = {};
hashList.forEach((hash) => {
try {
@ -31,7 +31,7 @@ export const getBookHT = (hashList: string[]) => {
return bookHT;
};
export const saveBook = (key: string, book: IBook) =>
export const saveBook = (key: string, book: BookT) =>
localStorage.setItem(key, JSON.stringify(book));
export const updateHashList = (hashList: string[]) =>
@ -78,3 +78,37 @@ export const savePages = (
width: number,
pages: number[]
) => localStorage.setItem(hashStr(hash, height, width), JSON.stringify(pages));
export const validateBookState = (obj: unknown): obj is BookState =>
Boolean(
obj &&
typeof obj === "object" &&
!Array.isArray(obj) &&
"currentPage" in obj
);
export const loadBookState = (
hash: string,
cb: (bookState: BookState) => void,
ecb: () => void
) => {
const str = localStorage.getItem(hash + "-state");
if (str) {
try {
const obj: unknown = JSON.parse(str);
if (validateBookState(obj)) {
cb(obj);
return true;
}
} catch (e) {
console.error(e);
}
}
ecb();
};
export const saveBookState = (hash: string, state: BookState) =>
localStorage.setItem(hash + "-state", JSON.stringify(state));