Added global book list context and useLibrary hook for it

This commit is contained in:
Dmitriy Shishkov 2021-07-16 03:59:33 +05:00
parent 21448cf91d
commit 917b00ea18
No known key found for this signature in database
GPG Key ID: 14358F96FCDD8060
8 changed files with 102 additions and 55 deletions

View File

@ -1,5 +1,6 @@
import React from "react";
import { Route, Switch } from "wouter";
import { BookListContextProvider } from "~/context";
import { Bookshelf } from "~/pages/Bookshelf";
import { BookView } from "~/pages/BookView";
@ -9,11 +10,13 @@ import styles from "./App.module.css";
export const App = () => (
<div className={styles.container}>
<Switch>
<Route path="/upload" component={UploadForm} />
<Route path="/" component={Bookshelf} />
<Route path="/:hash" component={BookView} />
</Switch>
<BookListContextProvider>
<Switch>
<Route path="/upload" component={UploadForm} />
<Route path="/" component={Bookshelf} />
<Route path="/:hash" component={BookView} />
</Switch>
</BookListContextProvider>
</div>
);

View File

@ -1,4 +1,4 @@
import { IBook, optionalBookProps, requiredBookProps } from "~/types/book";
import { IBook, requiredBookProps } from "~/types/book";
import { API_URL } from "./constants";
@ -39,12 +39,11 @@ export const submitFile = async (
}
};
export const validateResponse = (
content: Record<string, unknown>
): content is IBook => {
for (const key of requiredBookProps)
if (!(key in content))
throw new Error(`${key} is not specified in server response`);
export const validateResponse = (content: unknown): content is IBook => {
if (content && typeof content === "object")
for (const key of requiredBookProps)
if (!(key in content))
throw new Error(`${key} is not specified in server response`);
return true;
};

18
src/context.tsx Normal file
View File

@ -0,0 +1,18 @@
import React from "react";
import { useLibrary, UseLibraryReturnTuple } from "./hooks/useLibrary";
import { IBook } from "./types/book";
export const BookListContext = React.createContext<UseLibraryReturnTuple>([
[],
(book: IBook) => {},
]);
export const BookListContextProvider: React.FC = ({ children }) => {
const library = useLibrary();
return (
<BookListContext.Provider value={library}>
{children}
</BookListContext.Provider>
);
};

38
src/hooks/useLibrary.ts Normal file
View File

@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { IBook } from "~/types/book";
import {
getBookList,
getTitleList,
setBook,
updateTitleList,
} from "~/utils/localStorage";
export type AddBookFT = (book: IBook) => void;
export type UseLibraryReturnTuple = [IBook[], AddBookFT];
export const useLibrary = (): UseLibraryReturnTuple => {
const [bookList, setBookList] = useState<IBook[]>([]);
const [titleList, setTitleList] = useState<string[]>([]);
const addBook: AddBookFT = (book) => {
const key = book.hash || Date.now().toString();
if (key && !titleList.includes(key)) {
setTitleList([key, ...titleList]);
setBookList([book, ...bookList]);
setBook(key, book);
}
};
useEffect(() => {
const receivedTitleList = getTitleList();
setTitleList(receivedTitleList);
setBookList(getBookList(receivedTitleList));
}, []);
useEffect(() => updateTitleList(titleList), [titleList]);
return [bookList, addBook];
};

View File

@ -3,7 +3,6 @@ import { useLocation } from "wouter";
import plusIcon from "~/assets/plus.svg";
import styles from "./AddBook.module.css";
import { BASE_URL } from "~/constants";
export const AddBook = () => {
const [_, setLocation] = useLocation();

View File

@ -1,18 +1,13 @@
import React, { useEffect, useState } from "react";
import { IBook } from "~/types/book";
import React, { useContext, useEffect, useState } from "react";
import styles from "./Bookshelf.module.css";
import { BookItem } from "./BookItem";
import { AddBook } from "./AddBook";
import { readBooks } from "~/utils/localStorage";
import { BookListContext } from "~/context";
export const Bookshelf = () => {
const [books, setBooks] = useState<IBook[]>([]);
useEffect(() => {
setBooks(readBooks());
}, []);
const [books] = useContext(BookListContext);
return (
<div className={styles.container}>

View File

@ -1,16 +1,18 @@
import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { useLocation } from "wouter";
import plusIcon from "~/assets/plus.svg";
import styles from "./UploadForm.module.css";
import { submitFile, validateResponse, validState } from "~/api";
import { saveBook } from "~/utils/localStorage";
import { BookListContext } from "~/context";
export const UploadForm = () => {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [_, setLocation] = useLocation();
const [__, saveBook] = useContext(BookListContext);
const processFile = async (file: File | undefined) => {
try {
if (validState(file)) {
@ -23,7 +25,7 @@ export const UploadForm = () => {
console.log(validateResponse(res));
if (validateResponse(res)) {
saveBook(res, res.hash || Date.now().toString());
saveBook(res);
setLocation("/");
}
}

View File

@ -1,40 +1,33 @@
import { IBook } from "~/types/book";
import { isArrOfStr } from "~/types/utils";
import { validateResponse } from "~/api";
import { BookItem } from "~/pages/Bookshelf/BookItem";
const readBookList = <T>(
cb: (bookList: string[]) => T,
defaultValue: T extends void ? undefined : T
) => {
const bookListStr = localStorage.getItem("list") || "[]";
const bookList: unknown = JSON.parse(bookListStr);
export const getTitleList = () => {
const titleListStr = localStorage.getItem("list") || "[]";
const titleList: unknown = JSON.parse(titleListStr);
if (isArrOfStr(bookList)) return cb(bookList);
return defaultValue;
if (isArrOfStr(titleList)) return titleList;
else {
localStorage.setItem("list", "[]");
return [];
}
};
export const saveBook = (bookObj: IBook, key: string) =>
readBookList((bookList) => {
if (!bookList.includes(key)) {
const newBookList = [key, ...bookList];
export const getBookList = (titleList: string[]) =>
titleList
.map<unknown>((hash) => JSON.parse(localStorage.getItem(hash) || "{}"))
.filter((obj): obj is IBook => {
try {
return validateResponse(obj);
} catch (err) {
if (import.meta.env.NODE_ENV === "development")
console.log(err.message);
return false;
}
});
localStorage.setItem("list", JSON.stringify(newBookList));
localStorage.setItem(key, JSON.stringify(bookObj));
}
}, undefined);
export const setBook = (key: string, book: IBook) =>
localStorage.setItem(key, JSON.stringify(book));
export const readBooks = (): IBook[] =>
readBookList(
(bookList) =>
bookList
.map((hash) => JSON.parse(localStorage.getItem(hash) || "{}"))
.filter((e) => {
try {
return validateResponse(e);
} catch (err) {
return false;
}
}),
[]
);
export const updateTitleList = (titleList: string[]) =>
localStorage.setItem("list", JSON.stringify(titleList));