Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
8eb5befb1d | |||
1d326ae69c | |||
b7f5a0eb55 | |||
e13f37923d | |||
76311cd4b6 | |||
fd8e58367d | |||
d39511f736 | |||
3b88688c93 | |||
44973de80d | |||
87139bd358 |
@ -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", "."]
|
@ -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
|
||||
|
@ -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
48
src/hooks/useBookState.ts
Normal 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];
|
||||
};
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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)) {
|
||||
|
@ -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));
|
||||
|
Reference in New Issue
Block a user