Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
@ -1,5 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
build/
|
build/
|
||||||
pnpm-lock.yaml
|
|
||||||
.vscode
|
|
5
.gitignore
vendored
@ -1,6 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
build/
|
build/
|
||||||
pnpm-lock.yaml
|
|
||||||
.vscode
|
|
||||||
.vercel
|
|
@ -4,12 +4,13 @@ WORKDIR /app
|
|||||||
COPY ./package.json ./
|
COPY ./package.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
ARG PUBLIC_API_URL=https://publitebackend.dm1sh.ru
|
ENV SNOWPACK_PUBLIC_API_URL=https://publitebackend.dmitriy.icu
|
||||||
ARG PUBLIC_BASE_URL=https://publite.dm1sh.ru
|
ENV SNOWPACK_PUBLIC_BASE_URL=https://publite.dmitriy.icu
|
||||||
RUN NODE_ENV=production npm run build
|
RUN NODE_ENV=production npm run build
|
||||||
|
|
||||||
FROM node:alpine
|
FROM node:alpine
|
||||||
RUN npm install serve -g --silent
|
RUN npm install serve -g --silent
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/build .
|
COPY --from=builder /app/build .
|
||||||
CMD serve -p ${PORT:-8080} -s .
|
EXPOSE 5000
|
||||||
|
CMD ["serve", "-p", "5000", "-s", "."]
|
23
README.md
@ -8,20 +8,6 @@
|
|||||||
|
|
||||||
Frontend for Publite service — E-Books reader
|
Frontend for Publite service — E-Books reader
|
||||||
|
|
||||||
<table style="margin: auto">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<img style="max-height: 50vh" src="public/images/screenshot3.webp"/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<img style="max-height: 50vh" src="public/images/screenshot2.webp"/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<img style="max-height: 50vh" src="public/images/screenshot1.webp"/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
Dev environment setup:
|
Dev environment setup:
|
||||||
@ -41,10 +27,10 @@ Simple docker deployment
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# build docker image
|
# build docker image
|
||||||
docker build . --build-arg PUBLIC_API_URL=<https://...> --build-arg PUBLIC_BASE_URL=<https://...> -t publite_frontend
|
docker build . -t publite_frontend
|
||||||
|
|
||||||
# run it with docker
|
# run it with docker
|
||||||
docker run -p <port>:8080 publite_frontend
|
docker run -p <port>:5000 publite_frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Dokku deployment with image from Docker Hub
|
Dokku deployment with image from Docker Hub
|
||||||
@ -55,8 +41,7 @@ dokku apps:create publitefrontend
|
|||||||
dokku git:from-image publitefrontend publite/frontend:latest
|
dokku git:from-image publitefrontend publite/frontend:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO
|
# TODO
|
||||||
|
|
||||||
- Migrate pagination cache and book state from LocalStorage to IndexedDB
|
|
||||||
- Add menu with book view setting
|
|
||||||
- Optimize page spliting algorythm (rewrite it)
|
- Optimize page spliting algorythm (rewrite it)
|
||||||
|
- Fix css modules bundling
|
||||||
|
27
package.json
@ -1,27 +1,20 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config ./webpack.config.prod.js",
|
"dev": "SNOWPACK_PUBLIC_API_URL=http://localhost:8081 SNOWPACK_PUBLIC_BASE_URL=http://localhost:8080 snowpack dev",
|
||||||
"dev": "webpack serve --config ./webpack.config.dev.js --stats-error-details",
|
"build": "snowpack build",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
"h": "cat ./webpack.config.dev.js"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "^6.3.1",
|
"@snowpack/plugin-typescript": "^1.2.1",
|
||||||
"@types/react": "^17.0.15",
|
"@types/node": "^16.3.3",
|
||||||
|
"@types/react": "^17.0.14",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
"@types/snowpack-env": "^2.3.3",
|
||||||
"css-loader": "^6.2.0",
|
"snowpack": "^3.8.0",
|
||||||
"esbuild-loader": "^2.13.1",
|
"snowpack-plugin-svgr": "^0.1.2",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.2.13",
|
"typescript": "^4.3.5"
|
||||||
"style-loader": "^3.2.1",
|
|
||||||
"typescript": "^4.3.5",
|
|
||||||
"webpack": "^5.46.0",
|
|
||||||
"webpack-cli": "^4.7.2",
|
|
||||||
"webpack-dev-server": "^4.11.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"idb": "^6.1.2",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"wouter": "^2.7.4"
|
"wouter": "^2.7.4"
|
||||||
|
@ -1 +0,0 @@
|
|||||||
/
|
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 261 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 22 KiB |
@ -3,15 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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="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>
|
<title>Publite</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/index.js"></script>
|
<script type="module" src="/dist/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"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.webp",
|
|
||||||
"type": "image/webp",
|
|
||||||
"sizes": "1080x2400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/images/screenshot2.webp",
|
|
||||||
"type": "image/webp",
|
|
||||||
"sizes": "1080x2400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/images/screenshot3.webp",
|
|
||||||
"type": "image/webp",
|
|
||||||
"sizes": "1080x2400"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
28
snowpack.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } 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";
|
||||||
@ -23,17 +24,19 @@ export const App = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Switch>
|
<BookListContextProvider>
|
||||||
<Route path="/upload">
|
<Switch>
|
||||||
<UploadForm setLoading={setLoading} loading={loading} />
|
<Route path="/upload">
|
||||||
</Route>
|
<UploadForm setLoading={setLoading} loading={loading} />
|
||||||
<Route path="/">
|
</Route>
|
||||||
<Bookshelf setLoading={setLoading} loading={loading} />
|
<Route path="/">
|
||||||
</Route>
|
<Bookshelf setLoading={setLoading} loading={loading} />
|
||||||
<Route path="/:hash">
|
</Route>
|
||||||
<BookView setLoading={setLoading} loading={loading} />
|
<Route path="/:hash">
|
||||||
</Route>
|
<BookView setLoading={setLoading} loading={loading} />
|
||||||
</Switch>
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</BookListContextProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,2 @@
|
|||||||
export const API_URL = process.env.PUBLIC_API_URL || "";
|
export const API_URL = import.meta.env.SNOWPACK_PUBLIC_API_URL;
|
||||||
export const BASE_URL = process.env.PUBLIC_BASE_URL || "";
|
export const BASE_URL = import.meta.env.SNOWPACK_PUBLIC_BASE_URL;
|
||||||
export const CACHE = "v1.1.1";
|
|
||||||
export const DB_NAME = "publite";
|
|
||||||
export const DB_VERSION = 111;
|
|
||||||
|
19
src/context.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,44 +0,0 @@
|
|||||||
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(() => {
|
|
||||||
if (!ready && state?.currentPage && pagesReady) {
|
|
||||||
goToPage(state.currentPage);
|
|
||||||
setReady(true);
|
|
||||||
} else if (hash && !ready && pagesReady && typeof state === "object") {
|
|
||||||
saveBookState(hash, { currentPage: 0 });
|
|
||||||
setReady(true);
|
|
||||||
}
|
|
||||||
}, [ready, state, goToPage]);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
if (hash) {
|
|
||||||
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 { useEffect, useState } from "react";
|
||||||
import { BookT } from "~/types/book";
|
import { IBook } from "~/types/book";
|
||||||
import {
|
import {
|
||||||
getBookHT,
|
getBookHT,
|
||||||
getHashList,
|
getHashList,
|
||||||
@ -7,16 +7,16 @@ import {
|
|||||||
updateHashList,
|
updateHashList,
|
||||||
} from "~/utils/localStorage";
|
} from "~/utils/localStorage";
|
||||||
|
|
||||||
export type AddBookFT = (book: BookT) => void;
|
export type AddBookFT = (book: IBook) => void;
|
||||||
|
|
||||||
export type UseLibraryReturnTuple = [
|
export type UseLibraryReturnTuple = [
|
||||||
Record<string, BookT> | null,
|
Record<string, IBook> | null,
|
||||||
AddBookFT,
|
AddBookFT,
|
||||||
string[]
|
string[]
|
||||||
];
|
];
|
||||||
|
|
||||||
export const useLibrary = (): UseLibraryReturnTuple => {
|
export const useLibrary = (): UseLibraryReturnTuple => {
|
||||||
const [library, setLibrary] = useState<Record<string, BookT> | null>(null);
|
const [library, setLibrary] = useState<Record<string, IBook> | null>(null);
|
||||||
const [hashList, setHashList] = useState<string[]>([]);
|
const [hashList, setHashList] = useState<string[]>([]);
|
||||||
|
|
||||||
const addBook: AddBookFT = (book) => {
|
const addBook: AddBookFT = (book) => {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import * as ServiceWorker from "./registerServiceWorker";
|
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
@ -13,4 +12,6 @@ ReactDOM.render(
|
|||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
|
||||||
ServiceWorker.register();
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.accept();
|
||||||
|
}
|
||||||
|
@ -31,19 +31,13 @@
|
|||||||
|
|
||||||
.page {
|
.page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 1.25rem;
|
|
||||||
font-family: serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page img {
|
img {
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageIndicator {
|
.pageIndicator {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -56,10 +50,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageNumber {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageSwitchArrow {
|
.pageSwitchArrow {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -1,52 +1,28 @@
|
|||||||
import React, { MouseEventHandler, useState, useEffect, useRef } from "react";
|
import React, { useContext, useEffect, useRef } from "react";
|
||||||
import { Redirect, useRoute } from "wouter";
|
import { Redirect, useRoute } from "wouter";
|
||||||
|
|
||||||
import styles from "./BookView.module.css";
|
import styles from "./BookView.module.css";
|
||||||
|
|
||||||
|
import { BookListContext } from "~/context";
|
||||||
import { usePagination } from "~/hooks/usePagination";
|
import { usePagination } from "~/hooks/usePagination";
|
||||||
import { IPageProps } from "~/types/page";
|
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) => {
|
export const BookView = ({ setLoading, loading }: IPageProps) => {
|
||||||
useEffect(() => setLoading(true), []);
|
useEffect(() => setLoading(true), []);
|
||||||
|
|
||||||
const [hasErr, setHasErr] = useState(false);
|
|
||||||
const [book, setBook] = useState<BookT>();
|
|
||||||
|
|
||||||
const [_, params] = useRoute("/:hash");
|
const [_, params] = useRoute("/:hash");
|
||||||
|
const [books] = useContext(BookListContext);
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const pageContainerRef = useRef<HTMLDivElement>(null);
|
const pageContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const pageRef = useRef<HTMLDivElement>(null);
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const [ready, goToPage, currentPage, pagesNumber] = usePagination(
|
||||||
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,
|
contentRef,
|
||||||
pageContainerRef,
|
pageContainerRef,
|
||||||
pageRef,
|
pageRef,
|
||||||
book ? params?.hash : undefined,
|
books && loading ? params?.hash : undefined,
|
||||||
params?.hash ? book?.content : undefined
|
params?.hash && books && loading ? books[params.hash]?.content : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentPageRef = useRef(currentPage);
|
const currentPageRef = useRef(currentPage);
|
||||||
@ -55,15 +31,8 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
|
|||||||
currentPageRef.current = currentPage;
|
currentPageRef.current = currentPage;
|
||||||
}, [currentPage]);
|
}, [currentPage]);
|
||||||
|
|
||||||
const [bookStateReady, bs] = useBookState(
|
|
||||||
pagesReady,
|
|
||||||
params?.hash,
|
|
||||||
goToPage,
|
|
||||||
currentPageRef
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bookStateReady) {
|
if (ready) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
const handleKey = ({ key }: KeyboardEvent) => {
|
const handleKey = ({ key }: KeyboardEvent) => {
|
||||||
@ -79,45 +48,40 @@ export const BookView = ({ setLoading, loading }: IPageProps) => {
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKey);
|
window.addEventListener("keydown", handleKey);
|
||||||
}
|
}
|
||||||
}, [bookStateReady]);
|
}, [ready]);
|
||||||
|
|
||||||
const goPrev = () => goToPage(currentPage - 1);
|
const goPrev = () => goToPage(currentPage - 1);
|
||||||
const goNext = () => 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 (hasErr) return <Redirect to="/" />;
|
if (books) {
|
||||||
|
if (params?.hash && params.hash in books)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`${styles.border} ${styles.leftBorder}`}
|
className={`${styles.border} ${styles.leftBorder}`}
|
||||||
onClick={goPrev}
|
onClick={goPrev}
|
||||||
/>
|
/>
|
||||||
<div className={styles.content} ref={contentRef} />
|
<div className={styles.content} ref={contentRef} />
|
||||||
<div className={styles.pageContainer} ref={pageContainerRef}>
|
<div className={styles.pageContainer} ref={pageContainerRef}>
|
||||||
<div className={styles.page} ref={pageRef} onClick={goNext} />
|
<div className={styles.page} ref={pageRef} onClick={goNext} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`${styles.border} ${styles.rightBorder}`}
|
className={`${styles.border} ${styles.rightBorder}`}
|
||||||
onClick={goNext}
|
onClick={goNext}
|
||||||
/>
|
/>
|
||||||
<div className={styles.pageIndicator}>
|
<div className={styles.pageIndicator}>
|
||||||
<button className={styles.pageSwitchArrow} onClick={goPrev}>
|
<button className={styles.pageSwitchArrow} onClick={goPrev}>
|
||||||
{currentPage !== 0 && "←"}
|
{currentPage !== 0 && "←"}
|
||||||
</button>
|
</button>
|
||||||
<span className={styles.pageNumber} onClick={insertNumber}>
|
<span>
|
||||||
{currentPage + 1} / {pagesNumber}
|
{currentPage + 1} / {pagesNumber}
|
||||||
</span>
|
</span>
|
||||||
<button className={styles.pageSwitchArrow} onClick={goNext}>
|
<button className={styles.pageSwitchArrow} onClick={goNext}>
|
||||||
{currentPage !== pagesNumber - 1 && "→"}
|
{currentPage !== pagesNumber - 1 && "→"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
return <Redirect to="/" />;
|
||||||
|
} else return <></>;
|
||||||
};
|
};
|
||||||
|
@ -2,10 +2,10 @@ import React from "react";
|
|||||||
|
|
||||||
import styles from "./BookItem.module.css";
|
import styles from "./BookItem.module.css";
|
||||||
|
|
||||||
import { BookT } from "~/types/book";
|
import { IBook } from "~/types/book";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
|
||||||
interface IBookItemProps extends BookT {}
|
interface IBookItemProps extends IBook {}
|
||||||
|
|
||||||
export const BookItem = ({ author, title, cover, hash }: IBookItemProps) => {
|
export const BookItem = ({ author, title, cover, hash }: IBookItemProps) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,29 +1,16 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
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 { BookListContext } from "~/context";
|
||||||
import { IPageProps } from "~/types/page";
|
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) => {
|
export const Bookshelf = ({ setLoading }: IPageProps) => {
|
||||||
useEffect(() => setLoading(true), []);
|
useEffect(() => setLoading(true), []);
|
||||||
|
|
||||||
const [books, setBooks] = useState<BookT[]>([]);
|
const [books] = useContext(BookListContext);
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (books) setLoading(false);
|
if (books) setLoading(false);
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useContext, useEffect, 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 "~/utils/api";
|
import { submitFile, validateResponse, validState } from "~/utils/api";
|
||||||
|
import { BookListContext } from "~/context";
|
||||||
import { IPageProps } from "~/types/page";
|
import { IPageProps } from "~/types/page";
|
||||||
|
|
||||||
export const UploadForm = ({ setLoading }: IPageProps) => {
|
export const UploadForm = ({ setLoading }: IPageProps) => {
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
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)) {
|
||||||
@ -19,15 +22,14 @@ export const UploadForm = ({ setLoading }: IPageProps) => {
|
|||||||
const res = await submitFile(file);
|
const res = await submitFile(file);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (validateResponse(res)) setLocation("/");
|
if (validateResponse(res)) {
|
||||||
|
saveBook(res);
|
||||||
|
setLocation("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setError(err.message);
|
||||||
if (err instanceof Error)
|
|
||||||
setError(err.message);
|
|
||||||
else
|
|
||||||
setError(String(err))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
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",
|
|
||||||
"/manifest.json",
|
|
||||||
"/favicon.ico",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
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);
|
|
@ -1,59 +0,0 @@
|
|||||||
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 :(",
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,39 +0,0 @@
|
|||||||
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));
|
|
||||||
});
|
|
@ -1,9 +0,0 @@
|
|||||||
import { DBSchema } from "idb";
|
|
||||||
import { BookT } from "../types/book";
|
|
||||||
|
|
||||||
export interface PubliteDB extends DBSchema {
|
|
||||||
Books: {
|
|
||||||
key: string;
|
|
||||||
value: BookT;
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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: unknown): ResponseInit => {
|
|
||||||
if (err instanceof Error && err.name === "NetowrkError")
|
|
||||||
return { status: 503, statusText: err.message };
|
|
||||||
else return { status: 500, statusText: "Something bad happened (IDK)" };
|
|
||||||
};
|
|
@ -1,13 +1,9 @@
|
|||||||
export const requiredBookProps = ["title", "author", "content"] as const;
|
export const requiredBookProps = ["title", "author", "content"] as const;
|
||||||
export const optionalBookProps = ["cover", "hash"] as const;
|
export const optionalBookProps = ["cover", "hash"] as const;
|
||||||
|
|
||||||
export type BookT = {
|
export type IBook = {
|
||||||
[key in typeof requiredBookProps[number]]: string;
|
[key in typeof requiredBookProps[number]]: string;
|
||||||
} &
|
} &
|
||||||
{
|
{
|
||||||
[key in typeof optionalBookProps[number]]: string | undefined;
|
[key in typeof optionalBookProps[number]]: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BookState = {
|
|
||||||
currentPage: number;
|
|
||||||
};
|
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import { BookT, requiredBookProps } from "~/types/book";
|
import { IBook, requiredBookProps } from "~/types/book";
|
||||||
|
|
||||||
import { API_URL } from "~/constants";
|
import { API_URL } from "~/constants";
|
||||||
|
|
||||||
export const validState = (file: File | undefined): file is File => {
|
export const validState = (file: File | undefined): file is File => {
|
||||||
if (!file) throw new Error("Book file is required. Please, attach one");
|
if (!file) throw new Error("Book file is required. Please, attach one");
|
||||||
|
|
||||||
if (file.name.endsWith(".zip"))
|
|
||||||
throw new Error(
|
|
||||||
"Please, unzip file before sending to reader if it is fb2.zip"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!file.name.match(/\.(fb2|epub)/))
|
if (!file.name.match(/\.(fb2|epub)/))
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Wrong file type. Only FB2 and Epub files are supported. \
|
"Wrong file type. Only FB2 and Epub files are supported. \
|
||||||
@ -39,20 +34,18 @@ export const submitFile = async (
|
|||||||
|
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error)
|
console.log("Network error:", err.message);
|
||||||
console.error("Network error:", err.message);
|
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateResponse = (content: unknown): content is BookT => {
|
export const validateResponse = (content: unknown): content is IBook => {
|
||||||
if (content && typeof content === "object")
|
if (content && typeof content === "object")
|
||||||
for (const key of requiredBookProps)
|
for (const key of requiredBookProps)
|
||||||
if (!(key in content)) {
|
if (!(key in content)) {
|
||||||
if (process.env.NODE_ENV === "development")
|
if (import.meta.env.NODE_ENV === "development")
|
||||||
console.error(`${key} is not specified in server response`);
|
console.log(`${key} is not specified in server response`);
|
||||||
throw new Error(`${key} is not specified in server response`);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BookState, BookT } from "~/types/book";
|
import { IBook } from "~/types/book";
|
||||||
import { isArrOfStr } from "~/types/utils";
|
import { isArrOfStr } from "~/types/utils";
|
||||||
import { validateResponse } from "~/utils/api";
|
import { validateResponse } from "~/utils/api";
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export const getHashList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getBookHT = (hashList: string[]) => {
|
export const getBookHT = (hashList: string[]) => {
|
||||||
const bookHT: Record<string, BookT> = {};
|
const bookHT: Record<string, IBook> = {};
|
||||||
|
|
||||||
hashList.forEach((hash) => {
|
hashList.forEach((hash) => {
|
||||||
try {
|
try {
|
||||||
@ -31,7 +31,7 @@ export const getBookHT = (hashList: string[]) => {
|
|||||||
return bookHT;
|
return bookHT;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveBook = (key: string, book: BookT) =>
|
export const saveBook = (key: string, book: IBook) =>
|
||||||
localStorage.setItem(key, JSON.stringify(book));
|
localStorage.setItem(key, JSON.stringify(book));
|
||||||
|
|
||||||
export const updateHashList = (hashList: string[]) =>
|
export const updateHashList = (hashList: string[]) =>
|
||||||
@ -78,37 +78,3 @@ export const savePages = (
|
|||||||
width: number,
|
width: number,
|
||||||
pages: number[]
|
pages: number[]
|
||||||
) => localStorage.setItem(hashStr(hash, height, width), JSON.stringify(pages));
|
) => 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));
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
export const connected = new Promise<void>((resolve) => {
|
|
||||||
if (navigator.serviceWorker.controller) return resolve();
|
|
||||||
navigator.serviceWorker.addEventListener("controllerchange", (e) =>
|
|
||||||
resolve()
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,18 +1,17 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"isolatedModules": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"lib": ["DOM", "webworker"],
|
"lib": ["DOM"],
|
||||||
"module": "ES2020",
|
"module": "ES2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"target": "es6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
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 ForkTsCheckerPlugin(),
|
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [{ from: "./public", to: "." }],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const webpack = require("webpack");
|
|
||||||
|
|
||||||
const webpackConfig = require("./webpack.config.common");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...webpackConfig,
|
|
||||||
mode: "development",
|
|
||||||
watchOptions: { ignored: /node_modules/ },
|
|
||||||
devServer: {
|
|
||||||
static: path.join(__dirname, "build"),
|
|
||||||
compress: true,
|
|
||||||
port: 8080,
|
|
||||||
hot: false,
|
|
||||||
historyApiFallback: {
|
|
||||||
index: "index.html",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
"process.env.NODE_ENV": JSON.stringify("development"),
|
|
||||||
"process.env.PUBLIC_API_URL": JSON.stringify("http://localhost:8081"),
|
|
||||||
"process.env.PUBLIC_BASE_URL": JSON.stringify("http://localhost:8080"),
|
|
||||||
}),
|
|
||||||
...webpackConfig.plugins,
|
|
||||||
],
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
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_API_URL": JSON.stringify(process.env.PUBLIC_API_URL),
|
|
||||||
"process.env.PUBLIC_BASE_URL": JSON.stringify(process.env.PUBLIC_BASE_URL),
|
|
||||||
}),
|
|
||||||
...webpackConfig.plugins,
|
|
||||||
],
|
|
||||||
};
|
|