commit
9fa8734c09
@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
build/
|
||||
build/
|
||||
pnpm-lock.yaml
|
||||
.vscode
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
build/
|
||||
build/
|
||||
pnpm-lock.yaml
|
||||
.vscode
|
@ -43,10 +43,6 @@ dokku git:from-image publitefrontend publite/frontend:latest
|
||||
|
||||
# TODO
|
||||
|
||||
- Create ServiceWorker (make it PWA)
|
||||
- Migrate from LocalStorage to IndexedDB
|
||||
- Add page position persistance
|
||||
- Migrate pagination cache and book state from LocalStorage to IndexedDB
|
||||
- Add menu with book view setting
|
||||
- Add move to page by number
|
||||
- Optimize page spliting algorythm (rewrite it)
|
||||
- Fix css modules bundling
|
||||
|
28
package.json
28
package.json
@ -1,20 +1,28 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "SNOWPACK_PUBLIC_API_URL=http://localhost:8081 SNOWPACK_PUBLIC_BASE_URL=http://localhost:8080 snowpack dev",
|
||||
"build": "snowpack build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"build": "webpack --config ./webpack.config.prod.js",
|
||||
"dev": "webpack serve --config ./webpack.config.dev.js --stats-error-details",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"h": "cat ./webpack.config.dev.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@snowpack/plugin-typescript": "^1.2.1",
|
||||
"@types/node": "^16.3.3",
|
||||
"@types/react": "^17.0.14",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/snowpack-env": "^2.3.3",
|
||||
"snowpack": "^3.8.0",
|
||||
"snowpack-plugin-svgr": "^0.1.2",
|
||||
"typescript": "^4.3.5"
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"css-loader": "^6.2.0",
|
||||
"esbuild-loader": "^2.13.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.13",
|
||||
"style-loader": "^3.2.1",
|
||||
"typescript": "^4.3.5",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
"webpack": "^5.46.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"idb": "^6.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"wouter": "^2.7.4"
|
||||
|
BIN
public/images/icons-128.png
Normal file
BIN
public/images/icons-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/icons-192.png
Normal file
BIN
public/images/icons-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
BIN
public/images/icons-512.png
Normal file
BIN
public/images/icons-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
public/images/screenshot1.jpg
Normal file
BIN
public/images/screenshot1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 599 KiB |
BIN
public/images/screenshot2.jpg
Normal file
BIN
public/images/screenshot2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
BIN
public/images/screenshot3.jpg
Normal file
BIN
public/images/screenshot3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
@ -3,12 +3,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/images/icons-512.png">
|
||||
<title>Publite</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/dist/index.js"></script>
|
||||
<script type="module" src="/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
47
public/manifest.json
Normal file
47
public/manifest.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"short_name": "Publite",
|
||||
"name": "Publite: eBook reader",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/icons-128.png",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icons-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#000000",
|
||||
"description": "eBook reader supporting EPUB and FB2 files",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot1.jpg",
|
||||
"type": "image/jpg",
|
||||
"sizes": "1080x2400"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshot2.jpg",
|
||||
"type": "image/jpg",
|
||||
"sizes": "1080x2400"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshot3.jpg",
|
||||
"type": "image/jpg",
|
||||
"sizes": "1080x2400"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// Snowpack Configuration File
|
||||
// See all supported options: https://www.snowpack.dev/reference/configuration
|
||||
|
||||
/** @type {import("snowpack").SnowpackUserConfig } */
|
||||
module.exports = {
|
||||
plugins: ["@snowpack/plugin-typescript", "snowpack-plugin-svgr"],
|
||||
packageOptions: {
|
||||
polyfillNode: true,
|
||||
},
|
||||
mount: {
|
||||
public: "/",
|
||||
src: "/dist",
|
||||
},
|
||||
optimize: {
|
||||
// bundle: true,
|
||||
},
|
||||
routes: [
|
||||
{ match: "routes", src: "robots.txt", dest: "/robots.txt" },
|
||||
{ match: "routes", src: ".*", dest: "/index.html" },
|
||||
],
|
||||
devOptions: {
|
||||
open: "none",
|
||||
},
|
||||
alias: {
|
||||
"~": "./src",
|
||||
},
|
||||
exclude: ["**/node_modules/**/*", "**/*.test.*"],
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { Route, Switch } from "wouter";
|
||||
import { BookListContextProvider } from "~/context";
|
||||
|
||||
import { Bookshelf } from "~/pages/Bookshelf";
|
||||
import { BookView } from "~/pages/BookView";
|
||||
@ -24,19 +23,17 @@ export const App = () => {
|
||||
</div>
|
||||
)}
|
||||
<Navbar />
|
||||
<BookListContextProvider>
|
||||
<Switch>
|
||||
<Route path="/upload">
|
||||
<UploadForm setLoading={setLoading} loading={loading} />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Bookshelf setLoading={setLoading} loading={loading} />
|
||||
</Route>
|
||||
<Route path="/:hash">
|
||||
<BookView setLoading={setLoading} loading={loading} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</BookListContextProvider>
|
||||
<Switch>
|
||||
<Route path="/upload">
|
||||
<UploadForm setLoading={setLoading} loading={loading} />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Bookshelf setLoading={setLoading} loading={loading} />
|
||||
</Route>
|
||||
<Route path="/:hash">
|
||||
<BookView setLoading={setLoading} loading={loading} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,2 +1,5 @@
|
||||
export const API_URL = import.meta.env.SNOWPACK_PUBLIC_API_URL;
|
||||
export const BASE_URL = import.meta.env.SNOWPACK_PUBLIC_BASE_URL;
|
||||
export const API_URL = process.env.PUBLIC_API_URL || "";
|
||||
export const BASE_URL = process.env.PUBLIC_BASE_URL || "";
|
||||
export const CACHE = "v1.0.1";
|
||||
export const DB_NAME = "publite";
|
||||
export const DB_VERSION = 101;
|
||||
|
@ -1,19 +0,0 @@
|
||||
import React from "react";
|
||||
import { useLibrary, UseLibraryReturnTuple } from "./hooks/useLibrary";
|
||||
import { BookT } from "./types/book";
|
||||
|
||||
export const BookListContext = React.createContext<UseLibraryReturnTuple>([
|
||||
{},
|
||||
(book: BookT) => {},
|
||||
[],
|
||||
]);
|
||||
|
||||
export const BookListContextProvider: React.FC = ({ children }) => {
|
||||
const library = useLibrary();
|
||||
|
||||
return (
|
||||
<BookListContext.Provider value={library}>
|
||||
{children}
|
||||
</BookListContext.Provider>
|
||||
);
|
||||
};
|
@ -21,11 +21,8 @@ export const useBookState = (
|
||||
}, [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 });
|
||||
@ -36,7 +33,6 @@ export const useBookState = (
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (hash) {
|
||||
console.log(currentPage);
|
||||
if (ready && state) saveBookState(hash, state);
|
||||
else saveBookState(hash, { currentPage: currentPage.current || 0 });
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import * as ServiceWorker from "./registerServiceWorker";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@ -12,6 +13,4 @@ ReactDOM.render(
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept();
|
||||
}
|
||||
ServiceWorker.register();
|
||||
|
@ -1,29 +1,52 @@
|
||||
import React, { MouseEventHandler, useContext, useEffect, useRef } from "react";
|
||||
import React, { MouseEventHandler, useState, useEffect, useRef } from "react";
|
||||
import { Redirect, useRoute } from "wouter";
|
||||
|
||||
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";
|
||||
import { BookT } from "~/types/book";
|
||||
import { API_URL } from "~/constants";
|
||||
import { validateResponse } from "~/utils/api";
|
||||
|
||||
export const BookView = ({ setLoading, loading }: IPageProps) => {
|
||||
useEffect(() => setLoading(true), []);
|
||||
|
||||
const [hasErr, setHasErr] = useState(false);
|
||||
const [book, setBook] = useState<BookT>();
|
||||
|
||||
const [_, params] = useRoute("/:hash");
|
||||
const [books] = useContext(BookListContext);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const pageContainerRef = useRef<HTMLDivElement>(null);
|
||||
const pageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (params?.hash) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(API_URL + "/book/" + params.hash);
|
||||
if (!res.ok) throw new Error(res.status + " " + res.statusText);
|
||||
|
||||
const book: unknown = await res.json();
|
||||
|
||||
if (validateResponse(book)) setBook(book);
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV === "development") console.error(err);
|
||||
setHasErr(true);
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [pagesReady, goToPage, currentPage, pagesNumber] = usePagination(
|
||||
contentRef,
|
||||
pageContainerRef,
|
||||
pageRef,
|
||||
books && loading ? params?.hash : undefined,
|
||||
params?.hash && books && loading ? books[params.hash]?.content : undefined
|
||||
book ? params?.hash : undefined,
|
||||
params?.hash ? book?.content : undefined
|
||||
);
|
||||
|
||||
const currentPageRef = useRef(currentPage);
|
||||
@ -68,35 +91,33 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (books) {
|
||||
if (params?.hash && params.hash in books)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${styles.border} ${styles.leftBorder}`}
|
||||
onClick={goPrev}
|
||||
/>
|
||||
<div className={styles.content} ref={contentRef} />
|
||||
<div className={styles.pageContainer} ref={pageContainerRef}>
|
||||
<div className={styles.page} ref={pageRef} onClick={goNext} />
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.border} ${styles.rightBorder}`}
|
||||
onClick={goNext}
|
||||
/>
|
||||
<div className={styles.pageIndicator}>
|
||||
<button className={styles.pageSwitchArrow} onClick={goPrev}>
|
||||
{currentPage !== 0 && "←"}
|
||||
</button>
|
||||
<span className={styles.pageNumber} onClick={insertNumber}>
|
||||
{currentPage + 1} / {pagesNumber}
|
||||
</span>
|
||||
<button className={styles.pageSwitchArrow} onClick={goNext}>
|
||||
{currentPage !== pagesNumber - 1 && "→"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <Redirect to="/" />;
|
||||
} else return <></>;
|
||||
if (hasErr) return <Redirect to="/" />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${styles.border} ${styles.leftBorder}`}
|
||||
onClick={goPrev}
|
||||
/>
|
||||
<div className={styles.content} ref={contentRef} />
|
||||
<div className={styles.pageContainer} ref={pageContainerRef}>
|
||||
<div className={styles.page} ref={pageRef} onClick={goNext} />
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.border} ${styles.rightBorder}`}
|
||||
onClick={goNext}
|
||||
/>
|
||||
<div className={styles.pageIndicator}>
|
||||
<button className={styles.pageSwitchArrow} onClick={goPrev}>
|
||||
{currentPage !== 0 && "←"}
|
||||
</button>
|
||||
<span className={styles.pageNumber} onClick={insertNumber}>
|
||||
{currentPage + 1} / {pagesNumber}
|
||||
</span>
|
||||
<button className={styles.pageSwitchArrow} onClick={goNext}>
|
||||
{currentPage !== pagesNumber - 1 && "→"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +1,29 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import styles from "./Bookshelf.module.css";
|
||||
|
||||
import { BookItem } from "./BookItem";
|
||||
import { AddBook } from "./AddBook";
|
||||
import { BookListContext } from "~/context";
|
||||
import { IPageProps } from "~/types/page";
|
||||
import { API_URL } from "~/constants";
|
||||
import { BookT } from "~/types/book";
|
||||
import { connected as swConnected } from "~/utils/serviceFetch";
|
||||
|
||||
export const Bookshelf = ({ setLoading }: IPageProps) => {
|
||||
useEffect(() => setLoading(true), []);
|
||||
|
||||
const [books] = useContext(BookListContext);
|
||||
const [books, setBooks] = useState<BookT[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
swConnected.then(async () => {
|
||||
try {
|
||||
const res = await fetch(API_URL + "/list");
|
||||
setBooks(await res.json());
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV === "development") console.error(err);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (books) setLoading(false);
|
||||
|
@ -1,18 +1,15 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
import PlusIcon from "~/assets/plus.svg";
|
||||
import styles from "./UploadForm.module.css";
|
||||
import { submitFile, validateResponse, validState } from "~/utils/api";
|
||||
import { BookListContext } from "~/context";
|
||||
import { IPageProps } from "~/types/page";
|
||||
|
||||
export const UploadForm = ({ setLoading }: IPageProps) => {
|
||||
const [error, setError] = useState("");
|
||||
const [_, setLocation] = useLocation();
|
||||
|
||||
const [__, saveBook] = useContext(BookListContext);
|
||||
|
||||
const processFile = async (file: File | undefined) => {
|
||||
try {
|
||||
if (validState(file)) {
|
||||
@ -22,10 +19,7 @@ export const UploadForm = ({ setLoading }: IPageProps) => {
|
||||
const res = await submitFile(file);
|
||||
setLoading(false);
|
||||
|
||||
if (validateResponse(res)) {
|
||||
saveBook(res);
|
||||
setLocation("/");
|
||||
}
|
||||
if (validateResponse(res)) setLocation("/");
|
||||
}
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
|
16
src/registerServiceWorker.ts
Normal file
16
src/registerServiceWorker.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const register = () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () =>
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js", { scope: "" })
|
||||
.then((registration) => {
|
||||
if (process.env.NODE_ENV === "development")
|
||||
console.log(
|
||||
"Successfully registered ServiceWorker with scope:",
|
||||
registration.scope
|
||||
);
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
);
|
||||
}
|
||||
};
|
23
src/serviceWorker/cache.ts
Normal file
23
src/serviceWorker/cache.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CACHE } from "../constants";
|
||||
|
||||
const getCache = () => caches.open(CACHE);
|
||||
|
||||
/**
|
||||
* Caches static files for application
|
||||
*/
|
||||
export const precache = async () =>
|
||||
(await getCache()).addAll(["/", "/index.js", "/sw.js"]);
|
||||
|
||||
/**
|
||||
* Requests file from network or gets it from cache if offline
|
||||
*/
|
||||
export const fromCache = async (request: Request): Promise<Response> => {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
(await getCache()).put(request, response.clone());
|
||||
return response;
|
||||
} catch (err) {
|
||||
const response = await (await getCache()).match(request);
|
||||
return response || new Response();
|
||||
}
|
||||
};
|
31
src/serviceWorker/db.ts
Normal file
31
src/serviceWorker/db.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { openDB as idbOpenDB } from "idb";
|
||||
import { BookT } from "~/types/book";
|
||||
import { DB_NAME, DB_VERSION } from "../constants";
|
||||
import { PubliteDB } from "./schema";
|
||||
|
||||
/**
|
||||
* Opens IndexedDB for interactions
|
||||
*/
|
||||
export const openDB = () =>
|
||||
idbOpenDB<PubliteDB>(DB_NAME, DB_VERSION, {
|
||||
upgrade: (db, oldVersion, _, tsx) => {
|
||||
if (oldVersion < 1) db.createObjectStore("Books", { keyPath: "hash" });
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Saves IBook object in IndexedDB
|
||||
*/
|
||||
export const saveBook = async (book: BookT) =>
|
||||
(await openDB()).add("Books", book);
|
||||
|
||||
/**
|
||||
* Returns all books saved in IndexedDB
|
||||
*/
|
||||
export const getBooks = async () => (await openDB()).getAll("Books");
|
||||
|
||||
/**
|
||||
* Gets book from IndexedDB by hash
|
||||
*/
|
||||
export const getBook = async (hash: string) =>
|
||||
(await openDB()).get("Books", hash);
|
59
src/serviceWorker/fetchHandlers.ts
Normal file
59
src/serviceWorker/fetchHandlers.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { getBook, getBooks, saveBook } from "./db";
|
||||
import { composeResponseStatus } from "./utils";
|
||||
|
||||
export interface PathHandler {
|
||||
/** Path start for handler */
|
||||
path: string;
|
||||
/** Function returning Response object */
|
||||
getResponse: () => Response | Promise<Response>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes fetch request path to specified handler
|
||||
*/
|
||||
export const handle = (requestPath: string, table: PathHandler[]) => {
|
||||
for (const { path, getResponse: response } of table)
|
||||
if (requestPath.startsWith(path)) return response();
|
||||
|
||||
return new Response();
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts book to html with publiteBackend server.
|
||||
*
|
||||
* First fetch handler with network request
|
||||
*/
|
||||
export const handleBookUpload = async (request: Request) => {
|
||||
try {
|
||||
const res = await fetch(request);
|
||||
if (res.ok) {
|
||||
const book = await res.json();
|
||||
await saveBook(book);
|
||||
return new Response(JSON.stringify(book));
|
||||
} else throw new Error(res.status.toString() + res.statusText);
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify(err), composeResponseStatus(err));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all books from database
|
||||
*/
|
||||
export const handleBooks = async () => {
|
||||
const list = await getBooks();
|
||||
|
||||
return new Response(JSON.stringify(list));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets book from database
|
||||
*/
|
||||
export const handleBook = async (request: Request, hash: string) => {
|
||||
const book = await getBook(hash);
|
||||
if (book) return new Response(JSON.stringify(book));
|
||||
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
statusText: "No such book :(",
|
||||
});
|
||||
};
|
39
src/serviceWorker/index.ts
Normal file
39
src/serviceWorker/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { API_URL } from "~/constants";
|
||||
import { fromCache, precache } from "./cache";
|
||||
import { openDB as createDB } from "./db";
|
||||
import {
|
||||
handle,
|
||||
handleBook,
|
||||
handleBooks,
|
||||
handleBookUpload,
|
||||
PathHandler,
|
||||
} from "./fetchHandlers";
|
||||
import { getHash } from "./utils";
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
self.addEventListener("install", (event) => event.waitUntil(precache()));
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
self.clients.claim();
|
||||
event.waitUntil(createDB());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
const path = new URL(request.url).pathname;
|
||||
|
||||
let handlers: PathHandler[];
|
||||
|
||||
if (request.url.startsWith(API_URL)) {
|
||||
handlers = [
|
||||
{ path: "/list", getResponse: () => handleBooks() },
|
||||
{ path: "/book/", getResponse: () => handleBook(request, getHash(path)) },
|
||||
{ path: "/uploadfile", getResponse: () => handleBookUpload(request) },
|
||||
];
|
||||
} else {
|
||||
handlers = [{ path: "", getResponse: () => fromCache(request) }];
|
||||
}
|
||||
|
||||
event.respondWith(handle(path, handlers));
|
||||
});
|
9
src/serviceWorker/schema.ts
Normal file
9
src/serviceWorker/schema.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { DBSchema } from "idb";
|
||||
import { BookT } from "../types/book";
|
||||
|
||||
export interface PubliteDB extends DBSchema {
|
||||
Books: {
|
||||
key: string;
|
||||
value: BookT;
|
||||
};
|
||||
}
|
15
src/serviceWorker/utils.ts
Normal file
15
src/serviceWorker/utils.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Gets hash string from book path
|
||||
*/
|
||||
export const getHash = (path: string) => {
|
||||
let hashLength = path.length - "/book/".length;
|
||||
if (path.endsWith("/")) hashLength--;
|
||||
|
||||
return path.substr("/book/".length, hashLength);
|
||||
};
|
||||
|
||||
export const composeResponseStatus = (err: Error): ResponseInit => {
|
||||
if (err.name === "NetowrkError")
|
||||
return { status: 503, statusText: err.message };
|
||||
else return { status: 500, statusText: "Something bad happened (IDK)" };
|
||||
};
|
@ -34,7 +34,7 @@ export const submitFile = async (
|
||||
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.log("Network error:", err.message);
|
||||
console.error("Network error:", err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@ -43,9 +43,9 @@ export const validateResponse = (content: unknown): content is BookT => {
|
||||
if (content && typeof content === "object")
|
||||
for (const key of requiredBookProps)
|
||||
if (!(key in content)) {
|
||||
if (import.meta.env.NODE_ENV === "development")
|
||||
console.log(`${key} is not specified in server response`);
|
||||
return false;
|
||||
if (process.env.NODE_ENV === "development")
|
||||
console.error(`${key} is not specified in server response`);
|
||||
throw new Error(`${key} is not specified in server response`);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
6
src/utils/serviceFetch.ts
Normal file
6
src/utils/serviceFetch.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const connected = new Promise<void>((resolve) => {
|
||||
if (navigator.serviceWorker.controller) return resolve();
|
||||
navigator.serviceWorker.addEventListener("controllerchange", (e) =>
|
||||
resolve()
|
||||
);
|
||||
});
|
@ -1,17 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM"],
|
||||
"lib": ["DOM", "webworker"],
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "es6"
|
||||
}
|
||||
}
|
||||
|
53
webpack.config.common.js
Normal file
53
webpack.config.common.js
Normal file
@ -0,0 +1,53 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const ForkTsCheckerPlugin = require("fork-ts-checker-webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
"index.js": "./src/index.tsx",
|
||||
"sw.js": "./src/serviceWorker/index.ts",
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, `./build/`),
|
||||
clean: true,
|
||||
filename: "[name]",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "esbuild-loader",
|
||||
options: {
|
||||
loader: "tsx",
|
||||
target: "es2015",
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: ["@svgr/webpack"],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".json"],
|
||||
alias: { "~": path.resolve(__dirname, "src/") },
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.PUBLIC_API_URL": JSON.stringify(
|
||||
"http://localhost:8081"
|
||||
),
|
||||
}),
|
||||
new ForkTsCheckerPlugin(),
|
||||
new CopyPlugin({
|
||||
patterns: [{ from: "./public", to: "." }],
|
||||
}),
|
||||
],
|
||||
};
|
27
webpack.config.dev.js
Normal file
27
webpack.config.dev.js
Normal file
@ -0,0 +1,27 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const webpackConfig = require("./webpack.config.common");
|
||||
|
||||
module.exports = {
|
||||
...webpackConfig,
|
||||
mode: "development",
|
||||
watchOptions: { ignored: /node_modules/ },
|
||||
devServer: {
|
||||
contentBase: path.join(__dirname, "build"),
|
||||
compress: true,
|
||||
port: 8080,
|
||||
hot: false,
|
||||
inline: false,
|
||||
historyApiFallback: {
|
||||
index: "index.html",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.NODE_ENV": JSON.stringify("development"),
|
||||
"process.env.PUBLIC_BASE_URL": JSON.stringify("http://localhost:8080"),
|
||||
}),
|
||||
...webpackConfig.plugins,
|
||||
],
|
||||
};
|
21
webpack.config.prod.js
Normal file
21
webpack.config.prod.js
Normal file
@ -0,0 +1,21 @@
|
||||
const { ESBuildMinifyPlugin } = require("esbuild-loader");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const webpackConfig = require("./webpack.config.common");
|
||||
|
||||
module.exports = {
|
||||
...webpackConfig,
|
||||
optimization: {
|
||||
minimizer: [new ESBuildMinifyPlugin()],
|
||||
},
|
||||
mode: "production",
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
"process.env.PUBLIC_BASE_URL": JSON.stringify(
|
||||
"https://publite.dmitriy.icu"
|
||||
),
|
||||
}),
|
||||
...webpackConfig.plugins,
|
||||
],
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user