Cleanup folder layout
This commit is contained in:
@@ -4,18 +4,18 @@ Welcome to your new project.
|
|||||||
|
|
||||||
It contains these folders and files, following our recommended project layout:
|
It contains these folders and files, following our recommended project layout:
|
||||||
|
|
||||||
| File or Folder | Purpose |
|
| File or Folder | Purpose |
|
||||||
| -------------- | ------------------------------------ |
|
|------------------|--------------------------------------|
|
||||||
| `app/` | will contain compiled front bundles |
|
| `app/` | will contain compiled front bundles |
|
||||||
| `app-src/` | contains frontend app on react |
|
| `app/react/` | contains frontend app on react |
|
||||||
| `deployers/` | contains deployment staff |
|
| `app/deployers/` | contains deployment staff |
|
||||||
| `db/` | your domain models and data go here |
|
| `db/` | your domain models and data go here |
|
||||||
| `srv/` | your service models and code go here |
|
| `srv/` | your service models and code go here |
|
||||||
| `test/` | your services tests |
|
| `test/` | your services tests |
|
||||||
| `package.json` | project metadata and configuration |
|
| `package.json` | project metadata and configuration |
|
||||||
| `mta.yaml` | deployment config |
|
| `mta.yaml` | deployment config |
|
||||||
| `readme.md` | this getting started guide |
|
| `readme.md` | this getting started guide |
|
||||||
| `server.js` | initial server set up |
|
| `server.js` | initial server set up |
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ npm run deploy
|
|||||||
cds watch
|
cds watch
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open `app-src` folder and run next commands. This will install dependencies and run frontend src files watcher. When you will change src files your bundles in app directory will re-compiled. Now you can enjoy development:
|
- Open `app/react` folder and run next commands. This will install dependencies and run frontend src files watcher. When you will change src files your bundles in app directory will re-compiled. Now you can enjoy development:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
npm install
|
npm install
|
||||||
@@ -62,14 +62,14 @@ npm run watch
|
|||||||
cf login
|
cf login
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open `app-src` folder and run the following commands. This will create frontend production bundles in app subfolder:
|
- Open `app/react` folder and run the following commands. This will create frontend production bundles in app subfolder:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
- Clean up deployers/html5Deployer/resources folder from the previous frontend build
|
- Clean up app/deployers/html5Deployer/resources folder from the previous frontend build
|
||||||
|
|
||||||
- From root directory run:
|
- From root directory run:
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["@babel/preset-react", "@babel/preset-env"],
|
|
||||||
"plugins": ["@babel/plugin-transform-runtime", "babel-plugin-syntax-dynamic-import"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2020": true
|
|
||||||
},
|
|
||||||
"extends": ["plugin:react/recommended", "airbnb", "prettier"],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": 11,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": ["react", "prettier"],
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": ["error", { "parser": "flow", "endOfLine": "auto" }],
|
|
||||||
"linebreak-style": [0, "error", "windows"],
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"no-shadow": "off",
|
|
||||||
"react/forbid-prop-types": "off",
|
|
||||||
"no-alert": "off",
|
|
||||||
"jsx-a11y/label-has-associated-control": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"required": {
|
|
||||||
"some": ["nesting", "id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"jsx-a11y/label-has-for": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"required": {
|
|
||||||
"some": ["nesting", "id"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/jsx-props-no-spreading": "off", // props spreading,
|
|
||||||
"no-console": "off",
|
|
||||||
"consistent-return": "off",
|
|
||||||
"prefer-destructuring": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
media-store/app-src/.gitignore
vendored
23
media-store/app-src/.gitignore
vendored
@@ -1,23 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
13
media-store/app-src/.vscode/launch.json
vendored
13
media-store/app-src/.vscode/launch.json
vendored
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Chrome",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceRoot}/src"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"# Media store UI"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mediastore",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": false,
|
|
||||||
"scripts": {
|
|
||||||
"start": "./node_modules/.bin/webpack-dev-server --config ./webpack/webpack-dev-server.js",
|
|
||||||
"watch": "./node_modules/.bin/webpack -w --config ./webpack/webpack.dev.js",
|
|
||||||
"build:dev": "./node_modules/.bin/webpack --config ./webpack/webpack.dev.js",
|
|
||||||
"build:prod": "./node_modules/.bin/webpack --config ./webpack/webpack.prod.js",
|
|
||||||
"lint": "./node_modules/.bin/eslint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
|
||||||
"@testing-library/react": "^9.3.2",
|
|
||||||
"@testing-library/user-event": "^7.1.2",
|
|
||||||
"@umijs/hooks": "^1.9.3",
|
|
||||||
"antd": "^4.8.2",
|
|
||||||
"@ant-design/icons": "4.3.0",
|
|
||||||
"axios": "^0.20.0",
|
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
|
||||||
"copy-webpack-plugin": "^6.3.2",
|
|
||||||
"css-minimizer-webpack-plugin": "^1.1.5",
|
|
||||||
"events": "^3.2.0",
|
|
||||||
"html-webpack-plugin": "^4.5.0",
|
|
||||||
"lodash": "^4.17.20",
|
|
||||||
"mini-css-extract-plugin": "^1.3.1",
|
|
||||||
"moment": "^2.29.1",
|
|
||||||
"prop-types": "^15.7.2",
|
|
||||||
"react": "^16.14.0",
|
|
||||||
"react-dev-utils": "^11.0.1",
|
|
||||||
"react-dom": "^16.14.0",
|
|
||||||
"react-router-dom": "^5.2.0",
|
|
||||||
"terser-webpack-plugin": "^5.0.3",
|
|
||||||
"webpack": "5.8.0",
|
|
||||||
"webpack-dev-server": "^3.11.0",
|
|
||||||
"webpack-merge": "^5.4.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.12.9",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
|
||||||
"@babel/polyfill": "^7.12.1",
|
|
||||||
"@babel/preset-env": "^7.12.7",
|
|
||||||
"@babel/preset-react": "^7.12.7",
|
|
||||||
"@babel/runtime": "^7.12.5",
|
|
||||||
"babel-loader": "^8.2.2",
|
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
|
||||||
"cowsay": "^1.4.0",
|
|
||||||
"css-loader": "^5.0.1",
|
|
||||||
"eslint": "^7.14.0",
|
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
|
||||||
"eslint-config-prettier": "^6.15.0",
|
|
||||||
"eslint-plugin-import": "^2.22.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
|
||||||
"eslint-plugin-react": "^7.21.5",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
|
||||||
"prettier": "^2.2.1",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"webpack-cli": "^3.3.12"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "react-app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"sap.app": {
|
|
||||||
"id": "mediastore",
|
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"welcomeFile": "/index.html",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"source": "^(.*)",
|
|
||||||
"target": "$1",
|
|
||||||
"service": "html5-apps-repo-rt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
@import "~antd/dist/antd.css";
|
|
||||||
|
|
||||||
html {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
section.ant-layout {
|
|
||||||
height: 100vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout
|
|
||||||
*/
|
|
||||||
.site-layout .site-layout-background {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import 'antd/dist/antd.css';
|
|
||||||
import './App.css';
|
|
||||||
import { Layout } from 'antd';
|
|
||||||
import { MyRouter } from './components/Router';
|
|
||||||
import { AppStateContextProvider } from './contexts/AppStateContext';
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
|
||||||
<Layout style={{ height: '100%' }}>
|
|
||||||
<AppStateContextProvider>
|
|
||||||
<MyRouter />
|
|
||||||
</AppStateContextProvider>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { getUserFromLS, getLocaleFromLS } from '../util/localStorageService';
|
|
||||||
import { emitter } from '../util/EventEmitter';
|
|
||||||
|
|
||||||
const TIMEOUT = 2000;
|
|
||||||
const RETRY_COUNT = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is axios instance
|
|
||||||
*/
|
|
||||||
const axiosInstance = axios.create({
|
|
||||||
baseURL: process.env.SERVICE_URL,
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
retryDelay: TIMEOUT,
|
|
||||||
retry: RETRY_COUNT,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changing user axios default params,
|
|
||||||
* which are used in api call functions (calls.js)
|
|
||||||
* @param {*} currentUser current user from react state and local storage
|
|
||||||
*/
|
|
||||||
function changeUserDefaults(currentUser) {
|
|
||||||
if (currentUser) {
|
|
||||||
axiosInstance.defaults.headers.common.Authorization = `Basic ${currentUser.accessToken}`;
|
|
||||||
axiosInstance.defaults.userID = currentUser.ID;
|
|
||||||
if (currentUser.roles.includes('customer')) {
|
|
||||||
axiosInstance.defaults.userEntity = `Customers/${currentUser.ID}`;
|
|
||||||
axiosInstance.defaults.tracksEntity = 'MarkedTracks';
|
|
||||||
} else {
|
|
||||||
axiosInstance.defaults.userEntity = `Employees/${currentUser.ID}`;
|
|
||||||
axiosInstance.defaults.tracksEntity = 'Tracks';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
axiosInstance.defaults.tracksEntity = 'Tracks';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This func changing axios instance default params
|
|
||||||
* @param {*} locale current locale from react state and local storage
|
|
||||||
*/
|
|
||||||
function changeLocaleDefaults(locale) {
|
|
||||||
if (locale) {
|
|
||||||
axiosInstance.defaults.headers.common['Accept-language'] = locale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init axios defaults
|
|
||||||
*/
|
|
||||||
const user = getUserFromLS();
|
|
||||||
const locale = getLocaleFromLS();
|
|
||||||
changeUserDefaults(user);
|
|
||||||
changeLocaleDefaults(locale);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry request if response time is too long
|
|
||||||
* See link below
|
|
||||||
* {@link https://github.com/axios/axios/issues/164#issuecomment-327837467 GitHub}
|
|
||||||
* @param {*} err response error object
|
|
||||||
*/
|
|
||||||
function axiosRetryInterceptor(err) {
|
|
||||||
const config = err.config;
|
|
||||||
// If config does not exist or the retry option is not set, reject
|
|
||||||
if (config && config.retry) {
|
|
||||||
// Set the variable for keeping track of the retry count
|
|
||||||
config.retryCount = config.retryCount || 0;
|
|
||||||
|
|
||||||
// Check if we've maxed out the total number of retries
|
|
||||||
if (config.retryCount >= config.retry) {
|
|
||||||
// Reject with the error
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increase the retry count
|
|
||||||
config.retryCount += 1;
|
|
||||||
|
|
||||||
// Create new promise to handle exponential backoff
|
|
||||||
const backoff = new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, config.retryDelay || 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the promise in which recalls axios to retry the request
|
|
||||||
return backoff.then(() => {
|
|
||||||
return axios(config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Things below needed for refresh tokens mechanism implementation
|
|
||||||
*/
|
|
||||||
let isRefreshing = false;
|
|
||||||
let subscribers = [];
|
|
||||||
const refreshTokens = (refreshToken) => {
|
|
||||||
return axiosInstance.post(
|
|
||||||
'users/refreshTokens',
|
|
||||||
{ refreshToken },
|
|
||||||
{
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh tokens interceptor
|
|
||||||
* See link below
|
|
||||||
* {@link https://gist.github.com/mkjiau/650013a99c341c9f23ca00ccb213db1c#gistcomment-3536511 GitHub}
|
|
||||||
* @param {*} error error response object
|
|
||||||
*/
|
|
||||||
function axiosRefreshTokensInterceptor(error) {
|
|
||||||
const originalRequest = error.config;
|
|
||||||
const user = getUserFromLS();
|
|
||||||
|
|
||||||
if (error.response && error.response.status === 401 && !!user) {
|
|
||||||
if (originalRequest.url === 'users/login') {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if users/refreshTokens request failed
|
|
||||||
if (isRefreshing && originalRequest.url === 'users/refreshTokens') {
|
|
||||||
subscribers.forEach((request) => request.reject(error));
|
|
||||||
subscribers = [];
|
|
||||||
isRefreshing = false;
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if got a 401 error we sending users/refreshTokens request
|
|
||||||
if (!isRefreshing) {
|
|
||||||
isRefreshing = true;
|
|
||||||
|
|
||||||
refreshTokens(user.refreshToken)
|
|
||||||
.then((response) => {
|
|
||||||
emitter.emit('UPDATE_USER', response.data);
|
|
||||||
subscribers.forEach((request) => request.resolve(response.data.accessToken));
|
|
||||||
subscribers = [];
|
|
||||||
isRefreshing = false;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
emitter.emit('UPDATE_USER', undefined);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// holding requests which should be sended after users/refreshTokens complete
|
|
||||||
// otherwise if users/refreshTokens failed an error will be thrown
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
subscribers.push({
|
|
||||||
resolve: (newAccessToken) => {
|
|
||||||
originalRequest.headers.Authorization = `Basic ${newAccessToken}`;
|
|
||||||
resolve(axiosInstance(originalRequest));
|
|
||||||
},
|
|
||||||
reject: (err) => {
|
|
||||||
reject(err);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
axiosInstance.interceptors.response.use(null, (error) => {
|
|
||||||
return (
|
|
||||||
axiosRefreshTokensInterceptor(error) || axiosRetryInterceptor(error) || Promise.reject(error)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export { axiosInstance, changeLocaleDefaults, changeUserDefaults };
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { axiosInstance } from './axiosInstance';
|
|
||||||
|
|
||||||
const BROWSE_TRACKS_SERVICE = 'browse-tracks';
|
|
||||||
const INVOICES_SERVICE = 'browse-invoices';
|
|
||||||
const USER_SERVICE = 'users';
|
|
||||||
const MANAGE_STORE = 'manage-store';
|
|
||||||
|
|
||||||
const constructGenresQuery = (genreIds) => {
|
|
||||||
return !isEmpty(genreIds)
|
|
||||||
? ` and ${genreIds.map((value) => `genre_ID eq ${value}`).join(' or ')}`
|
|
||||||
: '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchTacks = ({ $top = 20, $skip = 0, genreIds = [], substr = '' } = {}) => {
|
|
||||||
const serializeTracksUrl = () => {
|
|
||||||
return `$expand=genre,album($expand=artist)&$top=${$top}&$skip=${$skip}&$filter=${`contains(name,'${substr}')${constructGenresQuery(
|
|
||||||
genreIds
|
|
||||||
)}`}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return axiosInstance.get(`${BROWSE_TRACKS_SERVICE}/${axiosInstance.defaults.tracksEntity}`, {
|
|
||||||
params: {},
|
|
||||||
paramsSerializer: () => serializeTracksUrl(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const countTracks = ({ genreIds = [], substr = '' } = {}) => {
|
|
||||||
const { tracksEntity } = axiosInstance.defaults;
|
|
||||||
|
|
||||||
return axiosInstance.get(
|
|
||||||
`${BROWSE_TRACKS_SERVICE}/${tracksEntity}/$count?$filter=${`contains(name,'${substr}')${constructGenresQuery(
|
|
||||||
genreIds
|
|
||||||
)}`}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchGenres = () => {
|
|
||||||
return axiosInstance.get(`${BROWSE_TRACKS_SERVICE}/Genres`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const invoice = (tracks) => {
|
|
||||||
return axiosInstance.post(
|
|
||||||
`${INVOICES_SERVICE}/invoice`,
|
|
||||||
{
|
|
||||||
tracks,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPerson = () => {
|
|
||||||
return axiosInstance.get(`${USER_SERVICE}/${axiosInstance.defaults.userEntity}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmPerson = (person) => {
|
|
||||||
return axiosInstance.put(
|
|
||||||
`${USER_SERVICE}/${axiosInstance.defaults.userEntity}`,
|
|
||||||
{
|
|
||||||
...person,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchInvoices = () => {
|
|
||||||
return axiosInstance.get(
|
|
||||||
`${INVOICES_SERVICE}/Invoices?$expand=invoiceItems($expand=track($expand=album($expand=artist)))`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelInvoice = (ID) => {
|
|
||||||
return axiosInstance.post(
|
|
||||||
`${INVOICES_SERVICE}/cancelInvoice`,
|
|
||||||
{
|
|
||||||
ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchAlbumsByName = (substr = '', top) => {
|
|
||||||
return axiosInstance.get(
|
|
||||||
`${BROWSE_TRACKS_SERVICE}/Albums?$filter=${`contains(title,'${substr}')&$top=${top}`}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTrack = (data) => {
|
|
||||||
return axiosInstance.post(`${MANAGE_STORE}/Tracks`, data, {
|
|
||||||
headers: { 'content-type': 'application/json;IEEE754Compatible=true' },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addArtist = (data) => {
|
|
||||||
return axiosInstance.post(`${MANAGE_STORE}/Artists`, data, {
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAlbum = (data) => {
|
|
||||||
return axiosInstance.post(`${MANAGE_STORE}/Albums`, data, {
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchArtistsByName = (substr = '', top) => {
|
|
||||||
return axiosInstance.get(
|
|
||||||
`${MANAGE_STORE}/Artists?$filter=${`contains(name,'${substr}')&$top=${top}`}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const login = (data) => {
|
|
||||||
return axiosInstance.post(`${USER_SERVICE}/login`, data, {
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrack = (track) => {
|
|
||||||
return axiosInstance.put(
|
|
||||||
`${MANAGE_STORE}/Tracks/${track.ID}`,
|
|
||||||
{
|
|
||||||
...track,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: { 'content-type': 'application/json;IEEE754Compatible=true' },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTrack = (ID) => {
|
|
||||||
return axiosInstance.get(
|
|
||||||
`${BROWSE_TRACKS_SERVICE}/${axiosInstance.defaults.tracksEntity}/${ID}?$expand=genre,album($expand=artist)`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTrack = (ID) => {
|
|
||||||
return axiosInstance.delete(`${MANAGE_STORE}/Tracks(${ID})`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
fetchTacks,
|
|
||||||
countTracks,
|
|
||||||
fetchGenres,
|
|
||||||
invoice,
|
|
||||||
fetchPerson,
|
|
||||||
confirmPerson,
|
|
||||||
fetchInvoices,
|
|
||||||
cancelInvoice,
|
|
||||||
fetchAlbumsByName,
|
|
||||||
addTrack,
|
|
||||||
addArtist,
|
|
||||||
addAlbum,
|
|
||||||
fetchArtistsByName,
|
|
||||||
login,
|
|
||||||
updateTrack,
|
|
||||||
getTrack,
|
|
||||||
deleteTrack,
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { Result, Button } from 'antd';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
|
|
||||||
const ErrorPage = () => {
|
|
||||||
const { error, setError } = useAppState();
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const onGoHome = () => {
|
|
||||||
setError({});
|
|
||||||
history.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const goLoginPage = () => {
|
|
||||||
setError({});
|
|
||||||
history.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const goHomeButton = (
|
|
||||||
<Button onClick={onGoHome} key={1} type="primary">
|
|
||||||
Back Home
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
const goLoginButton = (
|
|
||||||
<Button onClick={goLoginPage} key={2} type="primary">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const errorResultProps = isEmpty(error)
|
|
||||||
? {
|
|
||||||
status: 404,
|
|
||||||
title: 'Not found',
|
|
||||||
subTitle: 'Sorry, the page you visited does not exist.',
|
|
||||||
extra: goHomeButton,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
status: [404, 403, 500].includes(error.status) ? error.status : 'error',
|
|
||||||
title: error.statusText,
|
|
||||||
subTitle: error.message,
|
|
||||||
extra: error.status === 401 ? [goHomeButton, goLoginButton] : goHomeButton,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Result {...errorResultProps} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ErrorPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.ant-menu-item .anticon {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Menu, Badge, Spin, message } from 'antd';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import {
|
|
||||||
CreditCardOutlined,
|
|
||||||
LogoutOutlined,
|
|
||||||
LoginOutlined,
|
|
||||||
LoadingOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { setLocaleToLS } from '../util/localStorageService';
|
|
||||||
import { changeLocaleDefaults } from '../api/axiosInstance';
|
|
||||||
import { emitter } from '../util/EventEmitter';
|
|
||||||
import './Header.css';
|
|
||||||
import { requireEmployee, requireCustomer, MESSAGE_TIMEOUT } from '../util/constants';
|
|
||||||
|
|
||||||
const { SubMenu } = Menu;
|
|
||||||
|
|
||||||
const keys = ['/', '/person', '/login', '/manage', '/invoice', '/invoices'];
|
|
||||||
const AVAILABLE_LOCALES = ['en', 'fr', 'de'];
|
|
||||||
const RELOAD_LOCATION_NUMBER = 0;
|
|
||||||
|
|
||||||
const Header = () => {
|
|
||||||
const history = useHistory();
|
|
||||||
const location = useLocation();
|
|
||||||
const { user, invoicedItems, locale, setLocale, loading } = useAppState();
|
|
||||||
const currentKey = [keys.find((key) => key === location.pathname)];
|
|
||||||
const haveInvoicedItems = !isEmpty(invoicedItems);
|
|
||||||
const invoicedItemsLength = invoicedItems.length;
|
|
||||||
|
|
||||||
const onChangeLocale = (value) => {
|
|
||||||
setLocaleToLS(value);
|
|
||||||
changeLocaleDefaults(value);
|
|
||||||
setLocale(value);
|
|
||||||
history.go(RELOAD_LOCATION_NUMBER);
|
|
||||||
};
|
|
||||||
const localeElements = AVAILABLE_LOCALES.filter((localeName) => localeName !== locale).map(
|
|
||||||
(curLocale) => (
|
|
||||||
<Menu.Item key={curLocale} onClick={() => onChangeLocale(curLocale)}>
|
|
||||||
{curLocale}
|
|
||||||
</Menu.Item>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onUserLogout = () => {
|
|
||||||
emitter.emit('UPDATE_USER', undefined);
|
|
||||||
message.warn(
|
|
||||||
'Now you are not authenticated. Log in to use full functionality',
|
|
||||||
MESSAGE_TIMEOUT
|
|
||||||
);
|
|
||||||
history.push('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'baseline',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingLeft: '15vh',
|
|
||||||
paddingRight: '15vh',
|
|
||||||
background: 'white',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Menu theme="light" mode="horizontal" style={{ width: '50%' }} selectedKeys={currentKey}>
|
|
||||||
<Menu.Item key="/" onClick={() => history.push('/')}>
|
|
||||||
Browse
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
{!!user && (
|
|
||||||
<Menu.Item key="/person" onClick={() => history.push('/person')}>
|
|
||||||
Profile
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
{requireCustomer(user) && (
|
|
||||||
<Menu.Item key="/invoices" onClick={() => history.push('/invoices')}>
|
|
||||||
Invoices
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
{requireEmployee(user) && (
|
|
||||||
<Menu.Item key="/manage" onClick={() => history.push('/manage')}>
|
|
||||||
Manages
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Menu
|
|
||||||
style={{ width: '50%', display: 'flex', justifyContent: 'flex-end' }}
|
|
||||||
theme="light"
|
|
||||||
mode="horizontal"
|
|
||||||
selectedKeys={currentKey}
|
|
||||||
>
|
|
||||||
<Menu.Item>
|
|
||||||
{loading && <Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />}
|
|
||||||
</Menu.Item>
|
|
||||||
{haveInvoicedItems && (
|
|
||||||
<Menu.Item
|
|
||||||
style={{
|
|
||||||
width: 40,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
onClick={() => history.push('/invoice')}
|
|
||||||
key="/invoice"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
size="default"
|
|
||||||
style={{ backgroundColor: '#2db7f5' }}
|
|
||||||
count={invoicedItemsLength}
|
|
||||||
>
|
|
||||||
<CreditCardOutlined style={{ fontSize: 16 }} />
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
<SubMenu title={locale}>{localeElements}</SubMenu>
|
|
||||||
{user ? (
|
|
||||||
<Menu.Item
|
|
||||||
onClick={onUserLogout}
|
|
||||||
danger
|
|
||||||
icon={<LogoutOutlined style={{ fontSize: 16 }} />}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Menu.Item
|
|
||||||
key="/login"
|
|
||||||
onClick={() => history.push('/login')}
|
|
||||||
icon={<LoginOutlined style={{ fontSize: 16 }} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Table, Button, message } from 'antd';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { invoice } from '../api/calls';
|
|
||||||
import { useErrors } from '../hooks/useErrors';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../util/constants';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: 'Name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Artist',
|
|
||||||
dataIndex: 'artist',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Album',
|
|
||||||
dataIndex: 'albumTitle',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Price',
|
|
||||||
dataIndex: 'unitPrice',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const InvoicePage = () => {
|
|
||||||
const history = useHistory();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const { user, invoicedItems, setInvoicedItems, setLoading } = useAppState();
|
|
||||||
|
|
||||||
const data = invoicedItems.map(({ ID, ...otherProps }) => ({
|
|
||||||
key: `invoiceItem${ID}`,
|
|
||||||
...otherProps,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const onBuy = () => {
|
|
||||||
setLoading(true);
|
|
||||||
invoice(
|
|
||||||
invoicedItems.map(({ ID }) => ({
|
|
||||||
ID,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
setInvoicedItems([]);
|
|
||||||
message.success('Invoice successfully completed', MESSAGE_TIMEOUT);
|
|
||||||
history.push('/invoices');
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
const onCancel = () => {
|
|
||||||
setInvoicedItems([]);
|
|
||||||
history.push('/');
|
|
||||||
};
|
|
||||||
const goLogin = () => {
|
|
||||||
history.push('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ backgroundColor: 'white', padding: 10 }}>
|
|
||||||
<Table
|
|
||||||
bordered={false}
|
|
||||||
pagination={false}
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
|
||||||
size="middle"
|
|
||||||
footer={() => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
padding: 5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user ? (
|
|
||||||
<>
|
|
||||||
<Button type="primary" size="large" onClick={onBuy}>
|
|
||||||
Buy
|
|
||||||
</Button>
|
|
||||||
<Button size="large" style={{ marginLeft: 5 }} onClick={onCancel} danger>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<section>
|
|
||||||
<Button type="primary" size="large" onClick={goLogin}>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
<span> to buy selected</span>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InvoicePage;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Form, Input, Button, Checkbox, message } from 'antd';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { login } from '../api/calls';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { useErrors } from '../hooks/useErrors';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../util/constants';
|
|
||||||
import { emitter } from '../util/EventEmitter';
|
|
||||||
|
|
||||||
const layout = {
|
|
||||||
labelCol: {
|
|
||||||
span: 8,
|
|
||||||
},
|
|
||||||
wrapperCol: {
|
|
||||||
span: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const tailLayout = {
|
|
||||||
wrapperCol: {
|
|
||||||
offset: 8,
|
|
||||||
span: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const history = useHistory();
|
|
||||||
const { setLoading, setInvoicedItems } = useAppState();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
|
|
||||||
const onFinish = (values) => {
|
|
||||||
setLoading(true);
|
|
||||||
login({ email: values.email, password: values.password })
|
|
||||||
.then(({ data: user }) => {
|
|
||||||
emitter.emit('UPDATE_USER', user);
|
|
||||||
if (user.roles.includes('employee')) {
|
|
||||||
setInvoicedItems([]);
|
|
||||||
}
|
|
||||||
history.push('/');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
if (error.response && error.response.status === 401) {
|
|
||||||
form.resetFields();
|
|
||||||
message.error('Invalid credentials!', MESSAGE_TIMEOUT);
|
|
||||||
} else {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFinishFailed = (errorInfo) => {
|
|
||||||
console.log('Validation Failed:', errorInfo);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
{...layout}
|
|
||||||
name="basic"
|
|
||||||
initialValues={{
|
|
||||||
remember: true,
|
|
||||||
}}
|
|
||||||
onFinish={onFinish}
|
|
||||||
onFinishFailed={onFinishFailed}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label="Email"
|
|
||||||
name="email"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please input your email!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="Password"
|
|
||||||
name="password"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please input your password!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.Password style={{}} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item {...tailLayout} name="remember" valuePropName="checked">
|
|
||||||
<Checkbox>Remember me</Checkbox>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item {...tailLayout}>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
|
||||||
import { Form, Radio, Button, message } from 'antd';
|
|
||||||
import { TrackForm } from './manage-store/TrackForm';
|
|
||||||
import { AddArtistForm } from './manage-store/AddArtistForm';
|
|
||||||
import { AddAlbumForm } from './manage-store/AddAlbumForm';
|
|
||||||
import { useErrors } from '../hooks/useErrors';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { addTrack, addArtist, addAlbum } from '../api/calls';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../util/constants';
|
|
||||||
|
|
||||||
const FORM_TYPES = {
|
|
||||||
track: 'track',
|
|
||||||
artist: 'artist',
|
|
||||||
album: 'album',
|
|
||||||
playlist: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const chooseForm = (type) => {
|
|
||||||
return (
|
|
||||||
(type === 'track' && <TrackForm />) ||
|
|
||||||
(type === 'artist' && <AddArtistForm />) ||
|
|
||||||
(type === 'album' && <AddAlbumForm />)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ManageStore = () => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const { setLoading } = useAppState();
|
|
||||||
const [formType, setFormType] = useState('track');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.resetFields();
|
|
||||||
}, [formType]);
|
|
||||||
|
|
||||||
const formElement = useMemo(() => {
|
|
||||||
return chooseForm(formType);
|
|
||||||
}, [formType]);
|
|
||||||
|
|
||||||
const onChangeForm = (event) => {
|
|
||||||
setFormType(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendCreateRequest = ({ type, ...data }) => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
let promise;
|
|
||||||
switch (type) {
|
|
||||||
case FORM_TYPES.track:
|
|
||||||
promise = addTrack({
|
|
||||||
name: data.name,
|
|
||||||
composer: data.composer,
|
|
||||||
album: { ID: data.albumID },
|
|
||||||
genre: { ID: data.genreID },
|
|
||||||
unitPrice: data.unitPrice.toString(),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case FORM_TYPES.artist:
|
|
||||||
promise = addArtist(data);
|
|
||||||
break;
|
|
||||||
case FORM_TYPES.album:
|
|
||||||
promise = addAlbum({ title: data.name, artist: { ID: data.artistID } });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
promise
|
|
||||||
.then(() => {
|
|
||||||
message.success('Entity successfully created', MESSAGE_TIMEOUT);
|
|
||||||
form.resetFields();
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
style={{ width: 700 }}
|
|
||||||
form={form}
|
|
||||||
labelCol={{
|
|
||||||
span: 4,
|
|
||||||
}}
|
|
||||||
wrapperCol={{
|
|
||||||
span: 14,
|
|
||||||
}}
|
|
||||||
layout="horizontal"
|
|
||||||
initialValues={{
|
|
||||||
type: formType,
|
|
||||||
}}
|
|
||||||
type={formType}
|
|
||||||
onFinish={sendCreateRequest}
|
|
||||||
onFinishFailed={() => console.log('Not valid params provided')}
|
|
||||||
>
|
|
||||||
<Form.Item label="Entity" name="type">
|
|
||||||
<Radio.Group onChange={onChangeForm}>
|
|
||||||
<Radio.Button value="track">Track</Radio.Button>
|
|
||||||
<Radio.Button value="album">Album</Radio.Button>
|
|
||||||
<Radio.Button value="artist">Artist</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
{formElement}
|
|
||||||
<Form.Item
|
|
||||||
type="primary"
|
|
||||||
wrapperCol={{
|
|
||||||
span: 14,
|
|
||||||
offset: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button onClick={() => form.submit()}>Create</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ManageStore;
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button, message, Tag, Collapse, Table, Spin } from 'antd';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { useErrors } from '../hooks/useErrors';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { cancelInvoice, fetchInvoices } from '../api/calls';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../util/constants';
|
|
||||||
|
|
||||||
const { Panel } = Collapse;
|
|
||||||
const INVOICE_STATUS = {
|
|
||||||
2: {
|
|
||||||
tagTitle: 'Shipped',
|
|
||||||
color: 'green',
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
tagTitle: 'Submitted',
|
|
||||||
color: 'processing',
|
|
||||||
canCancel: true,
|
|
||||||
},
|
|
||||||
'-1': {
|
|
||||||
tagTitle: 'Cancelled',
|
|
||||||
color: 'default',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const CANCELLED_STATUS = -1;
|
|
||||||
const DATE_TIME_FORMAT_PATTERN = 'LLLL';
|
|
||||||
const UTC_DATE_TIME_FORMAT = 'YYYY-MM-DDThh:mm:ss';
|
|
||||||
const INVOICE_ITEMS_COLUMNS = [
|
|
||||||
{
|
|
||||||
title: 'Track name',
|
|
||||||
dataIndex: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Artist',
|
|
||||||
dataIndex: 'artistName',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Album',
|
|
||||||
dataIndex: 'albumTitle',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Price',
|
|
||||||
dataIndex: 'unitPrice',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const LEVERAGE_DURATION = 1; // in hours
|
|
||||||
const STATUSES = { submitted: 1, shipped: 2, canceled: -1 };
|
|
||||||
|
|
||||||
const isLeverageTimeExpired = (utcNowTimestamp, invoiceDate) => {
|
|
||||||
const duration = moment.duration(moment(utcNowTimestamp).diff(moment(invoiceDate).valueOf()));
|
|
||||||
return duration.asHours() > LEVERAGE_DURATION;
|
|
||||||
};
|
|
||||||
|
|
||||||
const chooseStatus = (utcNowTimestamp, invoiceDate, statusFromDb) => {
|
|
||||||
if (isLeverageTimeExpired(utcNowTimestamp, invoiceDate) && statusFromDb !== STATUSES.canceled) {
|
|
||||||
return INVOICE_STATUS[STATUSES.shipped];
|
|
||||||
}
|
|
||||||
return INVOICE_STATUS[statusFromDb];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExtraHeader = ({ ID, invoiceDate, status: initialStatus }) => {
|
|
||||||
const { loading, setLoading } = useAppState();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const [loadingHeaderId, setLoadingHeaderId] = useState();
|
|
||||||
const [status, setStatus] = useState(initialStatus);
|
|
||||||
|
|
||||||
const statusConfig = useMemo(() => {
|
|
||||||
const utcNowTimestamp = moment(moment().utc().format(UTC_DATE_TIME_FORMAT)).valueOf();
|
|
||||||
return chooseStatus(utcNowTimestamp, invoiceDate, status);
|
|
||||||
}, [status]);
|
|
||||||
|
|
||||||
const onCancelInvoice = (event, ID) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
setLoading(true);
|
|
||||||
setLoadingHeaderId(ID);
|
|
||||||
cancelInvoice(ID)
|
|
||||||
.then(() => {
|
|
||||||
message.success('Invoice successfully cancelled', MESSAGE_TIMEOUT);
|
|
||||||
setLoadingHeaderId(undefined);
|
|
||||||
setStatus(CANCELLED_STATUS);
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading && loadingHeaderId === ID}>
|
|
||||||
<Tag color={statusConfig.color}>{statusConfig.tagTitle}</Tag>
|
|
||||||
{statusConfig.canCancel && (
|
|
||||||
<Button onClick={(event) => onCancelInvoice(event, ID)} size="small" danger>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Spin>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ExtraHeader.propTypes = {
|
|
||||||
ID: PropTypes.number.isRequired,
|
|
||||||
status: PropTypes.number.isRequired,
|
|
||||||
invoiceDate: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MyInvoicesPage = () => {
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const { setLoading } = useAppState();
|
|
||||||
const [invoices, setInvoices] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
fetchInvoices()
|
|
||||||
.then(({ data: { value } }) => setInvoices(value))
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const genExtra = useCallback(
|
|
||||||
(ID, status, invoiceDate) => <ExtraHeader ID={ID} status={status} invoiceDate={invoiceDate} />,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const invoiceElements = useMemo(() => {
|
|
||||||
return invoices.map(({ ID, status, invoiceDate, total, invoiceItems }) => {
|
|
||||||
const invoiceItemsData = invoiceItems.map(
|
|
||||||
({
|
|
||||||
ID,
|
|
||||||
track: {
|
|
||||||
name,
|
|
||||||
unitPrice,
|
|
||||||
album: {
|
|
||||||
title: albumTitle,
|
|
||||||
artist: { name: artistName },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}) => ({
|
|
||||||
key: ID,
|
|
||||||
ID,
|
|
||||||
name,
|
|
||||||
unitPrice,
|
|
||||||
albumTitle,
|
|
||||||
artistName,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel
|
|
||||||
header={moment(invoiceDate).format(DATE_TIME_FORMAT_PATTERN)}
|
|
||||||
key={ID}
|
|
||||||
extra={genExtra(ID, status, invoiceDate)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
bordered={false}
|
|
||||||
pagination={false}
|
|
||||||
columns={INVOICE_ITEMS_COLUMNS}
|
|
||||||
dataSource={invoiceItemsData}
|
|
||||||
size="middle"
|
|
||||||
footer={() => <span style={{ fontWeight: 600 }}>{`Total price: ${total}`}</span>}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [invoices]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>{invoiceElements && <Collapse expandIconPosition="left">{invoiceElements}</Collapse>}</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MyInvoicesPage;
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Form, Button, message, Input } from 'antd';
|
|
||||||
import { omit, map } from 'lodash';
|
|
||||||
import { fetchPerson, confirmPerson } from '../api/calls';
|
|
||||||
import { useErrors } from '../hooks/useErrors';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../util/constants';
|
|
||||||
import { useAbortableEffect } from '../hooks/useAbortableEffect';
|
|
||||||
|
|
||||||
const PERSON_PROP = {
|
|
||||||
address: 'Address ',
|
|
||||||
city: 'City ',
|
|
||||||
country: 'Country ',
|
|
||||||
fax: 'Fax: ',
|
|
||||||
firstName: 'First name: ',
|
|
||||||
lastName: 'Last name: ',
|
|
||||||
phone: 'Phone: ',
|
|
||||||
postalCode: 'Postal code: ',
|
|
||||||
state: 'State',
|
|
||||||
email: 'email',
|
|
||||||
company: 'Company: ',
|
|
||||||
};
|
|
||||||
|
|
||||||
const PersonPage = () => {
|
|
||||||
const { setLoading } = useAppState();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [person, setPerson] = useState({
|
|
||||||
lastName: '',
|
|
||||||
firstName: '',
|
|
||||||
city: '',
|
|
||||||
state: '',
|
|
||||||
address: '',
|
|
||||||
country: '',
|
|
||||||
phone: '',
|
|
||||||
postalCode: '',
|
|
||||||
fax: '',
|
|
||||||
email: '',
|
|
||||||
company: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
useAbortableEffect((status) => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
fetchPerson()
|
|
||||||
.then(({ data }) => {
|
|
||||||
const personData = omit(data, '@odata.context', 'ID');
|
|
||||||
if (!status.aborted) {
|
|
||||||
setPerson(personData);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onConfirmChanges = (newPerson) => {
|
|
||||||
setLoading(true);
|
|
||||||
confirmPerson(newPerson)
|
|
||||||
.then(() => {
|
|
||||||
message.success('Person successfully updated', MESSAGE_TIMEOUT);
|
|
||||||
})
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const personProperties = map(Object.keys(person), (currentKey) => (
|
|
||||||
<div key={currentKey}>
|
|
||||||
<Form.Item label={PERSON_PROP[currentKey]} name={currentKey}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{person.lastName !== '' && (
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
labelCol={{
|
|
||||||
span: 4,
|
|
||||||
}}
|
|
||||||
wrapperCol={{
|
|
||||||
span: 14,
|
|
||||||
}}
|
|
||||||
layout="horizontal"
|
|
||||||
onFinish={onConfirmChanges}
|
|
||||||
onFinishFailed={() => console.log('Not valid params provided')}
|
|
||||||
initialValues={{
|
|
||||||
...person,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{personProperties}
|
|
||||||
<Form.Item
|
|
||||||
type="primary"
|
|
||||||
wrapperCol={{
|
|
||||||
span: 14,
|
|
||||||
offset: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button onClick={() => form.submit()}>Confirm changes</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PersonPage;
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import TracksContainer from './TracksPage';
|
|
||||||
import Header from './Header';
|
|
||||||
import PersonPage from './PersonPage';
|
|
||||||
import ErrorPage from './ErrorPage';
|
|
||||||
import InvoicePage from './InvoicePage';
|
|
||||||
import ManageStore from './ManageStore';
|
|
||||||
import MyInvoicesPage from './MyInvoicesPage';
|
|
||||||
import Login from './Login';
|
|
||||||
import { withRestrictions } from '../hocs/withRestrictions';
|
|
||||||
import { requireEmployee } from '../util/constants';
|
|
||||||
|
|
||||||
const RestrictedLogin = withRestrictions(Login, ({ user }) => !user);
|
|
||||||
const RestrictedInvoicePage = withRestrictions(
|
|
||||||
InvoicePage,
|
|
||||||
({ user, invoicedItems }) => !requireEmployee(user) && !isEmpty(invoicedItems)
|
|
||||||
);
|
|
||||||
const RestrictedPersonPage = withRestrictions(PersonPage, ({ user }) => !!user);
|
|
||||||
const RestrictedManageStore = withRestrictions(ManageStore, ({ user }) => requireEmployee(user));
|
|
||||||
|
|
||||||
const MyRouter = () => {
|
|
||||||
return (
|
|
||||||
<Router>
|
|
||||||
<Header />
|
|
||||||
<div style={{ padding: '2em 20vh' }}>
|
|
||||||
<React.Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path={['/index.html', '/tracks', '/']}>
|
|
||||||
<TracksContainer />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/person">
|
|
||||||
<RestrictedPersonPage />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/login">
|
|
||||||
<RestrictedLogin />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/invoice">
|
|
||||||
<RestrictedInvoicePage />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/invoices">
|
|
||||||
<MyInvoicesPage />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/manage">
|
|
||||||
<RestrictedManageStore />
|
|
||||||
</Route>
|
|
||||||
<Route path="/">
|
|
||||||
<ErrorPage />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</React.Suspense>
|
|
||||||
</div>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { MyRouter };
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
.ant-select > div.ant-select-selector {
|
|
||||||
padding: 5px;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { Input, Col, Row, Select, Pagination } from 'antd';
|
|
||||||
import { Track } from './tracks/Track';
|
|
||||||
import { ManagedTrack } from './tracks/ManagedTrack';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
import { useErrors } from '../hooks/useErrors';
|
|
||||||
import { fetchTacks, countTracks, fetchGenres } from '../api/calls';
|
|
||||||
import { useAbortableEffect } from '../hooks/useAbortableEffect';
|
|
||||||
import { requireEmployee } from '../util/constants';
|
|
||||||
import './TracksPage.css';
|
|
||||||
|
|
||||||
const { Search } = Input;
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
const DEBOUNCE_TIMER = 500;
|
|
||||||
const DEBOUNCE_OPTIONS = {
|
|
||||||
leading: true,
|
|
||||||
trailing: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderGenres = (genres) =>
|
|
||||||
genres.map(({ ID, name }) => (
|
|
||||||
<Option key={ID} value={ID.toString()}>
|
|
||||||
{name}
|
|
||||||
</Option>
|
|
||||||
));
|
|
||||||
|
|
||||||
const TracksContainer = () => {
|
|
||||||
const { setLoading, user } = useAppState();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const [state, setState] = useState({
|
|
||||||
tracks: [],
|
|
||||||
genres: [],
|
|
||||||
pagination: {
|
|
||||||
currentPage: 1,
|
|
||||||
totalItems: 0,
|
|
||||||
pageSize: 20,
|
|
||||||
},
|
|
||||||
searchOptions: {
|
|
||||||
substr: '',
|
|
||||||
genreIds: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useAbortableEffect((status) => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const countTracksReq = countTracks();
|
|
||||||
const getTracksRequest = fetchTacks();
|
|
||||||
const getGenresReq = fetchGenres();
|
|
||||||
|
|
||||||
Promise.all([countTracksReq, getTracksRequest, getGenresReq])
|
|
||||||
.then(
|
|
||||||
([
|
|
||||||
{ data: totalItems },
|
|
||||||
{
|
|
||||||
data: { value: tracks },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: { value: genres },
|
|
||||||
},
|
|
||||||
]) => {
|
|
||||||
if (!status.aborted) {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
tracks,
|
|
||||||
genres,
|
|
||||||
pagination: { ...state.pagination, totalItems },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSearch = debounce(
|
|
||||||
() => {
|
|
||||||
setLoading(true);
|
|
||||||
const options = {
|
|
||||||
$top: state.pagination.pageSize,
|
|
||||||
substr: state.searchOptions.substr.replace(`'`, `''`),
|
|
||||||
genreIds: state.searchOptions.genreIds,
|
|
||||||
};
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
fetchTacks(options),
|
|
||||||
countTracks({
|
|
||||||
substr: options.substr,
|
|
||||||
genreIds: options.genreIds,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
.then(([{ data: { value: tracks } }, { data: totalItems }]) =>
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
tracks,
|
|
||||||
pagination: { ...state.pagination, totalItems },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
},
|
|
||||||
DEBOUNCE_TIMER,
|
|
||||||
DEBOUNCE_OPTIONS
|
|
||||||
);
|
|
||||||
const onSelectChange = (genres) => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
searchOptions: {
|
|
||||||
...state.searchOptions,
|
|
||||||
genreIds: genres.map((value) => parseInt(value, 10)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onSearchChange = (event) => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
searchOptions: { ...state.searchOptions, substr: event.target.value },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onChangePage = (pageNumber) => {
|
|
||||||
document.querySelector('section.ant-layout').scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
$top: state.pagination.pageSize,
|
|
||||||
substr: state.searchOptions.substr,
|
|
||||||
genreIds: state.searchOptions.genreIds,
|
|
||||||
$skip: (pageNumber - 1) * state.pagination.pageSize,
|
|
||||||
};
|
|
||||||
fetchTacks(options)
|
|
||||||
.then((response) =>
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
tracks: response.data.value,
|
|
||||||
pagination: { ...state.pagination, currentPage: pageNumber },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
const deleteTrack = (ID) => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
tracks: state.tracks.filter(({ ID: curID }) => curID !== ID),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const renderTracks = (tracks) => {
|
|
||||||
const isEmployee = requireEmployee(user);
|
|
||||||
const TrackComponent = isEmployee ? ManagedTrack : Track;
|
|
||||||
return tracks.map((track) => {
|
|
||||||
const isAlreadyOrdered = !isEmployee && track.alreadyOrdered;
|
|
||||||
const onDeleteTrack = isEmployee && ((ID) => deleteTrack(ID));
|
|
||||||
return (
|
|
||||||
<Col key={`track-col${track.ID}`} className="gutter-row" span={8}>
|
|
||||||
<TrackComponent
|
|
||||||
initialTrack={track}
|
|
||||||
onDeleteTrack={onDeleteTrack}
|
|
||||||
isAlreadyOrdered={isAlreadyOrdered}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackElements = renderTracks(state.tracks);
|
|
||||||
const genreElements = renderGenres(state.genres);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'start',
|
|
||||||
maxWidth: 600,
|
|
||||||
paddingBottom: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
allowClear
|
|
||||||
style={{ marginRight: 10, borderRadius: 6 }}
|
|
||||||
placeholder="Genres"
|
|
||||||
onChange={(value) => onSelectChange(value)}
|
|
||||||
>
|
|
||||||
{genreElements}
|
|
||||||
</Select>
|
|
||||||
<Search
|
|
||||||
style={{
|
|
||||||
borderRadius: 6,
|
|
||||||
}}
|
|
||||||
placeholder="Search tracks"
|
|
||||||
size="large"
|
|
||||||
onSearch={onSearch}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Row gutter={[{ xs: 8, sm: 16, md: 24, lg: 32 }, 24]}>{trackElements}</Row>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Pagination
|
|
||||||
showSizeChanger={false}
|
|
||||||
defaultCurrent={1}
|
|
||||||
total={state.pagination.totalItems}
|
|
||||||
pageSize={state.pagination.pageSize}
|
|
||||||
onChange={onChangePage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TracksContainer;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Form, Input, Select } from 'antd';
|
|
||||||
import { useSearch } from '@umijs/hooks';
|
|
||||||
import { useErrors } from '../../hooks/useErrors';
|
|
||||||
import { fetchArtistsByName } from '../../api/calls';
|
|
||||||
|
|
||||||
const REQUIRED = [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'This filed is required!',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const ARTISTS_LIMIT = 10;
|
|
||||||
|
|
||||||
const getArtists = function (value) {
|
|
||||||
return fetchArtistsByName(value, ARTISTS_LIMIT)
|
|
||||||
.then((response) => response.data.value)
|
|
||||||
.catch(this.handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddAlbumForm = () => {
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const {
|
|
||||||
data: artists,
|
|
||||||
loading: isArtistsLoading,
|
|
||||||
onChange: onChangeArtistInput,
|
|
||||||
cancel: onArtistCancel,
|
|
||||||
} = useSearch(getArtists.bind({ handleError }));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onChangeArtistInput();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>Add album</h3>
|
|
||||||
<Form.Item label="Name" name="name" rules={REQUIRED}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Artist" name="artistID" rules={REQUIRED}>
|
|
||||||
<Select
|
|
||||||
showSearch
|
|
||||||
placeholder="Select artist"
|
|
||||||
filterOption={false}
|
|
||||||
onSearch={onChangeArtistInput}
|
|
||||||
loading={isArtistsLoading}
|
|
||||||
onBlur={onArtistCancel}
|
|
||||||
style={{ width: 300 }}
|
|
||||||
>
|
|
||||||
{artists &&
|
|
||||||
artists.map((artist) => (
|
|
||||||
<Select.Option key={artist.name} value={artist.ID}>
|
|
||||||
{artist.name}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { AddAlbumForm };
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Form, Input } from 'antd';
|
|
||||||
|
|
||||||
const REQUIRED = [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'This filed is required!',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const AddArtistForm = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3>Add artist</h3>
|
|
||||||
<Form.Item label="Name" name="name" rules={REQUIRED}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { AddArtistForm };
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Form, Input, Select, InputNumber } from 'antd';
|
|
||||||
import { head } from 'lodash';
|
|
||||||
import { useSearch } from '@umijs/hooks';
|
|
||||||
import { useAppState } from '../../hooks/useAppState';
|
|
||||||
import { fetchAlbumsByName, fetchGenres } from '../../api/calls';
|
|
||||||
import { useErrors } from '../../hooks/useErrors';
|
|
||||||
|
|
||||||
const ALBUMS_LIMIT = 10;
|
|
||||||
const REQUIRED = [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'This filed is required!',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function getAlbums(value) {
|
|
||||||
return fetchAlbumsByName(value, ALBUMS_LIMIT)
|
|
||||||
.then((response) => response.data.value)
|
|
||||||
.catch(this.handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TrackForm = ({ initialAlbumTitle }) => {
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
const {
|
|
||||||
data: albums,
|
|
||||||
loading: isAlbumsLoading,
|
|
||||||
onChange: onChangeAlbumInput,
|
|
||||||
cancel: onAlbumCancel,
|
|
||||||
} = useSearch(getAlbums.bind({ handleError }));
|
|
||||||
const { setLoading } = useAppState();
|
|
||||||
const [genres, setGenres] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
Promise.all([fetchGenres(), onChangeAlbumInput(initialAlbumTitle)])
|
|
||||||
.then((responses) => setGenres(head(responses).data.value))
|
|
||||||
.catch(handleError)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item label="Name" name="name" rules={REQUIRED}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Composer" name="composer" rules={REQUIRED}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Album" name="albumID" rules={REQUIRED}>
|
|
||||||
<Select
|
|
||||||
showSearch
|
|
||||||
placeholder="Select album"
|
|
||||||
filterOption={false}
|
|
||||||
onSearch={onChangeAlbumInput}
|
|
||||||
loading={isAlbumsLoading}
|
|
||||||
onBlur={onAlbumCancel}
|
|
||||||
>
|
|
||||||
{albums &&
|
|
||||||
albums.map((album) => (
|
|
||||||
<Select.Option key={album.title} value={album.ID}>
|
|
||||||
{album.title}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Genre" name="genreID" rules={REQUIRED}>
|
|
||||||
<Select showSearch placeholder="Select genre" filterOption={false}>
|
|
||||||
{genres &&
|
|
||||||
genres.map((genre) => (
|
|
||||||
<Select.Option key={genre.name} value={genre.ID}>
|
|
||||||
{genre.name}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Unit price" name="unitPrice" precision={2} rules={REQUIRED}>
|
|
||||||
<InputNumber
|
|
||||||
precision={2}
|
|
||||||
decimalSeparator="."
|
|
||||||
parser={(value) => value.replace(/\$\s?|(,*)/g, '')}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TrackForm.propTypes = {
|
|
||||||
initialAlbumTitle: PropTypes.string,
|
|
||||||
};
|
|
||||||
TrackForm.defaultProps = {
|
|
||||||
initialAlbumTitle: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { TrackForm };
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Modal, message } from 'antd';
|
|
||||||
import { DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { deleteTrack } from '../../api/calls';
|
|
||||||
import { useErrors } from '../../hooks/useErrors';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../../util/constants';
|
|
||||||
|
|
||||||
const DeleteAction = ({ ID, onDeleteTrack }) => {
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
|
|
||||||
const onOk = () => {
|
|
||||||
setModalVisible(false);
|
|
||||||
deleteTrack(ID)
|
|
||||||
.then(() => {
|
|
||||||
onDeleteTrack();
|
|
||||||
setModalVisible(false);
|
|
||||||
message.success('Track successfully deleted!', MESSAGE_TIMEOUT);
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCancel = () => setModalVisible(false);
|
|
||||||
const onOpenModal = () => {
|
|
||||||
setModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DeleteOutlined onClick={onOpenModal}>Delete</DeleteOutlined>
|
|
||||||
<Modal title="Confirm" visible={modalVisible} onOk={onOk} onCancel={onCancel}>
|
|
||||||
<p>Are You really want to delete this track?</p>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DeleteAction.propTypes = {
|
|
||||||
ID: PropTypes.number.isRequired,
|
|
||||||
onDeleteTrack: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DeleteAction };
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button, Modal, Form, message } from 'antd';
|
|
||||||
import { EditOutlined, LoadingOutlined } from '@ant-design/icons';
|
|
||||||
import { useErrors } from '../../hooks/useErrors';
|
|
||||||
import { TrackForm } from '../manage-store/TrackForm';
|
|
||||||
import { updateTrack, getTrack } from '../../api/calls';
|
|
||||||
import { MESSAGE_TIMEOUT } from '../../util/constants';
|
|
||||||
|
|
||||||
const EditAction = ({ ID, name, composer, genre, unitPrice, album, afterTrackUpdate }) => {
|
|
||||||
const [visible, setVisible] = React.useState(false);
|
|
||||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
|
||||||
const [updateLoading, setUpdateLoading] = React.useState(false);
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const { handleError } = useErrors();
|
|
||||||
|
|
||||||
const onShowModal = () => {
|
|
||||||
setVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const afterCloseModal = () => {
|
|
||||||
setUpdateLoading(true);
|
|
||||||
getTrack(ID)
|
|
||||||
.then((response) => {
|
|
||||||
afterTrackUpdate(response.data);
|
|
||||||
setUpdateLoading(false);
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFinish = (value) => {
|
|
||||||
setConfirmLoading(true);
|
|
||||||
updateTrack({
|
|
||||||
ID,
|
|
||||||
name: value.name,
|
|
||||||
composer: value.composer,
|
|
||||||
album: { ID: value.albumID },
|
|
||||||
genre: { ID: value.genreID },
|
|
||||||
unitPrice: value.unitPrice.toString(),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
message.success('Track successfully updated!', MESSAGE_TIMEOUT);
|
|
||||||
setConfirmLoading(false);
|
|
||||||
setVisible(false);
|
|
||||||
afterCloseModal();
|
|
||||||
})
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOk = () => {
|
|
||||||
form.submit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{updateLoading ? <LoadingOutlined /> : <EditOutlined onClick={onShowModal} />}
|
|
||||||
<Modal
|
|
||||||
title="Edit track"
|
|
||||||
visible={visible}
|
|
||||||
confirmLoading={confirmLoading}
|
|
||||||
onOk={handleOk}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
width={600}
|
|
||||||
footer={[
|
|
||||||
<Button key="back" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>,
|
|
||||||
<Button key="submit" type="primary" loading={confirmLoading} onClick={handleOk}>
|
|
||||||
Submit
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
labelCol={{
|
|
||||||
span: 4,
|
|
||||||
}}
|
|
||||||
wrapperCol={{
|
|
||||||
span: 14,
|
|
||||||
}}
|
|
||||||
layout="horizontal"
|
|
||||||
onFinish={onFinish}
|
|
||||||
onFinishFailed={() => console.log('Not valid params provided')}
|
|
||||||
initialValues={{
|
|
||||||
name,
|
|
||||||
composer,
|
|
||||||
genreID: genre.ID,
|
|
||||||
albumID: album.ID,
|
|
||||||
unitPrice,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrackForm initialAlbumTitle={album.title} />
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EditAction.propTypes = {
|
|
||||||
ID: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
composer: PropTypes.string.isRequired,
|
|
||||||
genre: PropTypes.object.isRequired,
|
|
||||||
unitPrice: PropTypes.number.isRequired,
|
|
||||||
album: PropTypes.object.isRequired,
|
|
||||||
afterTrackUpdate: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { EditAction };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
span > span.anticon.anticon-delete:hover {
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-element {
|
|
||||||
transition: opacity 0.5s ease-in-out;
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import { Card } from 'antd';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { EditAction } from './EditAction';
|
|
||||||
import { DeleteAction } from './DeleteAction';
|
|
||||||
import { TrackCardBody } from './TrackCardBody';
|
|
||||||
import './ManagedTrack.css';
|
|
||||||
|
|
||||||
const ManagedTrack = ({ initialTrack, onDeleteTrack }) => {
|
|
||||||
const trackElement = useRef();
|
|
||||||
const [track, setTrack] = useState(initialTrack);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card-element" ref={trackElement}>
|
|
||||||
<Card
|
|
||||||
actions={[
|
|
||||||
<DeleteAction
|
|
||||||
ID={track.ID}
|
|
||||||
onDeleteTrack={() => {
|
|
||||||
trackElement.current.style.opacity = 0;
|
|
||||||
setTimeout(() => onDeleteTrack(track.ID), 500);
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
<EditAction
|
|
||||||
ID={track.ID}
|
|
||||||
name={track.name}
|
|
||||||
composer={track.composer}
|
|
||||||
album={track.album}
|
|
||||||
genre={track.genre}
|
|
||||||
unitPrice={track.unitPrice}
|
|
||||||
afterTrackUpdate={(value) => setTrack(value)}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
title={track.name}
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<TrackCardBody track={track} />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ManagedTrack.propTypes = {
|
|
||||||
initialTrack: PropTypes.object.isRequired,
|
|
||||||
onDeleteTrack: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ManagedTrack };
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Card, Button } from 'antd';
|
|
||||||
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
|
|
||||||
import { useAppState } from '../../hooks/useAppState';
|
|
||||||
import { TrackCardBody } from './TrackCardBody';
|
|
||||||
|
|
||||||
const Track = ({ initialTrack, isAlreadyOrdered }) => {
|
|
||||||
const trackElement = useRef();
|
|
||||||
const { setInvoicedItems, invoicedItems } = useAppState();
|
|
||||||
const [isJustInvoiced, setIsJustInvoiced] = useState(
|
|
||||||
invoicedItems.find((curTrack) => curTrack.ID === initialTrack.ID)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangedStatus = () => {
|
|
||||||
const newIsJustInvoiced = !isJustInvoiced;
|
|
||||||
if (newIsJustInvoiced) {
|
|
||||||
setInvoicedItems([
|
|
||||||
...invoicedItems,
|
|
||||||
{
|
|
||||||
ID: initialTrack.ID,
|
|
||||||
name: initialTrack.name,
|
|
||||||
artist: initialTrack.album.artist.name,
|
|
||||||
albumTitle: initialTrack.album.title,
|
|
||||||
unitPrice: initialTrack.unitPrice,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setInvoicedItems(invoicedItems.filter(({ ID: curID }) => curID !== initialTrack.ID));
|
|
||||||
}
|
|
||||||
setIsJustInvoiced(newIsJustInvoiced);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card-element" ref={trackElement}>
|
|
||||||
<Card
|
|
||||||
actions={[
|
|
||||||
<>
|
|
||||||
{!isAlreadyOrdered && (
|
|
||||||
<Button onClick={onChangedStatus} danger={isJustInvoiced}>
|
|
||||||
{isJustInvoiced ? <MinusOutlined /> : <PlusOutlined />}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>,
|
|
||||||
]}
|
|
||||||
title={initialTrack.name}
|
|
||||||
bordered={false}
|
|
||||||
>
|
|
||||||
<TrackCardBody track={initialTrack} />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Track.propTypes = {
|
|
||||||
initialTrack: PropTypes.object.isRequired,
|
|
||||||
isAlreadyOrdered: PropTypes.bool,
|
|
||||||
};
|
|
||||||
Track.defaultProps = {
|
|
||||||
isAlreadyOrdered: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Track };
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const TrackCardBody = ({ track }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
Artist:
|
|
||||||
<span style={{ fontWeight: 600 }}>{track.album.artist.name}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Album:
|
|
||||||
<span style={{ fontWeight: 600 }}>{track.album.title}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Genre:
|
|
||||||
<span style={{ fontWeight: 600 }}>{track.genre.name}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{track.composer && (
|
|
||||||
<span>
|
|
||||||
Compositor:
|
|
||||||
<span style={{ fontWeight: 600 }}>{track.composer}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
||||||
<span>
|
|
||||||
Price:
|
|
||||||
<span style={{ fontWeight: 600 }}>{track.unitPrice}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TrackCardBody.propTypes = {
|
|
||||||
track: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { TrackCardBody };
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import React, { useMemo, createContext, useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { getUserFromLS, getLocaleFromLS, setUserToLS } from '../util/localStorageService';
|
|
||||||
import { changeUserDefaults } from '../api/axiosInstance';
|
|
||||||
import { emitter } from '../util/EventEmitter';
|
|
||||||
|
|
||||||
const globalContext = {
|
|
||||||
error: {},
|
|
||||||
loading: true,
|
|
||||||
user: {
|
|
||||||
ID: undefined,
|
|
||||||
roles: [],
|
|
||||||
email: undefined,
|
|
||||||
accessToken: undefined,
|
|
||||||
refreshToken: undefined,
|
|
||||||
},
|
|
||||||
locale: undefined,
|
|
||||||
invoicedItems: [],
|
|
||||||
notifications: [],
|
|
||||||
};
|
|
||||||
const AppStateContext = createContext(globalContext);
|
|
||||||
|
|
||||||
const AppStateContextProvider = ({ children }) => {
|
|
||||||
const [invoicedItems, setInvoicedItems] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState({});
|
|
||||||
const [user, setUser] = useState(getUserFromLS());
|
|
||||||
const [locale, setLocale] = useState(getLocaleFromLS());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateUser = (newUser) => {
|
|
||||||
console.log('USER_UPDATE WAS TRIGGERED');
|
|
||||||
changeUserDefaults(newUser);
|
|
||||||
setUserToLS(newUser);
|
|
||||||
setUser(newUser);
|
|
||||||
};
|
|
||||||
emitter.on('UPDATE_USER', updateUser);
|
|
||||||
return () => {
|
|
||||||
emitter.removeListener('UPDATE_USER', updateUser);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
invoicedItems,
|
|
||||||
user,
|
|
||||||
locale,
|
|
||||||
setLoading,
|
|
||||||
setError,
|
|
||||||
setInvoicedItems,
|
|
||||||
setUser,
|
|
||||||
setLocale,
|
|
||||||
}),
|
|
||||||
[locale, user, loading, error, invoicedItems]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <AppStateContext.Provider value={value}>{children}</AppStateContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
AppStateContextProvider.propTypes = {
|
|
||||||
children: PropTypes.element.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { AppStateContextProvider, AppStateContext };
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Redirect } from 'react-router-dom';
|
|
||||||
import { useAppState } from '../hooks/useAppState';
|
|
||||||
|
|
||||||
const withRestrictions = (Component, isUserMeetRestrictions) => {
|
|
||||||
return (props) => {
|
|
||||||
const { user, invoicedItems } = useAppState();
|
|
||||||
return isUserMeetRestrictions({ user, invoicedItems }) ? (
|
|
||||||
<Component {...props} />
|
|
||||||
) : (
|
|
||||||
<Redirect exact to="/error" />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { withRestrictions };
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
function useAbortableEffect(effect, dependencies) {
|
|
||||||
const status = {}; // mutable status object
|
|
||||||
useEffect(() => {
|
|
||||||
status.aborted = false;
|
|
||||||
// pass the mutable object to the effect callback
|
|
||||||
// store the returned value for cleanup
|
|
||||||
const cleanUpFn = effect(status);
|
|
||||||
return () => {
|
|
||||||
// mutate the object to signal the consumer
|
|
||||||
// this effect is cleaning up
|
|
||||||
status.aborted = true;
|
|
||||||
if (typeof cleanUpFn === 'function') {
|
|
||||||
// run the cleanup function
|
|
||||||
cleanUpFn();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [...dependencies]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useAbortableEffect };
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { AppStateContext } from '../contexts/AppStateContext';
|
|
||||||
|
|
||||||
const useAppState = () => useContext(AppStateContext);
|
|
||||||
|
|
||||||
export { useAppState };
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { useAppState } from './useAppState';
|
|
||||||
|
|
||||||
const useErrors = () => {
|
|
||||||
const history = useHistory();
|
|
||||||
const { setError } = useAppState();
|
|
||||||
|
|
||||||
const handleError = (error) => {
|
|
||||||
console.error('Error', error);
|
|
||||||
|
|
||||||
if (error.response) {
|
|
||||||
const { status, statusText, data } = error.response;
|
|
||||||
setError({
|
|
||||||
status,
|
|
||||||
statusText,
|
|
||||||
message: data.error ? data.error.message : data,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setError({
|
|
||||||
status: '',
|
|
||||||
statusText: 'Error',
|
|
||||||
message: 'Something went wrong. Seems like request is too long',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
history.push('/error');
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleError,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useErrors };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
|
||||||
// import * as serviceWorker from './serviceWorker';
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
|
||||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
|
||||||
// serviceWorker.unregister();
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import EventEmitter from 'events';
|
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
|
||||||
|
|
||||||
export { emitter };
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const AVAILABLE_LOCALES = ['en', 'fr', 'de'];
|
|
||||||
|
|
||||||
export const MESSAGE_TIMEOUT = 2;
|
|
||||||
|
|
||||||
export const requireEmployee = (user) => !!user && user.roles.includes('employee');
|
|
||||||
|
|
||||||
export const requireCustomer = (user) => !!user && user.roles.includes('customer');
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { isValidUser } from './validateUser';
|
|
||||||
import { AVAILABLE_LOCALES } from './constants';
|
|
||||||
|
|
||||||
const setUserToLS = (user) => {
|
|
||||||
if (user) {
|
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserFromLS = () => {
|
|
||||||
let userFromLS;
|
|
||||||
try {
|
|
||||||
userFromLS = JSON.parse(localStorage.getItem('user'));
|
|
||||||
if (isValidUser(userFromLS)) {
|
|
||||||
return userFromLS;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('User from local storage are not valid');
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLocaleFromLS = () => {
|
|
||||||
const localeFromLS = localStorage.getItem('locale');
|
|
||||||
return localeFromLS && localeFromLS !== 'undefined' && AVAILABLE_LOCALES.includes(localeFromLS)
|
|
||||||
? localeFromLS
|
|
||||||
: 'en';
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLocaleToLS = (locale) => {
|
|
||||||
localStorage.setItem('locale', locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { setLocaleToLS, getLocaleFromLS, getUserFromLS, setUserToLS };
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { isArray, isEmpty, isString, isNumber } from 'lodash';
|
|
||||||
|
|
||||||
const CUSTOMER_ROLE = 'customer';
|
|
||||||
const EMPLOYEE_ROLE = 'employee';
|
|
||||||
|
|
||||||
const isValidUser = (user) => {
|
|
||||||
return (
|
|
||||||
!isEmpty(user) &&
|
|
||||||
isNumber(user.ID) &&
|
|
||||||
isArray(user.roles) &&
|
|
||||||
!!user.roles.some((role) => role === CUSTOMER_ROLE || role === EMPLOYEE_ROLE) &&
|
|
||||||
isString(user.email) &&
|
|
||||||
isString(user.accessToken) &&
|
|
||||||
isString(user.refreshToken)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { isValidUser };
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
|
||||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
new CleanWebpackPlugin(),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: path.join(__dirname, '../public/index.html'),
|
|
||||||
filename: path.join(__dirname, '../../app/index.html'),
|
|
||||||
publicPath: '/static/', // for js bundles path
|
|
||||||
}),
|
|
||||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
|
|
||||||
PUBLIC_URL: '',
|
|
||||||
}),
|
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
from: path.join(__dirname, '../public'),
|
|
||||||
to: path.join(__dirname, '../../app'),
|
|
||||||
globOptions: {
|
|
||||||
dot: true,
|
|
||||||
ignore: ['**/index.html'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
new webpack.ProgressPlugin(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx)$/,
|
|
||||||
exclude: /(node_modules)/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(png|jpg)$/,
|
|
||||||
use: [{ loader: 'url-loader' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|
||||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
|
||||||
const { rules } = require('./common-rules');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
entry: {
|
|
||||||
index: './src/index.jsx',
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
contentBase: './dist',
|
|
||||||
compress: true, // compress files to gzip to increase download speed
|
|
||||||
port: 3000,
|
|
||||||
disableHostCheck: false, // by default true, it is not recomended,
|
|
||||||
// because it makes app vulnerable to DNS rebinding attacks
|
|
||||||
headers: {
|
|
||||||
'X-Custom-header': 'custom', // this requires apps with authentication
|
|
||||||
// useful config obj
|
|
||||||
},
|
|
||||||
open: true, // open the browser after server had been started
|
|
||||||
hot: true, // hot module replacement
|
|
||||||
historyApiFallback: true, // needs for react-router-dom
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: path.join(__dirname, '../public/index.html'),
|
|
||||||
}),
|
|
||||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, {
|
|
||||||
PUBLIC_URL: '',
|
|
||||||
}),
|
|
||||||
new webpack.ProgressPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env.SERVICE_URL': JSON.stringify('http://localhost:4004/'),
|
|
||||||
}),
|
|
||||||
// new webpack.HotModuleReplacementPlugin(), // for hot module replacement option of devServer
|
|
||||||
],
|
|
||||||
output: {
|
|
||||||
filename: '[name].[fullhash].js',
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: { extensions: ['*', '.js', '.jsx'] },
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
app: './src/index.jsx', // Bundle with our code
|
|
||||||
react: ['react', 'react-dom'],
|
|
||||||
lodash: ['lodash'],
|
|
||||||
moment: ['moment'],
|
|
||||||
events: ['events'],
|
|
||||||
axios: ['axios'],
|
|
||||||
antd: ['antd'],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
// [name] - name of the entry (bundle),
|
|
||||||
// [checksum] or [hash] - to cache different bundles
|
|
||||||
// from update when developing (doing changes in the files)
|
|
||||||
filename: '[name].[fullhash].js',
|
|
||||||
// in this folder path bundles will be placed
|
|
||||||
path: path.resolve(__dirname, '../../app/static'),
|
|
||||||
// where you uploaded your bundled files. (Relative to server root)
|
|
||||||
// needs for react-router-dom
|
|
||||||
publicPath: '/static/',
|
|
||||||
},
|
|
||||||
resolve: { extensions: ['*', '.js', '.jsx'] },
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
const webpack = require('webpack');
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
const common = require('./webpack.common.js');
|
|
||||||
const { rules } = require('./common-rules');
|
|
||||||
const { plugins } = require('./common-plugins');
|
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
plugins: [
|
|
||||||
...plugins,
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env.SERVICE_URL': JSON.stringify('http://localhost:4004/'),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const webpack = require('webpack');
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
const common = require('./webpack.common.js');
|
|
||||||
const { rules } = require('./common-rules');
|
|
||||||
const { plugins } = require('./common-plugins');
|
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
mode: 'production',
|
|
||||||
devtool: 'source-map',
|
|
||||||
plugins: [
|
|
||||||
...plugins,
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env.SERVICE_URL': JSON.stringify('api/'),
|
|
||||||
}),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: '[name].css',
|
|
||||||
chunkFilename: '[id].css',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
// To split up js code to different bundles.
|
|
||||||
chunks: 'all', // Now bundle with our code will be cleaned up
|
|
||||||
}, // from vendors imports (2mb ~> 100kb)
|
|
||||||
minimize: true,
|
|
||||||
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], // to minimize file size
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
...rules,
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "media-store-approuter",
|
|
||||||
"description": "Approuter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/approuter": "^6.8.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "node node_modules/@sap/approuter/approuter.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"welcomeFile": "/index.html",
|
|
||||||
"authenticationMethod": "none",
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"source": "/api/(.*)",
|
|
||||||
"target": "$1",
|
|
||||||
"destination": "srv-binding",
|
|
||||||
"authenticationType": "none"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "^(.*)",
|
|
||||||
"target": "mediastore/$1",
|
|
||||||
"service": "html5-apps-repo-rt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "media-store-html5deployer",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/html5-app-deployer": "^2.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "node node_modules/@sap/html5-app-deployer/index.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"xsappname": "media-store-xsuaa",
|
|
||||||
"tenant-mode": "dedicated",
|
|
||||||
"scopes": [],
|
|
||||||
"attributes": [],
|
|
||||||
"role-templates": []
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ modules:
|
|||||||
- name: media-store-hmtl5-deployer
|
- name: media-store-hmtl5-deployer
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
type: com.sap.html5.application-content
|
type: com.sap.html5.application-content
|
||||||
path: deployers/html5Deployer
|
path: app/deployers/html5Deployer
|
||||||
requires:
|
requires:
|
||||||
- name: media-store-html5-host
|
- name: media-store-html5-host
|
||||||
build-parameters:
|
build-parameters:
|
||||||
@@ -71,7 +71,7 @@ modules:
|
|||||||
- name: media-store-approuter
|
- name: media-store-approuter
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
type: approuter.nodejs
|
type: approuter.nodejs
|
||||||
path: deployers/approuter
|
path: app/deployers/approuter
|
||||||
requires:
|
requires:
|
||||||
- name: media-store-html5-runtime
|
- name: media-store-html5-runtime
|
||||||
- name: media-store-xsuaa
|
- name: media-store-xsuaa
|
||||||
@@ -119,7 +119,7 @@ resources:
|
|||||||
- name: media-store-xsuaa
|
- name: media-store-xsuaa
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
parameters:
|
parameters:
|
||||||
path: deployers/xs-security.json
|
path: app/deployers/xs-security.json
|
||||||
service-plan: application
|
service-plan: application
|
||||||
service: xsuaa
|
service: xsuaa
|
||||||
type: org.cloudfoundry.managed-service
|
type: org.cloudfoundry.managed-service
|
||||||
|
|||||||
Reference in New Issue
Block a user