Added global book list context and useLibrary hook for it
This commit is contained in:
parent
21448cf91d
commit
917b00ea18
@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Route, Switch } from "wouter";
|
import { Route, Switch } from "wouter";
|
||||||
|
import { BookListContextProvider } from "~/context";
|
||||||
|
|
||||||
import { Bookshelf } from "~/pages/Bookshelf";
|
import { Bookshelf } from "~/pages/Bookshelf";
|
||||||
import { BookView } from "~/pages/BookView";
|
import { BookView } from "~/pages/BookView";
|
||||||
@ -9,11 +10,13 @@ import styles from "./App.module.css";
|
|||||||
|
|
||||||
export const App = () => (
|
export const App = () => (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Switch>
|
<BookListContextProvider>
|
||||||
<Route path="/upload" component={UploadForm} />
|
<Switch>
|
||||||
<Route path="/" component={Bookshelf} />
|
<Route path="/upload" component={UploadForm} />
|
||||||
<Route path="/:hash" component={BookView} />
|
<Route path="/" component={Bookshelf} />
|
||||||
</Switch>
|
<Route path="/:hash" component={BookView} />
|
||||||
|
</Switch>
|
||||||
|
</BookListContextProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
13
src/api.ts
13
src/api.ts
@ -1,4 +1,4 @@
|
|||||||
import { IBook, optionalBookProps, requiredBookProps } from "~/types/book";
|
import { IBook, requiredBookProps } from "~/types/book";
|
||||||
|
|
||||||
import { API_URL } from "./constants";
|
import { API_URL } from "./constants";
|
||||||
|
|
||||||
@ -39,12 +39,11 @@ export const submitFile = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateResponse = (
|
export const validateResponse = (content: unknown): content is IBook => {
|
||||||
content: Record<string, unknown>
|
if (content && typeof content === "object")
|
||||||
): content is IBook => {
|
for (const key of requiredBookProps)
|
||||||
for (const key of requiredBookProps)
|
if (!(key in content))
|
||||||
if (!(key in content))
|
throw new Error(`${key} is not specified in server response`);
|
||||||
throw new Error(`${key} is not specified in server response`);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
18
src/context.tsx
Normal file
18
src/context.tsx
Normal 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
38
src/hooks/useLibrary.ts
Normal 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];
|
||||||
|
};
|
@ -3,7 +3,6 @@ import { useLocation } from "wouter";
|
|||||||
|
|
||||||
import plusIcon from "~/assets/plus.svg";
|
import plusIcon from "~/assets/plus.svg";
|
||||||
import styles from "./AddBook.module.css";
|
import styles from "./AddBook.module.css";
|
||||||
import { BASE_URL } from "~/constants";
|
|
||||||
|
|
||||||
export const AddBook = () => {
|
export const AddBook = () => {
|
||||||
const [_, setLocation] = useLocation();
|
const [_, setLocation] = useLocation();
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { IBook } from "~/types/book";
|
|
||||||
|
|
||||||
import styles from "./Bookshelf.module.css";
|
import styles from "./Bookshelf.module.css";
|
||||||
|
|
||||||
import { BookItem } from "./BookItem";
|
import { BookItem } from "./BookItem";
|
||||||
import { AddBook } from "./AddBook";
|
import { AddBook } from "./AddBook";
|
||||||
import { readBooks } from "~/utils/localStorage";
|
import { BookListContext } from "~/context";
|
||||||
|
|
||||||
export const Bookshelf = () => {
|
export const Bookshelf = () => {
|
||||||
const [books, setBooks] = useState<IBook[]>([]);
|
const [books] = useContext(BookListContext);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBooks(readBooks());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
import plusIcon from "~/assets/plus.svg";
|
import plusIcon from "~/assets/plus.svg";
|
||||||
import styles from "./UploadForm.module.css";
|
import styles from "./UploadForm.module.css";
|
||||||
import { submitFile, validateResponse, validState } from "~/api";
|
import { submitFile, validateResponse, validState } from "~/api";
|
||||||
import { saveBook } from "~/utils/localStorage";
|
import { BookListContext } from "~/context";
|
||||||
|
|
||||||
export const UploadForm = () => {
|
export const UploadForm = () => {
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [_, setLocation] = useLocation();
|
const [_, setLocation] = useLocation();
|
||||||
|
|
||||||
|
const [__, saveBook] = useContext(BookListContext);
|
||||||
|
|
||||||
const processFile = async (file: File | undefined) => {
|
const processFile = async (file: File | undefined) => {
|
||||||
try {
|
try {
|
||||||
if (validState(file)) {
|
if (validState(file)) {
|
||||||
@ -23,7 +25,7 @@ export const UploadForm = () => {
|
|||||||
console.log(validateResponse(res));
|
console.log(validateResponse(res));
|
||||||
|
|
||||||
if (validateResponse(res)) {
|
if (validateResponse(res)) {
|
||||||
saveBook(res, res.hash || Date.now().toString());
|
saveBook(res);
|
||||||
setLocation("/");
|
setLocation("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,33 @@
|
|||||||
import { IBook } from "~/types/book";
|
import { IBook } from "~/types/book";
|
||||||
import { isArrOfStr } from "~/types/utils";
|
import { isArrOfStr } from "~/types/utils";
|
||||||
import { validateResponse } from "~/api";
|
import { validateResponse } from "~/api";
|
||||||
import { BookItem } from "~/pages/Bookshelf/BookItem";
|
|
||||||
|
|
||||||
const readBookList = <T>(
|
export const getTitleList = () => {
|
||||||
cb: (bookList: string[]) => T,
|
const titleListStr = localStorage.getItem("list") || "[]";
|
||||||
defaultValue: T extends void ? undefined : T
|
const titleList: unknown = JSON.parse(titleListStr);
|
||||||
) => {
|
|
||||||
const bookListStr = localStorage.getItem("list") || "[]";
|
|
||||||
const bookList: unknown = JSON.parse(bookListStr);
|
|
||||||
|
|
||||||
if (isArrOfStr(bookList)) return cb(bookList);
|
if (isArrOfStr(titleList)) return titleList;
|
||||||
return defaultValue;
|
else {
|
||||||
|
localStorage.setItem("list", "[]");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveBook = (bookObj: IBook, key: string) =>
|
export const getBookList = (titleList: string[]) =>
|
||||||
readBookList((bookList) => {
|
titleList
|
||||||
if (!bookList.includes(key)) {
|
.map<unknown>((hash) => JSON.parse(localStorage.getItem(hash) || "{}"))
|
||||||
const newBookList = [key, ...bookList];
|
.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));
|
export const setBook = (key: string, book: IBook) =>
|
||||||
localStorage.setItem(key, JSON.stringify(bookObj));
|
localStorage.setItem(key, JSON.stringify(book));
|
||||||
}
|
|
||||||
}, undefined);
|
|
||||||
|
|
||||||
export const readBooks = (): IBook[] =>
|
export const updateTitleList = (titleList: string[]) =>
|
||||||
readBookList(
|
localStorage.setItem("list", JSON.stringify(titleList));
|
||||||
(bookList) =>
|
|
||||||
bookList
|
|
||||||
.map((hash) => JSON.parse(localStorage.getItem(hash) || "{}"))
|
|
||||||
.filter((e) => {
|
|
||||||
try {
|
|
||||||
return validateResponse(e);
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user