Compare commits
10 Commits
9049e1d6b1
...
b2eae74b9e
| Author | SHA1 | Date | |
|---|---|---|---|
| b2eae74b9e | |||
| 167a608b93 | |||
| 86f632d72b | |||
| e1abcdb379 | |||
| 81bf9dc585 | |||
| 2e4f90d8ff | |||
| 32b3c643ef | |||
| cc0b5efe9f | |||
| d986f7801b | |||
| 546d3d760e |
7118
api/package-lock.json
generated
7118
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,43 @@
|
||||
{
|
||||
"name": "maanteeamet-fetch",
|
||||
"name": "car-fetcher-api",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "babel-node src/index.js",
|
||||
"start": "cross-env NODE_ENV=production babel-node src/index.js",
|
||||
"dev": "nodemon -r dotenv/config --watch src --exec babel-node src/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.3.0",
|
||||
"npm": "6.14.5"
|
||||
"node": "^14",
|
||||
"npm": "^6.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.10.4",
|
||||
"@babel/core": "7.10.4",
|
||||
"@babel/node": "7.10.4",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.4",
|
||||
"@babel/preset-env": "7.10.4",
|
||||
"body-parser": "1.19.0",
|
||||
"cacheman": "2.2.1",
|
||||
"cacheman-file": "0.2.1",
|
||||
"compression": "1.7.4",
|
||||
"cross-env": "7.0.2",
|
||||
"dotenv": "8.2.0",
|
||||
"express": "4.17.1",
|
||||
"form-data": "3.0.0",
|
||||
"helmet": "^3.22.0",
|
||||
"helmet": "3.23.3",
|
||||
"jsdom": "16.2.2",
|
||||
"multer": "1.4.2",
|
||||
"node-fetch": "2.6.0",
|
||||
"puppeteer": "3.1.0"
|
||||
"puppeteer": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.9.6",
|
||||
"@babel/node": "7.8.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.8.3",
|
||||
"@babel/preset-env": "7.9.6",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "18.1.0",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"eslint-plugin-react-hooks": "2.5.0",
|
||||
"eslint": "7.4.0",
|
||||
"eslint-config-airbnb": "18.2.0",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "6.3.1",
|
||||
"eslint-plugin-react": "7.20.3",
|
||||
"eslint-plugin-react-hooks": "4.0.6",
|
||||
"nodemon": "2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,17 @@ import {
|
||||
TEMP_DIR,
|
||||
SITE_COOKIES,
|
||||
} from '../util/Constants';
|
||||
import Selectors from '../util/Selectors';
|
||||
import Selectors, { SUBMIT_FORM_ID } from '../util/Selectors';
|
||||
import { createCacheDirectories } from '../util/TempDirCreator';
|
||||
|
||||
const getPuppeteerOptions = () => {
|
||||
const options = {};
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
options.args = ['--no-sandbox'];
|
||||
}
|
||||
return options;
|
||||
};
|
||||
console.log(Selectors);
|
||||
class CookieMonster {
|
||||
cache;
|
||||
browser;
|
||||
@@ -14,16 +23,21 @@ class CookieMonster {
|
||||
|
||||
constructor(cache) {
|
||||
this.cache = cache;
|
||||
createCacheDirectories(TEMP_DIR.screenshots);
|
||||
}
|
||||
|
||||
async submitForm(plate) {
|
||||
await this.page.focus(Selectors.form.plate);
|
||||
await this.page.keyboard.type(plate);
|
||||
const path = {
|
||||
s: `j_idt${SUBMIT_FORM_ID}:j_idt137`,
|
||||
u: `j_idt${SUBMIT_FORM_ID}`,
|
||||
};
|
||||
await this.page.evaluate(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
PrimeFaces.ab({
|
||||
s: 'j_idt104:j_idt131',
|
||||
u: 'j_idt104',
|
||||
s: 'j_idt110:j_idt137',
|
||||
u: 'j_idt110',
|
||||
});
|
||||
});
|
||||
await this.page.waitForNavigation({
|
||||
@@ -33,7 +47,7 @@ class CookieMonster {
|
||||
}
|
||||
|
||||
async launchPage() {
|
||||
this.browser = await puppeteer.launch();
|
||||
this.browser = await puppeteer.launch(getPuppeteerOptions());
|
||||
this.page = await this.browser.newPage();
|
||||
await this.page.goto(SEARCH_URL);
|
||||
await this.page.setCookie(...SITE_COOKIES);
|
||||
@@ -48,14 +62,23 @@ class CookieMonster {
|
||||
}
|
||||
|
||||
async init(plate) {
|
||||
console.log(`Fetching data for ${plate}`);
|
||||
await this.launchPage();
|
||||
await this.submitForm(plate);
|
||||
const pageContent = await this.page
|
||||
.$eval(Selectors.container.main, (element) => element.innerHTML);
|
||||
await this.cleanup(plate);
|
||||
console.log(`Successfully fetched fresh data for ${plate}`);
|
||||
return pageContent;
|
||||
try {
|
||||
console.log(`Fetching data for ${plate}`);
|
||||
await this.launchPage();
|
||||
try {
|
||||
await this.submitForm(plate);
|
||||
} catch (e) {
|
||||
console.warn(`Got a timeout for ${plate} but data might still be there, sometimes takes longer to load entire page.`, e.message);
|
||||
}
|
||||
const pageContent = await this.page
|
||||
.$eval(Selectors.container.main, (element) => element.innerHTML);
|
||||
await this.cleanup(plate);
|
||||
console.log(`Successfully fetched fresh data for ${plate}`);
|
||||
return pageContent;
|
||||
} catch (e) {
|
||||
await this.cleanup(plate);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ class PlateRecognizr {
|
||||
|
||||
fetch(image) {
|
||||
const body = new FormData();
|
||||
body.append('upload', fs.createReadStream('test.jpg'));
|
||||
body.append('regions', 'gb'); // Change to your country
|
||||
body.append('upload', image);
|
||||
body.append('regions', 'ee'); // Change to your country
|
||||
return fetch(ALPR_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -28,6 +28,7 @@ class Hack {
|
||||
async init(plate) {
|
||||
try {
|
||||
const data = await this.getData(plate);
|
||||
console.log(data);
|
||||
this.scraper.setContent(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -9,7 +9,7 @@ class Scraper {
|
||||
setContent(text) {
|
||||
const parsedContent = new JSDOM(text).window.document;
|
||||
if (parsedContent.querySelector(Selectors.properties.main.container) === null) {
|
||||
throw Error('No data was received.. Something went wrong.');
|
||||
throw Error('No data or invalid data was received.. Something went wrong.');
|
||||
}
|
||||
this.document = parsedContent;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class Main {
|
||||
|
||||
init() {
|
||||
this.api.init();
|
||||
this.alpr.handle();
|
||||
// this.alpr.handle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,34 @@ import express from 'express';
|
||||
import http from 'http';
|
||||
import compression from 'compression';
|
||||
import helmet from 'helmet';
|
||||
import multer from 'multer';
|
||||
import bodyParser from 'body-parser';
|
||||
import Hack from '../components/Hack';
|
||||
import PlateRecognizr from '../api/PlateRecognizr';
|
||||
|
||||
const PORT = process.env.PORT || "8000";
|
||||
const PORT = process.env.PORT || '8000';
|
||||
const upload = multer({ dest: '/tmp' });
|
||||
|
||||
class BasicApi {
|
||||
app;
|
||||
server;
|
||||
hack;
|
||||
alpr;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.app.use(express.static('public'));
|
||||
this.app.use(bodyParser.urlencoded({ extended: false }));
|
||||
this.app.use(compression());
|
||||
this.app.use(helmet());
|
||||
this.app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||
next();
|
||||
});
|
||||
this.server = http.createServer(this.app);
|
||||
this.hack = new Hack();
|
||||
this.alpr = new PlateRecognizr();
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -34,6 +47,14 @@ class BasicApi {
|
||||
.status(200)
|
||||
.send(this.hack.getCar());
|
||||
});
|
||||
|
||||
this.app.post('/alpr', upload.single('upload'), async ({ body }, response) => {
|
||||
const { upload: image } = body;
|
||||
console.log(Object.keys(body));
|
||||
const plate = await (await this.alpr.fetch(image)).json();
|
||||
console.log(plate);
|
||||
response.status(200).json(plate);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Cacheman from 'cacheman';
|
||||
import { CACHE } from './Constants';
|
||||
import { createCacheDirectories } from './TempDirCreator';
|
||||
|
||||
const formatKey = (name) => {
|
||||
if (!name) {
|
||||
@@ -12,6 +13,7 @@ class Cache {
|
||||
manager;
|
||||
|
||||
constructor() {
|
||||
createCacheDirectories(CACHE.directory);
|
||||
this.manager = new Cacheman({
|
||||
ttl: CACHE.ttl,
|
||||
engine: CACHE.engine,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// seems that this changes sometimes
|
||||
export const SUBMIT_FORM_ID = '110';
|
||||
|
||||
export default {
|
||||
form: {
|
||||
plate: '#j_idt104\\:regMark',
|
||||
plate: `#j_idt${SUBMIT_FORM_ID}\\:regMark`,
|
||||
},
|
||||
container: {
|
||||
main: '#content',
|
||||
form: '#j_idt104',
|
||||
form: `#j_idt${SUBMIT_FORM_ID}`,
|
||||
},
|
||||
properties: {
|
||||
plate: '.content-title h1',
|
||||
|
||||
15
api/src/util/TempDirCreator.js
Normal file
15
api/src/util/TempDirCreator.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import fs from 'fs';
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export const createCacheDirectories = async (targetDir) => {
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
console.log(`Directory ${targetDir} does not exist. Creating it.`);
|
||||
fs.mkdirSync(targetDir, {
|
||||
recursive: true,
|
||||
});
|
||||
} else {
|
||||
console.log(`Directory ${targetDir} already exists.`);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
5283
api/yarn.lock
Normal file
5283
api/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
const FtpDeploy = require("ftp-deploy");
|
||||
const ftpDeploy = new FtpDeploy();
|
||||
|
||||
const ftp = new FtpDeploy();
|
||||
|
||||
const config = {
|
||||
user: process.env.FTP_USER,
|
||||
@@ -8,9 +9,7 @@ const config = {
|
||||
port: process.env.FTP_PORT,
|
||||
localRoot: __dirname + "/../frontend",
|
||||
remoteRoot: "/public_html/car/",
|
||||
// include: ["*", "**/*"], // this would upload everything except dot files
|
||||
include: ["build/**"],
|
||||
// e.g. exclude sourcemaps, and ALL files in node_modules (including dot files)
|
||||
exclude: ["build/**/*.map", "node_modules/**", "node_modules/**/.*", ".git/**"],
|
||||
// delete ALL existing files at destination before uploading, if true
|
||||
deleteRemote: false,
|
||||
@@ -18,9 +17,9 @@ const config = {
|
||||
forcePasv: true
|
||||
};
|
||||
|
||||
ftpDeploy
|
||||
ftp
|
||||
.deploy(config)
|
||||
.then(res => {
|
||||
.then(() => {
|
||||
console.log("Deployment to FTP finished");
|
||||
return;
|
||||
})
|
||||
|
||||
@@ -53,6 +53,7 @@ module.exports = (api) => {
|
||||
'@context': './src/context',
|
||||
'@components': './src/components',
|
||||
'@hook': './src/hook',
|
||||
'@actions': './src/actions',
|
||||
'@assets': './assets',
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -125,5 +125,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-console': 0,
|
||||
},
|
||||
};
|
||||
|
||||
BIN
frontend/assets/images/Maanteeamet.png
Normal file
BIN
frontend/assets/images/Maanteeamet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
14628
frontend/package-lock.json
generated
14628
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,19 @@
|
||||
{
|
||||
"name": "react-base",
|
||||
"name": "car-fetcher-frontend",
|
||||
"version": "0.0.1",
|
||||
"description": "Base React template",
|
||||
"description": "Frontend for car fetcher",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "nodemon --exec \"cross-env NODE_ENV=development webpack-dev-server\"",
|
||||
"start": "nodemon --exec \"cross-env NODE_ENV=development webpack-dev-server -r dotenv/config \"",
|
||||
"prebundle": "rimraf build/*",
|
||||
"bundle": "cross-env NODE_ENV=production webpack",
|
||||
"cacheclean": "rimraf node_modules/.cache/hard-source",
|
||||
"precommit": "lint-staged",
|
||||
"lint": "eslint --fix src --ext .jsx"
|
||||
},
|
||||
"author": "k4rli",
|
||||
"contributors": [],
|
||||
"keywords": [
|
||||
"react",
|
||||
"reactjs",
|
||||
"react-boilerplate"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=11",
|
||||
"npm": ">=6"
|
||||
"node": "^14",
|
||||
"npm": "^6.14"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,css,md}": [
|
||||
@@ -30,66 +22,70 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.3.6",
|
||||
"@fortawesome/fontawesome-free": "5.13.1",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@reduxjs/toolkit": "1.4.0",
|
||||
"core-js": "3.6.5",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-hot-loader": "4.12.21",
|
||||
"react-webcam": "5.0.2",
|
||||
"react-webcam": "5.2.0",
|
||||
"regenerator-runtime": "0.13.5",
|
||||
"use-dark-mode": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "7.8.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.8.3",
|
||||
"@babel/plugin-proposal-function-sent": "7.8.3",
|
||||
"@babel/plugin-proposal-json-strings": "7.8.3",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.8.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.9.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.8.3",
|
||||
"@babel/core": "7.10.4",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "7.10.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.10.4",
|
||||
"@babel/plugin-proposal-function-sent": "7.10.4",
|
||||
"@babel/plugin-proposal-json-strings": "7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.10.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.10.4",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "7.8.3",
|
||||
"@babel/plugin-transform-runtime": "7.9.6",
|
||||
"@babel/preset-env": "7.9.6",
|
||||
"@babel/preset-react": "7.9.4",
|
||||
"@babel/register": "7.9.0",
|
||||
"@babel/runtime-corejs3": "7.9.6",
|
||||
"@babel/plugin-syntax-import-meta": "7.10.4",
|
||||
"@babel/plugin-transform-runtime": "7.10.4",
|
||||
"@babel/preset-env": "7.10.4",
|
||||
"@babel/preset-react": "7.10.4",
|
||||
"@babel/register": "7.10.4",
|
||||
"@babel/runtime-corejs3": "7.10.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-module-resolver": "4.0.0",
|
||||
"browserslist": "4.12.0",
|
||||
"browserslist": "4.13.0",
|
||||
"connect-history-api-fallback": "1.6.0",
|
||||
"copy-webpack-plugin": "6.0.1",
|
||||
"copy-webpack-plugin": "6.0.3",
|
||||
"cross-env": "7.0.2",
|
||||
"css-loader": "3.5.3",
|
||||
"eslint": "7.0.0",
|
||||
"eslint-config-airbnb": "18.1.0",
|
||||
"css-loader": "3.6.0",
|
||||
"dotenv": "8.2.0",
|
||||
"eslint": "7.4.0",
|
||||
"eslint-config-airbnb": "18.2.0",
|
||||
"eslint-import-resolver-babel-module": "5.1.2",
|
||||
"eslint-loader": "4.0.2",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"eslint-plugin-react-hooks": "4.0.2",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "6.3.1",
|
||||
"eslint-plugin-react": "7.20.3",
|
||||
"eslint-plugin-react-hooks": "4.0.6",
|
||||
"file-loader": "6.0.0",
|
||||
"hard-source-webpack-plugin": "0.13.1",
|
||||
"html-webpack-plugin": "4.3.0",
|
||||
"koa-connect": "2.0.1",
|
||||
"lint-staged": "10.2.2",
|
||||
"koa-connect": "2.1.0",
|
||||
"lint-staged": "10.2.11",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"node-sass": "4.14.1",
|
||||
"nodemon": "2.0.4",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
"rimraf": "3.0.2",
|
||||
"sass-loader": "8.0.2",
|
||||
"sass-loader": "9.0.2",
|
||||
"script-ext-html-webpack-plugin": "2.1.4",
|
||||
"style-loader": "1.2.1",
|
||||
"terser-webpack-plugin": "3.0.1",
|
||||
"terser-webpack-plugin": "3.0.6",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.7.0",
|
||||
"webpack-cli": "3.3.11",
|
||||
"webpack-bundle-analyzer": "3.8.0",
|
||||
"webpack-cli": "3.3.12",
|
||||
"webpack-dev-server": "3.11.0",
|
||||
"webpack-merge": "4.2.2"
|
||||
"webpack-merge": "5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
2
frontend/public/assets/css/skeleton.min.css
vendored
2
frontend/public/assets/css/skeleton.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -5,7 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>😕</title>
|
||||
<link type="text/css" rel="stylesheet" href="./assets/css/skeleton.min.css" />
|
||||
<link type="text/css" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata&display=swap">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./assets/images/icon.png">
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="./assets/images/icon.png">
|
||||
</head>
|
||||
|
||||
43
frontend/src/actions/CaptureActions.js
Normal file
43
frontend/src/actions/CaptureActions.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
alprFetchSuccess, alprFetchStart, dataFetchStart, dataFetchSuccess,
|
||||
} from '@slice/RecognitionSlice';
|
||||
|
||||
const API_BASE_PATH = process.env.API_BASE_PATH;
|
||||
console.log(process.env.API_BASE_PATH);
|
||||
export const getPlateRecognized = (image) => async (dispatch) => {
|
||||
console.log(image);
|
||||
dispatch(alprFetchStart());
|
||||
const body = new FormData();
|
||||
body.append('upload', image);
|
||||
try {
|
||||
console.log(API_BASE_PATH);
|
||||
const response = await fetch(`${API_BASE_PATH}/alpr`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
console.log(response);
|
||||
const data = await response.json();
|
||||
dispatch(alprFetchSuccess(data));
|
||||
console.log(data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDataForPlate = (plate) => async (dispatch) => {
|
||||
dispatch(dataFetchStart());
|
||||
console.log('Fetching data for plate', plate);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_PATH}/${plate}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
console.log(response);
|
||||
const data = await response.json();
|
||||
dispatch(dataFetchSuccess(data));
|
||||
console.log(data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPlateData = (plate) => null;
|
||||
25
frontend/src/components/BottomNavigation.jsx
Normal file
25
frontend/src/components/BottomNavigation.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { useContext } from 'react';
|
||||
import RootContext from '@context/RootContext';
|
||||
import { VIEW } from '@slice/StateSlice';
|
||||
import { fetchDataForPlate } from '@actions/CaptureActions';
|
||||
import Button from './common/Button';
|
||||
|
||||
const BottomNavigation = () => {
|
||||
const { state: { state, alpr: { results } }, dispatch } = useContext(RootContext);
|
||||
|
||||
switch (state.view) {
|
||||
case VIEW.alprResults:
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<Button text="Fetch data" onClick={() => fetchDataForPlate(results[0].plate)(dispatch)} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case VIEW.camera:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default BottomNavigation;
|
||||
34
frontend/src/components/alpr/DataView.jsx
Normal file
34
frontend/src/components/alpr/DataView.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useContext } from 'react';
|
||||
import RootContext from '@context/RootContext';
|
||||
|
||||
const DataView = () => {
|
||||
const { state, dispatch } = useContext(RootContext);
|
||||
const { results, lastFetchedDataPlate } = state.alpr;
|
||||
|
||||
const { plate, score } = results;
|
||||
return (
|
||||
<div className="row">
|
||||
<h2>{plate.toUpperCase()}</h2>
|
||||
<h3>
|
||||
Confidence
|
||||
{' '}
|
||||
{score}
|
||||
</h3>
|
||||
{lastFetchedDataPlate === plate && (
|
||||
<ul>
|
||||
{Object.entries(results).map(([key, value]) => (
|
||||
<li key={`${key}`}>
|
||||
<b>{key}</b>
|
||||
{' '}
|
||||
:
|
||||
{' '}
|
||||
{value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataView;
|
||||
35
frontend/src/components/alpr/RecognitionResults.jsx
Normal file
35
frontend/src/components/alpr/RecognitionResults.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useContext } from 'react';
|
||||
import RootContext from '@context/RootContext';
|
||||
import Button from '@components/common/Button';
|
||||
import { fetchDataForPlate } from '@actions/CaptureActions';
|
||||
import DataView from './DataView';
|
||||
|
||||
const RecognitionResults = () => {
|
||||
const { state, dispatch } = useContext(RootContext);
|
||||
|
||||
const { results, lastFetchedDataPlate } = state.alpr;
|
||||
|
||||
const fetchData = (plate) => plate && fetchDataForPlate(plate)(dispatch);
|
||||
|
||||
console.log(results);
|
||||
const getPlate = () => {
|
||||
if (!results || !results.plate) {
|
||||
return (<h2>No plate detected</h2>);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<DataView />
|
||||
<div className="row">
|
||||
<Button text="Fetch data" onClick={() => fetchData(results.plate)} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="container">
|
||||
{getPlate()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecognitionResults;
|
||||
114
frontend/src/components/camera/CameraContainer.jsx
Normal file
114
frontend/src/components/camera/CameraContainer.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, {
|
||||
useState, useEffect, useRef, useCallback, useContext,
|
||||
} from 'react';
|
||||
import Webcam from 'react-webcam';
|
||||
import FacingMode from '@constant/FacingMode';
|
||||
import Loader from '@components/common/Loader';
|
||||
import '@style/RootContainer.scss';
|
||||
import RootContext from '@context/RootContext';
|
||||
import { resetResults } from '@slice/RecognitionSlice';
|
||||
import CameraOptions from './CameraOptions';
|
||||
import Capture from './Capture';
|
||||
|
||||
const CAMERA_OPTIONS = {
|
||||
format: 'image/jpeg',
|
||||
minHeight: 1920,
|
||||
minWidth: 1080,
|
||||
quality: 0.75,
|
||||
defaultView: FacingMode.FRONT,
|
||||
};
|
||||
|
||||
const CameraContainer = () => {
|
||||
const { dispatch } = useContext(RootContext);
|
||||
const ref = useRef(null);
|
||||
const [imgSrc, setImgSrc] = useState(null);
|
||||
const [captureError, setCaptureError] = useState(null);
|
||||
const [videoInputs, setVideoInputs] = useState([]);
|
||||
const [resetCameraView, setResetCameraView] = useState(false);
|
||||
const [videoConstraints, setVideoConstraints] = useState({
|
||||
facingMode: CAMERA_OPTIONS.defaultView,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const gotDevices = (mediaDevices) => new Promise((resolve, reject) => {
|
||||
const availableVideoInputs = mediaDevices
|
||||
.filter(({ kind }) => kind === 'videoinput')
|
||||
.map(({ deviceId, label }) => ({ deviceId, label }));
|
||||
if (availableVideoInputs.length > 0) {
|
||||
resolve(availableVideoInputs);
|
||||
} else {
|
||||
reject(new Error('ERR::NO_MEDIA_TO_STREAM'));
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaDevices
|
||||
.enumerateDevices()
|
||||
.then(gotDevices)
|
||||
.then((availableVideoInputs) => setVideoInputs({ availableVideoInputs }))
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
const changeCameraView = () => {
|
||||
if (videoInputs.length === 1) {
|
||||
console.error('ERR::AVAILABLE_MEDIA_STREAMS_IS_1');
|
||||
return;
|
||||
}
|
||||
|
||||
setResetCameraView(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const { facingMode } = videoConstraints;
|
||||
const newFacingMode = facingMode === FacingMode.FRONT ? FacingMode.REAR : FacingMode.FRONT;
|
||||
setResetCameraView(false);
|
||||
setVideoConstraints({ ...videoConstraints, facingMode: newFacingMode });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const capture = useCallback(() => {
|
||||
const imageSrc = ref.current.getScreenshot();
|
||||
setImgSrc(imageSrc);
|
||||
if (!imageSrc) {
|
||||
setCaptureError(['Failed to capture camera. Please try again.']);
|
||||
} else {
|
||||
setCaptureError(null);
|
||||
}
|
||||
}, [ref, setImgSrc]);
|
||||
|
||||
const onBackClick = () => {
|
||||
setImgSrc(null);
|
||||
dispatch(resetResults());
|
||||
};
|
||||
|
||||
const cameraView = () => (
|
||||
<>
|
||||
<div className="row">
|
||||
{resetCameraView ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<Webcam
|
||||
mirrored={false}
|
||||
width="100%"
|
||||
height="100%"
|
||||
audio={false}
|
||||
ref={ref}
|
||||
screenshotFormat={CAMERA_OPTIONS.format}
|
||||
minScreenshotWidth={CAMERA_OPTIONS.minWidth}
|
||||
minScreenshotHeight={CAMERA_OPTIONS.minHeight}
|
||||
screenshotQuality={CAMERA_OPTIONS.quality}
|
||||
videoConstraints={videoConstraints} />
|
||||
)}
|
||||
</div>
|
||||
<CameraOptions capture={capture} changeCameraView={changeCameraView} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{imgSrc
|
||||
? <Capture imgSrc={imgSrc} error={captureError} onBackClick={onBackClick} />
|
||||
: cameraView()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraContainer;
|
||||
11
frontend/src/components/camera/CameraOptions.jsx
Normal file
11
frontend/src/components/camera/CameraOptions.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import Button from '@components/common/Button';
|
||||
|
||||
const CameraOptions = ({ capture, changeCameraView }) => (
|
||||
<div className="row button-container">
|
||||
<Button text="Switch camera" onClick={changeCameraView} />
|
||||
<Button text="Take image" onClick={capture} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CameraOptions;
|
||||
40
frontend/src/components/camera/Capture.jsx
Normal file
40
frontend/src/components/camera/Capture.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import Loader from '@components/common/Loader';
|
||||
import { getPlateRecognized } from '@actions/CaptureActions';
|
||||
import Button from '@components/common/Button';
|
||||
import RootContext from '@context/RootContext';
|
||||
import RecognitionResults from '@components/alpr/RecognitionResults';
|
||||
|
||||
const UploadCapture = ({ imgSrc, onBackClick }) => {
|
||||
const [hasFetched, setHasFetched] = useState(true);
|
||||
const { state: { alpr: { isFetchingALPR } }, dispatch } = useContext(RootContext);
|
||||
const onUploadClick = () => {
|
||||
getPlateRecognized(imgSrc)(dispatch);
|
||||
setHasFetched(true);
|
||||
};
|
||||
return (
|
||||
<div className="container">
|
||||
{isFetchingALPR ? <Loader /> : (
|
||||
<div className="row">
|
||||
<img src={imgSrc} alt="captured" />
|
||||
</div>
|
||||
)}
|
||||
{hasFetched && !isFetchingALPR && (<RecognitionResults />)}
|
||||
<div className="row button-container">
|
||||
<Button text="Back" onClick={onBackClick} />
|
||||
{!hasFetched && <Button text="Upload" onClick={onUploadClick} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Capture = ({
|
||||
imgSrc, isCapturing, error, onBackClick,
|
||||
}) => (
|
||||
<div className="capture-container row">
|
||||
{isCapturing && (<Loader />)}
|
||||
{error ? (<p>{error}</p>) : (<UploadCapture imgSrc={imgSrc} onBackClick={onBackClick} />)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Capture;
|
||||
9
frontend/src/components/common/Button.jsx
Normal file
9
frontend/src/components/common/Button.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const Button = ({ text, onClick }) => (
|
||||
<button className="button light" type="button" onClick={onClick}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
6
frontend/src/components/common/Loader.jsx
Normal file
6
frontend/src/components/common/Loader.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import './Loader.scss';
|
||||
|
||||
const Loader = () => (<div className="loader">Loading...</div>);
|
||||
|
||||
export default Loader;
|
||||
46
frontend/src/components/common/Loader.scss
Normal file
46
frontend/src/components/common/Loader.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.loader,
|
||||
.loader:after {
|
||||
border-radius: 50%;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 60px auto;
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
border-top: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-right: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-bottom: 1.1em solid rgba(255, 255, 255, 0.2);
|
||||
border-left: 1.1em solid #ffffff;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load8 1.1s infinite linear;
|
||||
animation: load8 1.1s infinite linear;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load8 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
6
frontend/src/constant/FacingMode.js
Normal file
6
frontend/src/constant/FacingMode.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
FRONT: 'user',
|
||||
REAR: {
|
||||
exact: 'environment',
|
||||
},
|
||||
};
|
||||
@@ -1,21 +1,13 @@
|
||||
import React, { useReducer, useContext } from 'react';
|
||||
import { usePersistedContext, usePersistedReducer } from '@hook/usePersist';
|
||||
|
||||
import RootContainer from '@container/RootContainer';
|
||||
import RootContext from '@context/RootContext';
|
||||
import RootReducer from '@slice/RootSlice';
|
||||
import Properties from '@constant/Properties';
|
||||
|
||||
const App = () => {
|
||||
const globalStore = usePersistedContext(
|
||||
useContext(RootContext),
|
||||
Properties.STORE_KEY,
|
||||
);
|
||||
const globalStore = useContext(RootContext);
|
||||
|
||||
const [state, dispatch] = usePersistedReducer(
|
||||
useReducer(RootReducer, globalStore),
|
||||
Properties.STORE_KEY,
|
||||
);
|
||||
const [state, dispatch] = useReducer(RootReducer, globalStore);
|
||||
|
||||
return (
|
||||
<RootContext.Provider value={{ state, dispatch }}>
|
||||
|
||||
@@ -1,91 +1,14 @@
|
||||
import React, {
|
||||
useState, useRef, useEffect, useCallback,
|
||||
} from 'react';
|
||||
import Webcam from 'react-webcam';
|
||||
import React from 'react';
|
||||
import CameraContainer from '@components/camera/CameraContainer';
|
||||
import '@style/RootContainer.scss';
|
||||
import BottomNavigation from '@components/BottomNavigation';
|
||||
|
||||
import '@style/RootContainer';
|
||||
|
||||
const RootContainer = () => {
|
||||
const webcamRef = useRef(null);
|
||||
const [videoInputs, setVideoInputs] = useState([]);
|
||||
const [resetCameraView, setResetCameraView] = useState(false);
|
||||
const [videoConstraints, setVideoConstraints] = useState({
|
||||
facingMode: { exact: 'environment' },
|
||||
});
|
||||
|
||||
const [imgSrc, setImgSrc] = useState(null);
|
||||
|
||||
const capture = useCallback(() => {
|
||||
const imageSrc = webcamRef.current.getScreenshot();
|
||||
setImgSrc(imageSrc);
|
||||
}, [webcamRef, setImgSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
const gotDevices = (mediaDevices) => new Promise((resolve, reject) => {
|
||||
const availableVideoInputs = [];
|
||||
mediaDevices.forEach((mediaDevice) => {
|
||||
if (mediaDevice.kind === 'videoinput') {
|
||||
availableVideoInputs.push({
|
||||
deviceId: mediaDevice.deviceId,
|
||||
label: mediaDevice.label,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (availableVideoInputs.length > 0) {
|
||||
resolve(availableVideoInputs);
|
||||
} else {
|
||||
reject(new Error('ERR::NO_MEDIA_TO_STREAM'));
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaDevices
|
||||
.enumerateDevices()
|
||||
.then(gotDevices)
|
||||
.then((availableVideoInputs) => setVideoInputs({ availableVideoInputs }))
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
const changeCameraView = () => {
|
||||
if (videoInputs.length === 1) {
|
||||
return console.error('ERR::AVAILABLE_MEDIA_STREAMS_IS_1');
|
||||
}
|
||||
|
||||
setResetCameraView(true);
|
||||
|
||||
setTimeout(() => {
|
||||
const { facingMode } = videoConstraints;
|
||||
const newFacingMode = facingMode === 'user' ? { exact: 'environment' } : 'user';
|
||||
setResetCameraView(false);
|
||||
setVideoConstraints({ ...videoConstraints, facingMode: newFacingMode });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{resetCameraView ? (
|
||||
'Loading...'
|
||||
) : (
|
||||
<Webcam
|
||||
audio={false}
|
||||
height="100%"
|
||||
ref={webcamRef}
|
||||
screenshotFormat="image/png"
|
||||
minScreenshotWidth={1080}
|
||||
minScreenshotHeight={1920}
|
||||
screenshotQuality={1}
|
||||
width="100%"
|
||||
videoConstraints={videoConstraints} />
|
||||
)}
|
||||
<button type="button" onClick={changeCameraView}>
|
||||
Switch camera
|
||||
</button>
|
||||
<button type="button" onClick={capture}>
|
||||
Take image
|
||||
</button>
|
||||
{imgSrc && (<img src={imgSrc} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const RootContainer = () => (
|
||||
<div className="container root-container">
|
||||
<div className="header" />
|
||||
<CameraContainer />
|
||||
<BottomNavigation />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RootContainer;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
import { initialState as alpr } from '@slice/RecognitionSlice';
|
||||
import { initialState as state } from '@slice/StateSlice';
|
||||
|
||||
export default createContext({}); // include initialStates here
|
||||
export default createContext({
|
||||
alpr,
|
||||
state,
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||
const useInterval = (callback, delay) => {
|
||||
const savedCallback = useRef();
|
||||
|
||||
// Remember the latest callback.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
const tick = () => savedCallback.current();
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
return () => {};
|
||||
}, [delay]);
|
||||
};
|
||||
|
||||
export default useInterval;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const DEFAULT_STATE_KEY = 'state';
|
||||
|
||||
export const usePersistedContext = (context, key = DEFAULT_STATE_KEY) => {
|
||||
const persistedContext = localStorage.getItem(key);
|
||||
return persistedContext ? JSON.parse(persistedContext) : context;
|
||||
};
|
||||
|
||||
export const usePersistedReducer = ([state, dispatch], key = DEFAULT_STATE_KEY) => {
|
||||
useEffect(
|
||||
() => localStorage.setItem(key, JSON.stringify(state)),
|
||||
[key, state],
|
||||
);
|
||||
return [state, dispatch];
|
||||
};
|
||||
66
frontend/src/slice/RecognitionSlice.js
Normal file
66
frontend/src/slice/RecognitionSlice.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import {
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit';
|
||||
|
||||
export const initialState = {
|
||||
isFetchingALPR: false,
|
||||
isFetchingData: false,
|
||||
lastFetchedDataPlate: '540BLG',
|
||||
results: {
|
||||
plate: '540BLG',
|
||||
name: 'ALFA ROMEO 159 SPORTWAGON',
|
||||
vin: 'ZAR93900007055169',
|
||||
'first registration': '07.09.2006',
|
||||
category: 'passenger car',
|
||||
body: 'universal',
|
||||
'body colour': 'grey',
|
||||
engine: '2387 cm',
|
||||
'engine power': '147 kW',
|
||||
fuel: 'Diesel',
|
||||
transmission: 'Manual',
|
||||
drivetrain: 'front wheel drive',
|
||||
'registration certificate': 'EL573054',
|
||||
score: 1.0,
|
||||
},
|
||||
};
|
||||
|
||||
const recognitionSlice = createSlice({
|
||||
name: 'alpr',
|
||||
initialState,
|
||||
reducers: {
|
||||
alprFetchStart(state) {
|
||||
state.isFetchingALPR = true;
|
||||
},
|
||||
alprFetchSuccess(state, {
|
||||
payload,
|
||||
}) {
|
||||
state.isFetchingALPR = false;
|
||||
state.results = payload.results;
|
||||
},
|
||||
resetResults(state) {
|
||||
state.results = [];
|
||||
state.lastFetchedDataPlate = null;
|
||||
},
|
||||
dataFetchStart(state) {
|
||||
state.isFetchingData = true;
|
||||
},
|
||||
dataFetchSuccess(state, {
|
||||
payload,
|
||||
}) {
|
||||
state.isFetchingData = false;
|
||||
state.results = { ...state.results, ...payload };
|
||||
state.lastFetchedDataPlate = payload ? payload.plate : null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
alprFetchStart,
|
||||
alprFetchSuccess,
|
||||
resetResults,
|
||||
dataFetchStart,
|
||||
dataFetchSuccess,
|
||||
} = recognitionSlice.actions;
|
||||
|
||||
export default recognitionSlice.reducer;
|
||||
@@ -1,3 +1,8 @@
|
||||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
import alpr from '@slice/RecognitionSlice';
|
||||
import state from '@slice/StateSlice';
|
||||
|
||||
export default combineReducers({}); // include slices here
|
||||
export default combineReducers({
|
||||
alpr,
|
||||
state,
|
||||
});
|
||||
|
||||
29
frontend/src/slice/StateSlice.js
Normal file
29
frontend/src/slice/StateSlice.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
export const VIEW = {
|
||||
camera: 'camera',
|
||||
capture: 'capture',
|
||||
alprResults: 'alprResults',
|
||||
dataResults: 'dataResults',
|
||||
};
|
||||
|
||||
export const initialState = {
|
||||
view: VIEW.camera,
|
||||
};
|
||||
|
||||
const stateSlice = createSlice({
|
||||
name: 'state',
|
||||
initialState,
|
||||
reducers: {
|
||||
setView(state, { payload }) {
|
||||
state.view = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setView,
|
||||
} = stateSlice.actions;
|
||||
|
||||
export default stateSlice.reducer;
|
||||
@@ -1,10 +1,65 @@
|
||||
@import 'palette.scss';
|
||||
@import 'constants.scss';
|
||||
@import './common/font/camphor.scss';
|
||||
@import './common/palette.scss';
|
||||
@import './common/constants.scss';
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Roboto';
|
||||
font-family: 'Camphor' !important;
|
||||
font: 400 13.333px 'Camphor' !important;
|
||||
overflow-x: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: $orange;
|
||||
}
|
||||
|
||||
.root-container {
|
||||
//box-shadow: 0 30px 40px rgba(0,0,0,.1);
|
||||
padding: 2em;
|
||||
margin-top: 2em;
|
||||
height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
background-image: url('../../assets/images/Maanteeamet.png');
|
||||
background-size: cover;
|
||||
height: 120px;
|
||||
margin-bottom: 3em;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
img {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1em;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
margin: 0.25em;
|
||||
font-family: inherit;
|
||||
|
||||
&.light {
|
||||
background-color: rgba(255, 121, 121, 0.5);
|
||||
border: 1px solid rgba(255, 121, 121, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.capture-container {
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: 100%;
|
||||
max-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
46
frontend/src/style/common/font/camphor.scss
Normal file
46
frontend/src/style/common/font/camphor.scss
Normal file
File diff suppressed because one or more lines are too long
1
frontend/src/style/common/palette.scss
Normal file
1
frontend/src/style/common/palette.scss
Normal file
@@ -0,0 +1 @@
|
||||
$orange: #fed8b1;
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const { ProgressPlugin } = require('webpack');
|
||||
const { ProgressPlugin, EnvironmentPlugin } = require('webpack');
|
||||
const convert = require('koa-connect');
|
||||
const history = require('connect-history-api-fallback');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
@@ -71,6 +71,7 @@ module.exports = {
|
||||
extensions: ['*', '.js', '.jsx', '.css', '.scss'],
|
||||
},
|
||||
plugins: [
|
||||
new EnvironmentPlugin(['NODE_ENV', 'API_BASE_PATH']),
|
||||
new ProgressPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: commonPaths.templatePath,
|
||||
|
||||
@@ -46,44 +46,44 @@ module.exports = {
|
||||
contentBase: commonPaths.outputPath,
|
||||
compress: true,
|
||||
hot: true,
|
||||
port: 3001,
|
||||
port: 3333,
|
||||
},
|
||||
plugins: [
|
||||
new HotModuleReplacementPlugin(),
|
||||
new HardSourceWebpackPlugin({
|
||||
// Either an absolute path or relative to webpack's options.context.
|
||||
cacheDirectory: commonPaths.cachePath,
|
||||
// Either a string of object hash function given a webpack config.
|
||||
// node-object-hash on npm can be used to build this.
|
||||
configHash: (webpackConfig) => require('node-object-hash')({ // eslint-disable-line
|
||||
sort: false,
|
||||
}).hash(webpackConfig),
|
||||
// Either false, a string, an object, or a project hashing function.
|
||||
environmentHash: {
|
||||
root: process.cwd(),
|
||||
directories: [],
|
||||
files: [
|
||||
// Cache will get an unique hash based on those files
|
||||
// if either of them changes, new cache must be generated
|
||||
'package-lock.json',
|
||||
'.babelrc.js',
|
||||
],
|
||||
},
|
||||
info: {
|
||||
mode: 'none',
|
||||
level: 'debug',
|
||||
},
|
||||
// Clean up large, old caches automatically.
|
||||
cachePrune: {
|
||||
// Caches younger than `maxAge` are not considered for deletion.
|
||||
// They must be at least this old in milliseconds.
|
||||
maxAge: 3 * 60 * 60 * 1000, // 3 hours
|
||||
// All caches together must be larger than `sizeThreshold` before any
|
||||
// caches will be deleted.
|
||||
// Together they must be at least this big in bytes.
|
||||
sizeThreshold: 50 * 1024 * 1024, // 50 MB
|
||||
},
|
||||
}),
|
||||
// new HardSourceWebpackPlugin({
|
||||
// // Either an absolute path or relative to webpack's options.context.
|
||||
// cacheDirectory: commonPaths.cachePath,
|
||||
// // Either a string of object hash function given a webpack config.
|
||||
// // node-object-hash on npm can be used to build this.
|
||||
// configHash: (webpackConfig) => require('node-object-hash')({ // eslint-disable-line
|
||||
// sort: false,
|
||||
// }).hash(webpackConfig),
|
||||
// // Either false, a string, an object, or a project hashing function.
|
||||
// environmentHash: {
|
||||
// root: process.cwd(),
|
||||
// directories: [],
|
||||
// files: [
|
||||
// // Cache will get an unique hash based on those files
|
||||
// // if either of them changes, new cache must be generated
|
||||
// 'package-lock.json',
|
||||
// '.babelrc.js',
|
||||
// ],
|
||||
// },
|
||||
// info: {
|
||||
// mode: 'none',
|
||||
// level: 'debug',
|
||||
// },
|
||||
// // Clean up large, old caches automatically.
|
||||
// cachePrune: {
|
||||
// // Caches younger than `maxAge` are not considered for deletion.
|
||||
// // They must be at least this old in milliseconds.
|
||||
// maxAge: 3 * 60 * 60 * 1000, // 3 hours
|
||||
// // All caches together must be larger than `sizeThreshold` before any
|
||||
// // caches will be deleted.
|
||||
// // Together they must be at least this big in bytes.
|
||||
// sizeThreshold: 50 * 1024 * 1024, // 50 MB
|
||||
// },
|
||||
// }),
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerPort: 8888,
|
||||
openAnalyzer: false,
|
||||
|
||||
1856
frontend/yarn.lock
1856
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "maanteeamet-fetch",
|
||||
"name": "car-fetcher",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "maanteeamet-fetch",
|
||||
"name": "car-fetcher",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node api/src/index.js",
|
||||
"heroku": "git subtree push --prefix api heroku master",
|
||||
"predeploy": "cd frontend && npm run bundle",
|
||||
"deploy": "node -r dotenv/config deploy/ftp.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user