First commit
This commit is contained in:
17
.babelrc
Normal file
17
.babelrc
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"react-hot-loader/babel"
|
||||
],
|
||||
"env": {
|
||||
"production": {
|
||||
"presets": [
|
||||
"minify"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
30
.eslintrc.js
Normal file
30
.eslintrc.js
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
|
||||
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors.
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
ecmaFeatures: {
|
||||
jsx: true, // Allows for the parsing of JSX
|
||||
},
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||
},
|
||||
},
|
||||
react: {
|
||||
version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||
},
|
||||
},
|
||||
// Fine tune rules
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
},
|
||||
};
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.idea/
|
||||
.DS_Store
|
||||
comics/
|
||||
dist/
|
||||
server/
|
||||
node_modules/
|
||||
src/**/*.jsx
|
||||
tests/__coverage__/
|
||||
tests/**/*.jsx
|
||||
4
.prettierrc.js
Normal file
4
.prettierrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
};
|
||||
13
nodemon.json
Normal file
13
nodemon.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ignore": [
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"node_modules",
|
||||
"src/client"
|
||||
],
|
||||
"watch": [
|
||||
"src/server"
|
||||
],
|
||||
"exec": "tsc -p tsconfig.server.json && node server/",
|
||||
"ext": "ts"
|
||||
}
|
||||
117
package.json
Normal file
117
package.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"name": "threetwo",
|
||||
"version": "0.0.2",
|
||||
"description": "ThreeTwo! A comic book curator and tagger.",
|
||||
"main": "server/index.js",
|
||||
"typings": "server/index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"start": "npm run build && npm run server",
|
||||
"client": "webpack serve --mode development --devtool inline-source-map --hot",
|
||||
"server": "tsc -p tsconfig.server.json && node server/",
|
||||
"dev": "concurrently \"nodemon\" \"npm run client\"",
|
||||
"server-dev": "nodemon"
|
||||
},
|
||||
"author": "Rishi Ghan",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"express": "^4.17.1",
|
||||
"mongoose": "^5.10.11",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.13.10",
|
||||
"@babel/core": "^7.13.10",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/preset-env": "^7.13.10",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@root/walk": "^1.1.0",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/mongoose": "^5.7.37",
|
||||
"@types/node": "^14.14.34",
|
||||
"@types/pino": "^6.3.7",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/unzipper": "^0.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"axios": "^0.21.1",
|
||||
"axios-rate-limit": "^1.3.0",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"buffer": "^6.0.3",
|
||||
"bulma": "^0.9.2",
|
||||
"clean-webpack-plugin": "^1.0.0",
|
||||
"comlink": "^4.3.0",
|
||||
"compromise": "^13.10.5",
|
||||
"compromise-dates": "^2.0.1",
|
||||
"compromise-numbers": "^1.2.0",
|
||||
"compromise-sentences": "^0.2.0",
|
||||
"concurrently": "^4.0.0",
|
||||
"connected-react-router": "^6.9.1",
|
||||
"css-loader": "^5.1.2",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"etl": "^0.6.12",
|
||||
"express": "^4.17.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"image-webpack-loader": "^7.0.1",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^26.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^1.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mongoose": "^5.10.11",
|
||||
"node-sass": "^5.0.0",
|
||||
"node-unrar-js": "^1.0.1",
|
||||
"nodemon": "^1.17.3",
|
||||
"npm": "^7.9.0",
|
||||
"pino": "^6.11.2",
|
||||
"pino-pretty": "^4.7.1",
|
||||
"prettier": "^2.2.1",
|
||||
"qs": "^6.10.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-redux": "^7.2.3",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass-loader": "^11.0.1",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"string-similarity": "^4.0.4",
|
||||
"style-loader": "^2.0.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.2.3",
|
||||
"unzipper": "^0.10.11",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "^5.33.2",
|
||||
"webpack-cli": "^4.6.0",
|
||||
"webpack-dev-server": "^3.11.2",
|
||||
"webpack-merge": "^5.7.3",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
15
public/index.html
Normal file
15
public/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Express, React, Typescript, Less </title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
159
src/client/README.md
Normal file
159
src/client/README.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Client side boilerplate with ReactJS library and Typescript
|
||||
|
||||
## Introduction
|
||||
|
||||
In the client side boilerplate, Typescript has been used to achieve a more structured and maintainable source code. ReactJS library which is one of the most important libraries for UI development alongside the other big names in the market, has been picked over to build the presentation layer of the application. Also for CSS, Less has been used to make CSS more functional.
|
||||
|
||||
### Less
|
||||
|
||||
[Less](http://lesscss.org/) is a backwards-compatible language extension for CSS. Less helps to write CSS in a functional way and It's really easy to read and understand.
|
||||
|
||||
### ESLint
|
||||
|
||||
[ESLint](https://eslint.org/) is a pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript and Typescript.
|
||||
|
||||
[.eslintrc.json file](<(https://eslint.org/docs/user-guide/configuring)>) (alternatively configurations can be written in Javascript or YAML as well) is used describe the configurations required for ESLint. Below is the .eslintrc.json file which has been used.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"extends": ["airbnb"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"comma-dangle": "off",
|
||||
"react/jsx-filename-extension": "off"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[Airbnb's Javascript Style Guide](https://github.com/airbnb/javascript) which has been used by the majority of JavaScript and Typescript developers worldwide. Since the aim is support for both client (browser) and server side (Node.js) source code, the **env** has been set to browser and node.
|
||||
Optionally, you can override the current settings by installing `eslint` globally and running `eslint --init` to change the configurations to suit your needs. [**no-console**](https://eslint.org/docs/rules/no-console), [**comma-dangle**](https://eslint.org/docs/rules/comma-dangle) and [**react/jsx-filename-extension**](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md) rules have been turned off.
|
||||
|
||||
### Webpack
|
||||
|
||||
[Webpack](https://webpack.js.org/) is a module bundler. Its main purpose is to capable Front-end developers to experience a modular programming style and bundle JavaScript and CSS files for usage in a browser.
|
||||
|
||||
[webpack.config.js](https://webpack.js.org/configuration/) file has been used to describe the configurations required for webpack. Below is the webpack.config.js file which has been used.
|
||||
|
||||
```javascript
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const outputDirectory = 'dist';
|
||||
|
||||
module.exports = {
|
||||
entry: ['babel-polyfill', './src/client/index.tsx'],
|
||||
output: {
|
||||
path: path.join(__dirname, outputDirectory),
|
||||
filename: './js/[name].bundle.js'
|
||||
},
|
||||
devtool: "source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use:[
|
||||
{
|
||||
loader: "awesome-typescript-loader"
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
enforce: "pre",
|
||||
test: /\.js$/,
|
||||
loader: "source-map-loader"
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
publicPath: './Less',
|
||||
hmr: process.env.NODE_ENV === 'development',
|
||||
},
|
||||
},
|
||||
{ loader: 'css-loader' },
|
||||
{
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
strictMath: true,
|
||||
noIeCompat: true,
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
|
||||
loader: 'url-loader?limit=100000'
|
||||
},
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['*', '.ts', '.tsx', '.js', '.jsx', '.json', '.less']
|
||||
},
|
||||
devServer: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8050'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([outputDirectory]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './public/index.html',
|
||||
favicon: './public/favicon.ico',
|
||||
title: "Book Manager",
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: './css/[name].css',
|
||||
chunkFilename: './css/[id].css',
|
||||
}),
|
||||
new CopyPlugin([
|
||||
{ from: './src/client/Assets', to: 'assets' },
|
||||
])
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
1. **entry:** entry: ./src/client/index.tsx is where the application starts executing and Webpack starts bundling.
|
||||
Note: babel-polyfill is added to support async/await. Read more [here](https://babeljs.io/docs/en/babel-polyfill#usage-in-node-browserify-webpack).
|
||||
2. **output path and filename:** the target directory and the filename for the bundled output.
|
||||
3. **module loaders:** Module loaders are transformations that are applied on the source code of a module. We pass all the js file through [babel-loader](https://github.com/babel/babel-loader) to transform JSX to Javascript. CSS files are passed through [css-loaders](https://github.com/webpack-contrib/css-loader) and [style-loaders](https://github.com/webpack-contrib/style-loader) to load and bundle CSS files. Fonts and images are loaded through url-loader.
|
||||
4. **Dev Server:** Configurations for the webpack-dev-server which will be described in coming section.
|
||||
5. **plugins:** [clean-webpack-plugin](https://github.com/johnagan/clean-webpack-plugin) is a webpack plugin to remove the build directory before building. [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) simplifies creation of HTML files to serve your webpack bundles. It loads the template (public/index.html) and injects the output bundle.
|
||||
|
||||
### Webpack dev server
|
||||
|
||||
[Webpack dev server](https://webpack.js.org/configuration/dev-server/) is used along with webpack. It provides a development server that enables live reloading for the client side code changes.
|
||||
|
||||
The devServer section of webpack.config.js contains the configuration required to run webpack-dev-server which is given below.
|
||||
|
||||
```javascript
|
||||
devServer: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8050"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[**Port**](https://webpack.js.org/configuration/dev-server/#devserver-port) specifies the Webpack dev server to listen on this particular port (3000 in this case). When [**open**](https://webpack.js.org/configuration/dev-server/#devserver-open) is set to true, it will automatically open the home page on start-up. [Proxying](https://webpack.js.org/configuration/dev-server/#devserver-proxy) URLs can be useful when you have a separate API backend development server, and you want to send API requests on the same domain.
|
||||
55
src/client/actions/comicinfo.actions.tsx
Normal file
55
src/client/actions/comicinfo.actions.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import axios from "axios";
|
||||
import rateLimiter from "axios-rate-limit";
|
||||
import qs from "qs";
|
||||
import {
|
||||
CV_SEARCH_SUCCESS,
|
||||
CV_API_CALL_IN_PROGRESS,
|
||||
CV_API_GENERIC_FAILURE,
|
||||
} from "../constants/action-types";
|
||||
import { COMICBOOKINFO_SERVICE_URI } from "../constants/endpoints";
|
||||
|
||||
const http = rateLimiter(axios.create(), {
|
||||
maxRequests: 1,
|
||||
perMilliseconds: 1000,
|
||||
maxRPS: 1,
|
||||
});
|
||||
|
||||
export const comicinfoAPICall = (options) => async (dispatch) => {
|
||||
try {
|
||||
dispatch({
|
||||
type: CV_API_CALL_IN_PROGRESS,
|
||||
inProgress: true,
|
||||
});
|
||||
const serviceURI = COMICBOOKINFO_SERVICE_URI + options.callURIAction;
|
||||
const response = await http(serviceURI, {
|
||||
method: options.callMethod,
|
||||
params: options.callParams,
|
||||
data: options.data ? options.data : null,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
paramsSerializer: (params) => {
|
||||
return qs.stringify(params, { arrayFormat: "repeat" });
|
||||
},
|
||||
});
|
||||
|
||||
switch (options.callURIAction) {
|
||||
case "search":
|
||||
dispatch({
|
||||
type: CV_SEARCH_SUCCESS,
|
||||
result: response.data,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Could not complete request.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
dispatch({
|
||||
type: CV_API_GENERIC_FAILURE,
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
7
src/client/assets/img/react_logo.svg
Normal file
7
src/client/assets/img/react_logo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
52
src/client/assets/scss/App.scss
Normal file
52
src/client/assets/scss/App.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@import "../../../../node_modules/bulma/bulma.sass";
|
||||
$fa-font-path : "~@fortawesome/fontawesome-free/webfonts";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
$bg-color: yellow;
|
||||
$border-color: red;
|
||||
|
||||
.app {
|
||||
font-family: helvetica, arial, sans-serif;
|
||||
padding: 2em;
|
||||
border: 5px solid $border-color;
|
||||
|
||||
p {
|
||||
background-color: $bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item.is-mega {
|
||||
position: static;
|
||||
|
||||
.is-mega-menu-title {
|
||||
margin-bottom: 0;
|
||||
padding: .375rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// comicvine search results
|
||||
.search-results-container {
|
||||
margin: 15px 0 0 0;
|
||||
border: 1px solid hsl(0, 0%, 98%);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: $box-shadow;
|
||||
|
||||
> :nth-of-type(odd) {
|
||||
background-color: hsl(0, 0%, 98%);
|
||||
}
|
||||
|
||||
.search-result {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px;
|
||||
.cover-image {
|
||||
border-radius: 5px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.search-result-details {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/client/components/App.tsx
Normal file
32
src/client/components/App.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
import { hot } from "react-hot-loader";
|
||||
import Dashboard from "./Dashboard";
|
||||
import Import from "./Import";
|
||||
|
||||
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
|
||||
import Navbar from "./Navbar";
|
||||
import "../assets/scss/App.scss";
|
||||
|
||||
class App extends React.Component<Record<string, unknown>, undefined> {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<Router>
|
||||
<Navbar />
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Dashboard />
|
||||
</Route>
|
||||
<Route path="/import">
|
||||
<Import />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare let module: Record<string, unknown>;
|
||||
|
||||
export default hot(module)(App);
|
||||
27
src/client/components/Dashboard.tsx
Normal file
27
src/client/components/Dashboard.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import ZeroState from "./ZeroState";
|
||||
|
||||
interface IProps {}
|
||||
interface IState {}
|
||||
|
||||
class Dashboard extends React.Component<IProps, IState> {
|
||||
public render() {
|
||||
return (
|
||||
<section className="section">
|
||||
<h1 className="title">Dashboard</h1>
|
||||
<h2 className="subtitle">
|
||||
A simple container to divide your page into <strong>sections</strong>,
|
||||
like the one you're currently reading.
|
||||
</h2>
|
||||
<ZeroState
|
||||
header={"Set the source directory"}
|
||||
message={
|
||||
"No comics were found! Please point ThreeTwo! to a directory..."
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
186
src/client/components/Import.tsx
Normal file
186
src/client/components/Import.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as React from "react";
|
||||
import { hot } from "react-hot-loader";
|
||||
import _ from "lodash";
|
||||
import axios from "axios";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import { comicinfoAPICall } from "../actions/comicinfo.actions";
|
||||
import MatchResult from "./MatchResult";
|
||||
import {
|
||||
IFolderData,
|
||||
IComicVineSearchMatch,
|
||||
} from "../shared/interfaces/comicinfo.interfaces";
|
||||
import { folderWalk } from "../shared/utils/folder.utils";
|
||||
// import {} from "../constants/comicvine.mock";
|
||||
import * as Comlink from "comlink";
|
||||
import WorkerAdd from "../workers/extractCovers.worker";
|
||||
interface IProps {
|
||||
matches: unknown;
|
||||
}
|
||||
interface IState {
|
||||
folderWalkResults?: Array<IFolderData>;
|
||||
searchPaneIndex: number;
|
||||
}
|
||||
|
||||
class Import extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
folderWalkResults: undefined,
|
||||
searchPaneIndex: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public toggleSearchResultsPane(paneId: number): void {
|
||||
this.setState({
|
||||
searchPaneIndex: paneId,
|
||||
});
|
||||
}
|
||||
|
||||
public async init() {
|
||||
// WebWorkers use `postMessage` and therefore work with Comlink.
|
||||
const { add } = Comlink.wrap(new WorkerAdd());
|
||||
const res = await add(1, 3);
|
||||
console.log(res);
|
||||
}
|
||||
|
||||
public async startFolderWalk(): Promise<void> {
|
||||
this.init();
|
||||
const folderWalkResults = await folderWalk();
|
||||
this.setState({
|
||||
folderWalkResults,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<section className="section is-small">
|
||||
<h1 className="title">Import</h1>
|
||||
|
||||
<article className="message is-dark">
|
||||
<div className="message-body">
|
||||
<p className="mb-2">
|
||||
<span className="tag is-medium is-info is-light">
|
||||
Import Only
|
||||
</span>{" "}
|
||||
will add comics identified from the mapped folder into the local
|
||||
db.
|
||||
</p>
|
||||
<p>
|
||||
<span className="tag is-medium is-info is-light">
|
||||
Import and Tag
|
||||
</span>{" "}
|
||||
will scan the ComicVine, shortboxed APIs and import comics from
|
||||
the mapped folder with the additional metadata.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
<p className="buttons">
|
||||
<button
|
||||
className="button is-medium"
|
||||
onClick={() => this.startFolderWalk()}
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-file-import"></i>
|
||||
</span>
|
||||
<span>Import Only</span>
|
||||
</button>
|
||||
|
||||
<button className="button is-medium">
|
||||
<span className="icon">
|
||||
<i className="fas fa-tag"></i>
|
||||
</span>
|
||||
<span>Import and Tag</span>
|
||||
</button>
|
||||
</p>
|
||||
{/* Folder walk results */}
|
||||
{!_.isUndefined(this.state.folderWalkResults) ? (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Format</th>
|
||||
<th>Is File</th>
|
||||
</tr>
|
||||
{!_.isUndefined(this.state.folderWalkResults) &&
|
||||
this.state.folderWalkResults.map((result, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>
|
||||
{!result.isLink && !result.isFile ? (
|
||||
<span className="icon-text">
|
||||
<span className="icon">
|
||||
<i className="fas fa-folder"></i>
|
||||
</span>
|
||||
<span>{result.name}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="ml-5">{result.name}</span>
|
||||
)}
|
||||
|
||||
{this.state.searchPaneIndex === idx &&
|
||||
!_.isUndefined(this.props.matches) ? (
|
||||
<MatchResult
|
||||
queryData={result}
|
||||
matchData={this.props.matches}
|
||||
visible={true}
|
||||
/>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
{!_.isEmpty(result.extension) ? (
|
||||
<span className="tag is-info">
|
||||
{result.extension}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td>{result.isFile.toString()}</td>
|
||||
<td>
|
||||
<button
|
||||
key={idx}
|
||||
className="button is-small is-primary is-outlined"
|
||||
onClick={(e) => {
|
||||
this.props.findMatches(e, result);
|
||||
this.toggleSearchResultsPane(idx);
|
||||
}}
|
||||
>
|
||||
Find Match
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
</table>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
matches: state.comicInfo.searchResults,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
findMatches(e, results) {
|
||||
dispatch(
|
||||
comicinfoAPICall({
|
||||
callURIAction: "search",
|
||||
callMethod: "get",
|
||||
callParams: {
|
||||
format: "json",
|
||||
query: results.name,
|
||||
limit: 10,
|
||||
offset: 5,
|
||||
sort: "name:asc",
|
||||
resources: "issue",
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Import);
|
||||
57
src/client/components/MatchResult.tsx
Normal file
57
src/client/components/MatchResult.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
IComicVineSearchMatch,
|
||||
IFolderData,
|
||||
} from "../shared/interfaces/comicinfo.interfaces";
|
||||
import _ from "lodash";
|
||||
import { autoMatcher } from "../shared/utils/query.transformer";
|
||||
|
||||
interface IProps {
|
||||
matchData: unknown;
|
||||
visible: boolean;
|
||||
queryData: IFolderData;
|
||||
}
|
||||
|
||||
interface IState {}
|
||||
|
||||
class MatchResult extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
console.log(this.props);
|
||||
autoMatcher(this.props.queryData, this.props.matchData.results);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.visible ? (
|
||||
<div>
|
||||
<h3>Matches</h3>
|
||||
<div className="search-results-container">
|
||||
{this.props.matchData.results.map((result, idx) => {
|
||||
return (
|
||||
<div key={idx} className="search-result">
|
||||
<img className="cover-image" src={result.image.thumb_url} />
|
||||
<div className="search-result-details">
|
||||
<h5>{result.volume.name}</h5>
|
||||
|
||||
{!_.isEmpty(result.extension) ? (
|
||||
<span className="tag is-info">{result.extension}</span>
|
||||
) : null}
|
||||
|
||||
<span className="tag is-info">
|
||||
Issue Number: {result.issue_number}
|
||||
</span>
|
||||
<p>{result.site_detail_url}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
export default MatchResult;
|
||||
212
src/client/components/Navbar.tsx
Normal file
212
src/client/components/Navbar.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Navbar: React.FunctionComponent = (props) => {
|
||||
return (
|
||||
<nav className="navbar ">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="http://bulma.io">
|
||||
<img
|
||||
src="http://bulma.io/images/bulma-logo.png"
|
||||
alt="Bulma: a modern CSS framework based on Flexbox"
|
||||
width="112"
|
||||
height="28"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
className="navbar-item is-hidden-desktop"
|
||||
href="https://github.com/jgthms/bulma"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fa fa-github"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
className="navbar-item is-hidden-desktop"
|
||||
href="https://twitter.com/jgthms"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fa fa-twitter"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className="navbar-burger burger" data-target="navMenubd-example">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navMenubd-example" className="navbar-menu">
|
||||
<div className="navbar-start">
|
||||
<Link to="/" className="navbar-item">
|
||||
Dashboard
|
||||
</Link>
|
||||
|
||||
<Link to="/import" className="navbar-item">
|
||||
Import
|
||||
</Link>
|
||||
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
<a
|
||||
className="navbar-link is-active"
|
||||
href="/documentation/overview/start/"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<div className="navbar-dropdown ">
|
||||
<a className="navbar-item " href="/documentation/overview/start/">
|
||||
Overview
|
||||
</a>
|
||||
|
||||
<a
|
||||
className="navbar-item is-active"
|
||||
href="http://bulma.io/documentation/components/breadcrumb/"
|
||||
>
|
||||
Components
|
||||
</a>
|
||||
|
||||
<hr className="navbar-divider" />
|
||||
<div className="navbar-item">
|
||||
<div>
|
||||
<p className="is-size-6-desktop">
|
||||
<strong className="has-text-info">0.5.1</strong>
|
||||
</p>
|
||||
|
||||
<small>
|
||||
<a className="bd-view-all-versions" href="/versions">
|
||||
View all versions
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-item has-dropdown is-hoverable is-mega">
|
||||
<div className="navbar-link flex">
|
||||
Blog
|
||||
</div>
|
||||
<div id="blogDropdown" className="navbar-dropdown">
|
||||
<div className="container is-fluid">
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
|
||||
<a
|
||||
className="navbar-item "
|
||||
href="http://bulma.io/documentation/columns/basics/"
|
||||
>
|
||||
Columns
|
||||
</a>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
<a
|
||||
className="navbar-item "
|
||||
href="/documentation/overview/start/"
|
||||
>
|
||||
Overview
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="navbar-divider" />
|
||||
<div className="navbar-item">
|
||||
<div className="navbar-content">
|
||||
<div className="level is-mobile">
|
||||
<div className="level-left">
|
||||
<div className="level-item">
|
||||
<strong>Stay up to date!</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className="level-item">
|
||||
<a
|
||||
className="button bd-is-rss is-small"
|
||||
href="http://bulma.io/atom.xml"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa fa-rss"></i>
|
||||
</span>
|
||||
<span>Subscribe</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="navbar-end">
|
||||
<a
|
||||
className="navbar-item is-hidden-desktop-only"
|
||||
href="https://github.com/jgthms/bulma"
|
||||
target="_blank"
|
||||
></a>
|
||||
<div className="navbar-item">
|
||||
<div className="field is-grouped">
|
||||
<p className="control"></p>
|
||||
<p className="control">Settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
18
src/client/components/ZeroState.tsx
Normal file
18
src/client/components/ZeroState.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
|
||||
interface ZeroStateProps {
|
||||
header: string;
|
||||
message: string;
|
||||
}
|
||||
const ZeroState: React.FunctionComponent<ZeroStateProps> = (props) => {
|
||||
return (
|
||||
<article className="message is-info">
|
||||
<div className="message-body">
|
||||
<p>{ props.header }</p>
|
||||
{ props.message }
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZeroState;
|
||||
5
src/client/constants/action-types.js
Normal file
5
src/client/constants/action-types.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const CV_API_CALL_IN_PROGRESS = "CV_SEARCH_IN_PROGRESS";
|
||||
export const CV_SEARCH_FAILURE = "CV_SEARCH_FAILURE";
|
||||
export const CV_SEARCH_SUCCESS = "CV_SEARCH_SUCCESS";
|
||||
|
||||
export const CV_API_GENERIC_FAILURE = "CV_API_GENERIC_FAILURE";
|
||||
48
src/client/constants/comic.model.js
Normal file
48
src/client/constants/comic.model.js
Normal file
@@ -0,0 +1,48 @@
|
||||
export const comicModel = {
|
||||
name: "",
|
||||
type: "",
|
||||
import: {
|
||||
isImported: false,
|
||||
},
|
||||
userAddedMetadata: {
|
||||
tags: [],
|
||||
},
|
||||
|
||||
comicStructure: {
|
||||
cover: {
|
||||
thumb: "http://thumb",
|
||||
medium: "http://medium",
|
||||
large: "http://large",
|
||||
},
|
||||
collection: {
|
||||
publishDate: "",
|
||||
type: "", // issue, series, trade paperback
|
||||
metadata: {
|
||||
publisher: "",
|
||||
issueNumber: "",
|
||||
description: "",
|
||||
synopsis: "",
|
||||
team: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcedMetadata: {
|
||||
comicvine: {},
|
||||
shortboxed: {},
|
||||
gcd: {},
|
||||
},
|
||||
rawFileDetails: {
|
||||
fileName: "",
|
||||
path: "",
|
||||
extension: "",
|
||||
},
|
||||
acquisition: {
|
||||
release: {},
|
||||
torrent: {
|
||||
magnet: "",
|
||||
tracker: "",
|
||||
status: "",
|
||||
},
|
||||
usenet: {},
|
||||
},
|
||||
};
|
||||
619
src/client/constants/comicvine.mock.js
Normal file
619
src/client/constants/comicvine.mock.js
Normal file
File diff suppressed because one or more lines are too long
3
src/client/constants/endpoints.js
Normal file
3
src/client/constants/endpoints.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const COMICBOOKINFO_SERVICE_URI =
|
||||
"http://localhost:6050/api/comicbookinfo/";
|
||||
export const FOLDERUTIL_URI = "http://localhost:3000/walkfolder";
|
||||
15
src/client/index.tsx
Normal file
15
src/client/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import configureStore from "./store/index";
|
||||
import App from "./components/App";
|
||||
|
||||
const store = configureStore({});
|
||||
const rootEl = document.getElementById("root");
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<App history={history} />
|
||||
</Provider>,
|
||||
rootEl,
|
||||
);
|
||||
25
src/client/reducers/comicinfo.reducer.js
Normal file
25
src/client/reducers/comicinfo.reducer.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { CV_API_CALL_IN_PROGRESS, CV_SEARCH_SUCCESS } from "../constants/action-types";
|
||||
const initialState = {
|
||||
showResultsPane: false,
|
||||
};
|
||||
|
||||
function comicinfoReducer(state = initialState, action){
|
||||
switch (action.type) {
|
||||
case CV_API_CALL_IN_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
result: {},
|
||||
showResultsPane: false,
|
||||
};
|
||||
case CV_SEARCH_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
searchResults: action.result,
|
||||
showResultsPane: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default comicinfoReducer;
|
||||
9
src/client/reducers/index.js
Normal file
9
src/client/reducers/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { combineReducers } from "redux";
|
||||
import { connectRouter } from "connected-react-router";
|
||||
import comicinfoReducer from "../reducers/comicinfo.reducer";
|
||||
|
||||
export default (history) =>
|
||||
combineReducers({
|
||||
comicInfo: comicinfoReducer,
|
||||
router: connectRouter(history),
|
||||
});
|
||||
16
src/client/shared/interfaces/comicinfo.interfaces.ts
Normal file
16
src/client/shared/interfaces/comicinfo.interfaces.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface IFolderResponse {
|
||||
data: Array<IFolderData>;
|
||||
}
|
||||
|
||||
export interface IComicVineSearchMatch {
|
||||
description: string;
|
||||
id: number;
|
||||
volumes: string;
|
||||
}
|
||||
export interface IFolderData {
|
||||
name: string;
|
||||
extension: string;
|
||||
containedIn: string;
|
||||
isFile: boolean;
|
||||
isLink: boolean;
|
||||
}
|
||||
19
src/client/shared/utils/folder.utils.ts
Normal file
19
src/client/shared/utils/folder.utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import axios from "axios";
|
||||
import { IFolderData } from "../interfaces/comicinfo.interfaces";
|
||||
import { FOLDERUTIL_URI } from "../../constants/endpoints";
|
||||
|
||||
export async function folderWalk(): Promise<Array<IFolderData>> {
|
||||
return axios
|
||||
.request<Array<IFolderData>>({
|
||||
url: FOLDERUTIL_URI,
|
||||
transformResponse: (r: string) => JSON.parse(r),
|
||||
})
|
||||
.then((response) => {
|
||||
const { data } = response;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
export async function foo() {
|
||||
return { as: "af" };
|
||||
}
|
||||
57
src/client/shared/utils/nlp.utils.ts
Normal file
57
src/client/shared/utils/nlp.utils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { default as nlp } from "compromise";
|
||||
import { default as dates } from "compromise-dates";
|
||||
import { default as sentences } from "compromise-sentences";
|
||||
import { default as numbers } from "compromise-numbers";
|
||||
import _ from "lodash";
|
||||
|
||||
nlp.extend(sentences);
|
||||
nlp.extend(numbers);
|
||||
nlp.extend(dates);
|
||||
|
||||
export function tokenize(inputString) {
|
||||
const doc = nlp(inputString);
|
||||
const sentence = doc.sentences().json();
|
||||
const number = doc.numbers().fractions();
|
||||
const chapters = inputString.match(/ch(a?p?t?e?r?)(\W?)(\_?)(\#?)(\d)/gi);
|
||||
const volumes = inputString.match(/v(o?l?u?m?e?)(\W?)(\_?)(\s?)(\d+)/gi);
|
||||
const issues = inputString.match(/issue(\W?)(\_?)(\d+)/gi);
|
||||
const issueHashes = inputString.match(/\#\d/gi);
|
||||
const yearMatches = inputString.match(/\d{4}/g);
|
||||
|
||||
const sentenceToProcess = sentence[0].normal.replace(/_/g, " ");
|
||||
const normalizedSentence = nlp(sentenceToProcess)
|
||||
.text("normal")
|
||||
.trim()
|
||||
.split(" ");
|
||||
|
||||
const queryObject = {
|
||||
comicbook_identifiers: {
|
||||
issues,
|
||||
issueHashes,
|
||||
chapters,
|
||||
volumes,
|
||||
issueRanges: number,
|
||||
},
|
||||
years: {
|
||||
yearMatches,
|
||||
},
|
||||
sentences: {
|
||||
detailed: sentence,
|
||||
normalized: normalizedSentence,
|
||||
},
|
||||
};
|
||||
return queryObject;
|
||||
}
|
||||
|
||||
export function refineQuery(queryString) {
|
||||
let queryObj = tokenize(queryString);
|
||||
let removedYears = _.xor(
|
||||
queryObj.sentences.normalized,
|
||||
queryObj.years.yearMatches,
|
||||
);
|
||||
return {
|
||||
tokenized: removedYears,
|
||||
normalized: removedYears.join(" "),
|
||||
meta: queryObj,
|
||||
};
|
||||
}
|
||||
35
src/client/shared/utils/query.transformer.ts
Normal file
35
src/client/shared/utils/query.transformer.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Rishi Ghan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
import _ from "lodash";
|
||||
import { IFolderData } from "../interfaces/comicinfo.interfaces";
|
||||
import stringSimilarity from "string-similarity";
|
||||
import { logger } from "../utils/log.utils";
|
||||
|
||||
|
||||
export const autoMatcher = (query, matches) => {
|
||||
|
||||
}
|
||||
18
src/client/store/index.js
Normal file
18
src/client/store/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { routerMiddleware } from "connected-react-router";
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
import { createBrowserHistory } from "history";
|
||||
import thunk from "redux-thunk";
|
||||
import createRootReducer from "../reducers";
|
||||
|
||||
export const history = createBrowserHistory();
|
||||
export default function configureStore(initialState) {
|
||||
const store = createStore(
|
||||
createRootReducer(history),
|
||||
initialState,
|
||||
compose(
|
||||
applyMiddleware(thunk, routerMiddleware(history)),
|
||||
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
|
||||
),
|
||||
);
|
||||
return store;
|
||||
}
|
||||
8
src/client/types/index.d.ts
vendored
Normal file
8
src/client/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module '*.jpg'
|
||||
declare module '*.gif'
|
||||
declare module '*.less'
|
||||
20
src/client/workers/extractCovers.worker.ts
Normal file
20
src/client/workers/extractCovers.worker.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as Comlink from "comlink";
|
||||
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
function importComicBooks() {
|
||||
// 1. Walk the folder structure
|
||||
// 2. Scan for .cbz, .cbr
|
||||
// 3. extract cover image
|
||||
// 4. Calculate image hash
|
||||
// 5. Get metadata, add to data model
|
||||
// 5. Save cover to disk
|
||||
// 6. Save model to mongo
|
||||
}
|
||||
|
||||
Comlink.expose({
|
||||
add,
|
||||
});
|
||||
|
||||
export default null as any;
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2019",
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist/",
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"DOM"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"eslint.workingDirectories": [
|
||||
{"directory": "./node_modules", "changeProcessCWD": true }
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"./src/server "
|
||||
],
|
||||
"include": [
|
||||
"./src/client/*",
|
||||
"./src/client/types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
25
tsconfig.server.json
Normal file
25
tsconfig.server.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Node 12",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "./server",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"exclude": [
|
||||
"./src/client"
|
||||
],
|
||||
"include": [
|
||||
"./src/server"
|
||||
]
|
||||
}
|
||||
75
webpack.config.js
Normal file
75
webpack.config.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const outputDirectory = "dist";
|
||||
|
||||
module.exports = {
|
||||
entry: ["babel-polyfill", "./src/client/index.tsx"],
|
||||
output: {
|
||||
path: path.join(__dirname, outputDirectory),
|
||||
filename: "./js/[name].bundle.js",
|
||||
},
|
||||
devtool: "source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.ts$/,
|
||||
use: { loader: "worker-loader" },
|
||||
},
|
||||
{
|
||||
test: [/\.js?$/, /\.jsx?$/, /\.tsx?$/],
|
||||
use: ["babel-loader"],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
|
||||
{
|
||||
enforce: "pre",
|
||||
test: /\.js$/,
|
||||
loader: "source-map-loader",
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(scss|sass)$/,
|
||||
use: ["style-loader", "css-loader", "sass-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/i,
|
||||
use: [
|
||||
"file-loader?hash=sha512&digest=hex&name=img/[contenthash].[ext]",
|
||||
"image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ["*", ".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||
},
|
||||
devServer: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
hot: true,
|
||||
proxy: {
|
||||
"/api/**": {
|
||||
target: "http://localhost:8050",
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// new CleanWebpackPlugin([outputDirectory]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./public/index.html",
|
||||
favicon: "./public/favicon.ico",
|
||||
title: "express-typescript-react",
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "./css/[name].css",
|
||||
chunkFilename: "./css/[id].css",
|
||||
}),
|
||||
],
|
||||
};
|
||||
12488
yarn-error.log
Normal file
12488
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user