First commit

This commit is contained in:
2021-04-15 15:08:54 -07:00
commit 2ccebf13b8
39 changed files with 26887 additions and 0 deletions

17
.babelrc Normal file
View 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
View 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
View File

@@ -0,0 +1,9 @@
.idea/
.DS_Store
comics/
dist/
server/
node_modules/
src/**/*.jsx
tests/__coverage__/
tests/**/*.jsx

3
.jshintrc Normal file
View File

@@ -0,0 +1,3 @@
{
"esversion": 9
}

4
.prettierrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
semi: true,
trailingComma: "all",
};

0
README.md Normal file
View File

13
nodemon.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

15
public/index.html Normal file
View 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
View 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.

View 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,
});
}
};

View 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

View 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;
}
}
}

View 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);

View 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;

View 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);

View 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;

View 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;

View 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;

View 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";

View 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: {},
},
};

File diff suppressed because one or more lines are too long

View 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
View 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,
);

View 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;

View 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),
});

View 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;
}

View 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" };
}

View 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,
};
}

View 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
View 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
View File

@@ -0,0 +1,8 @@
declare module "*.png" {
const value: string;
export = value;
}
declare module '*.jpg'
declare module '*.gif'
declare module '*.less'

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

12361
yarn.lock Normal file

File diff suppressed because it is too large Load Diff