From dbe4b8a7bde5eeb7d4be6a669fa2c6ceacc6cbc3 Mon Sep 17 00:00:00 2001 From: "Dzmitry_Tamashevich@epam.com" Date: Sun, 29 Nov 2020 21:02:37 +0300 Subject: [PATCH] moving front app to subfolder. add webpack config with watch and dev-server mode. moving deploy things in subfolder --- media-store/app-src/.babelrc | 5 + media-store/app-src/.eslintrc.json | 41 ++++ media-store/app-src/.gitignore | 23 ++ media-store/app-src/.prettierrc | 4 + media-store/app-src/.vscode/launch.json | 13 ++ media-store/app-src/README.md | 1 + media-store/app-src/package.json | 63 +++++ media-store/app-src/public/index.html | 44 ++++ media-store/app-src/public/logo192.png | Bin 0 -> 5347 bytes media-store/app-src/public/logo512.png | Bin 0 -> 9664 bytes media-store/app-src/public/manifest.json | 31 +++ media-store/app-src/public/robots.txt | 3 + media-store/app-src/public/xs-app.json | 10 + media-store/app-src/src/App.css | 57 +++++ media-store/app-src/src/App.jsx | 18 ++ media-store/app-src/src/api/axiosInstance.js | 116 ++++++++++ media-store/app-src/src/api/calls.js | 167 ++++++++++++++ .../app-src/src/components/ErrorPage.jsx | 49 ++++ media-store/app-src/src/components/Header.css | 3 + media-store/app-src/src/components/Header.jsx | 139 +++++++++++ .../app-src/src/components/InvoicePage.jsx | 102 +++++++++ media-store/app-src/src/components/Login.jsx | 107 +++++++++ .../app-src/src/components/ManageStore.jsx | 115 ++++++++++ .../app-src/src/components/MyInvoicesPage.jsx | 170 ++++++++++++++ .../app-src/src/components/PersonPage.jsx | 108 +++++++++ media-store/app-src/src/components/Router.jsx | 67 ++++++ .../app-src/src/components/TracksPage.css | 4 + .../app-src/src/components/TracksPage.jsx | 215 ++++++++++++++++++ .../components/manage-store/AddAlbumForm.jsx | 62 +++++ .../components/manage-store/AddArtistForm.jsx | 22 ++ .../src/components/manage-store/TrackForm.jsx | 93 ++++++++ .../src/components/tracks/DeleteAction.jsx | 44 ++++ .../src/components/tracks/EditAction.jsx | 113 +++++++++ .../src/components/tracks/ManagedTrack.css | 7 + .../src/components/tracks/ManagedTrack.jsx | 42 ++++ .../app-src/src/components/tracks/Track.jsx | 60 +++++ .../src/components/tracks/TrackCardBody.jsx | 41 ++++ .../app-src/src/contexts/AppStateContext.jsx | 66 ++++++ .../app-src/src/hocs/withRestrictions.jsx | 16 ++ .../app-src/src/hooks/useAbortableEffect.js | 22 ++ media-store/app-src/src/hooks/useAppState.js | 6 + media-store/app-src/src/hooks/useErrors.js | 34 +++ media-store/app-src/src/index.jsx | 11 + media-store/app-src/src/logo.svg | 7 + media-store/app-src/src/serviceWorker.js | 0 media-store/app-src/src/setupTests.js | 5 + media-store/app-src/src/util/EventEmitter.js | 5 + media-store/app-src/src/util/constants.js | 7 + .../app-src/src/util/localStorageService.js | 36 +++ media-store/app-src/src/util/validateUser.js | 18 ++ media-store/app-src/webpack/common-plugins.js | 33 +++ media-store/app-src/webpack/common-rules.js | 18 ++ .../app-src/webpack/webpack-dev-server.js | 62 +++++ media-store/app-src/webpack/webpack.common.js | 25 ++ media-store/app-src/webpack/webpack.dev.js | 25 ++ media-store/app-src/webpack/webpack.prod.js | 40 ++++ media-store/app/favicon.ico | Bin 0 -> 3150 bytes media-store/app/index.html | 44 ++++ media-store/app/logo192.png | Bin 0 -> 5347 bytes media-store/app/logo512.png | Bin 0 -> 9664 bytes media-store/app/manifest.json | 31 +++ media-store/app/robots.txt | 3 + media-store/app/xs-app.json | 10 + media-store/deployers/approuter/package.json | 11 + media-store/deployers/approuter/xs-app.json | 17 ++ .../deployers/html5Deployer/package.json | 12 + media-store/srv/browse-invoices-service.js | 1 - media-store/srv/browse-tracks-service.js | 3 + media-store/srv/manage-store-service.js | 3 +- media-store/util/helpers.js | 28 +++ 70 files changed, 2755 insertions(+), 3 deletions(-) create mode 100644 media-store/app-src/.babelrc create mode 100644 media-store/app-src/.eslintrc.json create mode 100644 media-store/app-src/.gitignore create mode 100644 media-store/app-src/.prettierrc create mode 100644 media-store/app-src/.vscode/launch.json create mode 100644 media-store/app-src/README.md create mode 100644 media-store/app-src/package.json create mode 100644 media-store/app-src/public/index.html create mode 100644 media-store/app-src/public/logo192.png create mode 100644 media-store/app-src/public/logo512.png create mode 100644 media-store/app-src/public/manifest.json create mode 100644 media-store/app-src/public/robots.txt create mode 100644 media-store/app-src/public/xs-app.json create mode 100644 media-store/app-src/src/App.css create mode 100644 media-store/app-src/src/App.jsx create mode 100644 media-store/app-src/src/api/axiosInstance.js create mode 100644 media-store/app-src/src/api/calls.js create mode 100644 media-store/app-src/src/components/ErrorPage.jsx create mode 100644 media-store/app-src/src/components/Header.css create mode 100644 media-store/app-src/src/components/Header.jsx create mode 100644 media-store/app-src/src/components/InvoicePage.jsx create mode 100644 media-store/app-src/src/components/Login.jsx create mode 100644 media-store/app-src/src/components/ManageStore.jsx create mode 100644 media-store/app-src/src/components/MyInvoicesPage.jsx create mode 100644 media-store/app-src/src/components/PersonPage.jsx create mode 100644 media-store/app-src/src/components/Router.jsx create mode 100644 media-store/app-src/src/components/TracksPage.css create mode 100644 media-store/app-src/src/components/TracksPage.jsx create mode 100644 media-store/app-src/src/components/manage-store/AddAlbumForm.jsx create mode 100644 media-store/app-src/src/components/manage-store/AddArtistForm.jsx create mode 100644 media-store/app-src/src/components/manage-store/TrackForm.jsx create mode 100644 media-store/app-src/src/components/tracks/DeleteAction.jsx create mode 100644 media-store/app-src/src/components/tracks/EditAction.jsx create mode 100644 media-store/app-src/src/components/tracks/ManagedTrack.css create mode 100644 media-store/app-src/src/components/tracks/ManagedTrack.jsx create mode 100644 media-store/app-src/src/components/tracks/Track.jsx create mode 100644 media-store/app-src/src/components/tracks/TrackCardBody.jsx create mode 100644 media-store/app-src/src/contexts/AppStateContext.jsx create mode 100644 media-store/app-src/src/hocs/withRestrictions.jsx create mode 100644 media-store/app-src/src/hooks/useAbortableEffect.js create mode 100644 media-store/app-src/src/hooks/useAppState.js create mode 100644 media-store/app-src/src/hooks/useErrors.js create mode 100644 media-store/app-src/src/index.jsx create mode 100644 media-store/app-src/src/logo.svg create mode 100644 media-store/app-src/src/serviceWorker.js create mode 100644 media-store/app-src/src/setupTests.js create mode 100644 media-store/app-src/src/util/EventEmitter.js create mode 100644 media-store/app-src/src/util/constants.js create mode 100644 media-store/app-src/src/util/localStorageService.js create mode 100644 media-store/app-src/src/util/validateUser.js create mode 100644 media-store/app-src/webpack/common-plugins.js create mode 100644 media-store/app-src/webpack/common-rules.js create mode 100644 media-store/app-src/webpack/webpack-dev-server.js create mode 100644 media-store/app-src/webpack/webpack.common.js create mode 100644 media-store/app-src/webpack/webpack.dev.js create mode 100644 media-store/app-src/webpack/webpack.prod.js create mode 100644 media-store/app/favicon.ico create mode 100644 media-store/app/index.html create mode 100644 media-store/app/logo192.png create mode 100644 media-store/app/logo512.png create mode 100644 media-store/app/manifest.json create mode 100644 media-store/app/robots.txt create mode 100644 media-store/app/xs-app.json create mode 100644 media-store/deployers/approuter/package.json create mode 100644 media-store/deployers/approuter/xs-app.json create mode 100644 media-store/deployers/html5Deployer/package.json create mode 100644 media-store/util/helpers.js diff --git a/media-store/app-src/.babelrc b/media-store/app-src/.babelrc new file mode 100644 index 00000000..d3472902 --- /dev/null +++ b/media-store/app-src/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": ["@babel/preset-react", "@babel/preset-env"], + "plugins": ["@babel/plugin-transform-runtime", "babel-plugin-syntax-dynamic-import"] +} + \ No newline at end of file diff --git a/media-store/app-src/.eslintrc.json b/media-store/app-src/.eslintrc.json new file mode 100644 index 00000000..537def27 --- /dev/null +++ b/media-store/app-src/.eslintrc.json @@ -0,0 +1,41 @@ +{ + "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" + } +} diff --git a/media-store/app-src/.gitignore b/media-store/app-src/.gitignore new file mode 100644 index 00000000..4d29575d --- /dev/null +++ b/media-store/app-src/.gitignore @@ -0,0 +1,23 @@ +# 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* diff --git a/media-store/app-src/.prettierrc b/media-store/app-src/.prettierrc new file mode 100644 index 00000000..5ac85e27 --- /dev/null +++ b/media-store/app-src/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 100, + "singleQuote": true +} diff --git a/media-store/app-src/.vscode/launch.json b/media-store/app-src/.vscode/launch.json new file mode 100644 index 00000000..d4e0b5d5 --- /dev/null +++ b/media-store/app-src/.vscode/launch.json @@ -0,0 +1,13 @@ + +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Chrome", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceRoot}/src" + } + ] +} \ No newline at end of file diff --git a/media-store/app-src/README.md b/media-store/app-src/README.md new file mode 100644 index 00000000..269c0546 --- /dev/null +++ b/media-store/app-src/README.md @@ -0,0 +1 @@ +"# Media store UI" diff --git a/media-store/app-src/package.json b/media-store/app-src/package.json new file mode 100644 index 00000000..28696e9c --- /dev/null +++ b/media-store/app-src/package.json @@ -0,0 +1,63 @@ +{ + "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": "./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", + "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" + } +} diff --git a/media-store/app-src/public/index.html b/media-store/app-src/public/index.html new file mode 100644 index 00000000..e2722955 --- /dev/null +++ b/media-store/app-src/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + \ No newline at end of file diff --git a/media-store/app-src/public/logo192.png b/media-store/app-src/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/media-store/app-src/public/manifest.json b/media-store/app-src/public/manifest.json new file mode 100644 index 00000000..45979ace --- /dev/null +++ b/media-store/app-src/public/manifest.json @@ -0,0 +1,31 @@ +{ + "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" + } + } +} diff --git a/media-store/app-src/public/robots.txt b/media-store/app-src/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/media-store/app-src/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/media-store/app-src/public/xs-app.json b/media-store/app-src/public/xs-app.json new file mode 100644 index 00000000..930c40ee --- /dev/null +++ b/media-store/app-src/public/xs-app.json @@ -0,0 +1,10 @@ +{ + "welcomeFile": "/index.html", + "routes": [ + { + "source": "^(.*)", + "target": "$1", + "service": "html5-apps-repo-rt" + } + ] +} \ No newline at end of file diff --git a/media-store/app-src/src/App.css b/media-store/app-src/src/App.css new file mode 100644 index 00000000..1ec8f2fe --- /dev/null +++ b/media-store/app-src/src/App.css @@ -0,0 +1,57 @@ +@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); + } +} diff --git a/media-store/app-src/src/App.jsx b/media-store/app-src/src/App.jsx new file mode 100644 index 00000000..d1657522 --- /dev/null +++ b/media-store/app-src/src/App.jsx @@ -0,0 +1,18 @@ +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 ( + + + + + + ); +}; + +export default App; diff --git a/media-store/app-src/src/api/axiosInstance.js b/media-store/app-src/src/api/axiosInstance.js new file mode 100644 index 00000000..45fc04c4 --- /dev/null +++ b/media-store/app-src/src/api/axiosInstance.js @@ -0,0 +1,116 @@ +import axios from 'axios'; +import { getUserFromLS, getLocaleFromLS } from '../util/localStorageService'; +import { emitter } from '../util/EventEmitter'; + +/** + * This is axios instance + */ +const axiosInstance = axios.create({ + baseURL: process.env.SERVICE_URL, + timeout: 2000, +}); + +/** + * 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; + } +} + +/** + * Initializing initial data + */ +const user = getUserFromLS(); +const locale = getLocaleFromLS(); +changeUserDefaults(user); +changeLocaleDefaults(locale); + +/** + * Error interceptor for refresh tokens mechanism + */ +let isRefreshing = false; +let subscribers = []; +const refreshTokens = (refreshToken) => { + return axiosInstance.post( + 'users/refreshTokens', + { refreshToken }, + { + headers: { 'content-type': 'application/json' }, + } + ); +}; +axiosInstance.interceptors.response.use(null, (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); + }, + }); + }); + } + + return Promise.reject(error); +}); + +export { axiosInstance, changeLocaleDefaults, changeUserDefaults }; diff --git a/media-store/app-src/src/api/calls.js b/media-store/app-src/src/api/calls.js new file mode 100644 index 00000000..f22c18bb --- /dev/null +++ b/media-store/app-src/src/api/calls.js @@ -0,0 +1,167 @@ +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: tracks.map(({ unitPrice, ID }) => ({ + unitPrice: `${unitPrice}`, + ID, + })), + }, + { + headers: { 'content-type': 'application/json;IEEE754Compatible=true' }, + } + ); +}; + +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, +}; diff --git a/media-store/app-src/src/components/ErrorPage.jsx b/media-store/app-src/src/components/ErrorPage.jsx new file mode 100644 index 00000000..e39c7f52 --- /dev/null +++ b/media-store/app-src/src/components/ErrorPage.jsx @@ -0,0 +1,49 @@ +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 = ( + + ); + const goLoginButton = ( + + ); + + 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 ; +}; + +export default ErrorPage; diff --git a/media-store/app-src/src/components/Header.css b/media-store/app-src/src/components/Header.css new file mode 100644 index 00000000..3d78184d --- /dev/null +++ b/media-store/app-src/src/components/Header.css @@ -0,0 +1,3 @@ +.ant-menu-item .anticon { + margin: 0; +} diff --git a/media-store/app-src/src/components/Header.jsx b/media-store/app-src/src/components/Header.jsx new file mode 100644 index 00000000..9a509407 --- /dev/null +++ b/media-store/app-src/src/components/Header.jsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { Menu, Badge, Spin } 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 } 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, setInvoicedItems, 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, index) => ( + onChangeLocale(curLocale)}> + {curLocale} + + ) + ); + + const onUserLogout = () => { + emitter.emit('UPDATE_USER', undefined); + history.go(0); + }; + + return ( +
+ + history.push('/')}> + Browse + + + {!!user && ( + history.push('/person')}> + Profile + + )} + {requireCustomer(user) && ( + history.push('/invoices')}> + Invoices + + )} + {requireEmployee(user) && ( + history.push('/manage')}> + Manages + + )} + + {loading && } />} + + + + + {haveInvoicedItems && ( + history.push('/invoice')} + key="/invoice" + > +
+ + + +
+
+ )} + + {localeElements} + + {!!user ? ( + } + > + ) : ( + history.push('/login')} + icon={} + > + )} +
+
+ ); +}; + +export default Header; diff --git a/media-store/app-src/src/components/InvoicePage.jsx b/media-store/app-src/src/components/InvoicePage.jsx new file mode 100644 index 00000000..645a24a0 --- /dev/null +++ b/media-store/app-src/src/components/InvoicePage.jsx @@ -0,0 +1,102 @@ +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, unitPrice }) => ({ + ID, + unitPrice, + })) + ) + .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 ( +
+ ( +
+ {user ? ( + <> + + + + ) : ( +
+ + to buy selected +
+ )} +
+ )} + /> + + ); +}; + +export default InvoicePage; diff --git a/media-store/app-src/src/components/Login.jsx b/media-store/app-src/src/components/Login.jsx new file mode 100644 index 00000000..91ca97e8 --- /dev/null +++ b/media-store/app-src/src/components/Login.jsx @@ -0,0 +1,107 @@ +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 ( + + + + + + + + + + + Remember me + + + + + + + ); +}; + +export default Login; diff --git a/media-store/app-src/src/components/ManageStore.jsx b/media-store/app-src/src/components/ManageStore.jsx new file mode 100644 index 00000000..71167a7c --- /dev/null +++ b/media-store/app-src/src/components/ManageStore.jsx @@ -0,0 +1,115 @@ +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' && ) || + (type === 'artist' && ) || + (type === 'album' && ) + ); +}; + +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 ( +
console.log('Not valid params provided')} + > + + + Track + Album + Artist + + + {formElement} + + + + + ); +}; + +export default ManageStore; diff --git a/media-store/app-src/src/components/MyInvoicesPage.jsx b/media-store/app-src/src/components/MyInvoicesPage.jsx new file mode 100644 index 00000000..bb490282 --- /dev/null +++ b/media-store/app-src/src/components/MyInvoicesPage.jsx @@ -0,0 +1,170 @@ +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 ( + + {statusConfig.tagTitle} + {statusConfig.canCancel && ( + + )} + + ); +}; +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) => , + [] + ); + 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 ( + +
+
{`Total price: ${total}`}} + /> + + + ); + }); + }, [invoices]); + + return ( +
{invoiceElements && {invoiceElements}}
+ ); +}; + +export default MyInvoicesPage; diff --git a/media-store/app-src/src/components/PersonPage.jsx b/media-store/app-src/src/components/PersonPage.jsx new file mode 100644 index 00000000..d312a233 --- /dev/null +++ b/media-store/app-src/src/components/PersonPage.jsx @@ -0,0 +1,108 @@ +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) => ( +
+ + + +
+ )); + + return ( + <> + {person.lastName !== '' && ( + console.log('Not valid params provided')} + initialValues={{ + ...person, + }} + > + {personProperties} + + + + + )} + + ); +}; + +export default PersonPage; diff --git a/media-store/app-src/src/components/Router.jsx b/media-store/app-src/src/components/Router.jsx new file mode 100644 index 00000000..0877250a --- /dev/null +++ b/media-store/app-src/src/components/Router.jsx @@ -0,0 +1,67 @@ +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 TracksContainer = React.lazy(() => import('./TracksPage')); +// const Header = React.lazy(() => import('./Header')); +// const PersonPage = React.lazy(() => import('./PersonPage')); +// const ErrorPage = React.lazy(() => import('./ErrorPage')); +// const InvoicePage = React.lazy(() => import('./InvoicePage')); +// const ManageStore = React.lazy(() => import('./ManageStore')); +// const MyInvoicesPage = React.lazy(() => import('./MyInvoicesPage')); +// const Login = React.lazy(() => import('./Login')); + +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 ( + +
+
+ Loading...
}> + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export { MyRouter }; diff --git a/media-store/app-src/src/components/TracksPage.css b/media-store/app-src/src/components/TracksPage.css new file mode 100644 index 00000000..c3855497 --- /dev/null +++ b/media-store/app-src/src/components/TracksPage.css @@ -0,0 +1,4 @@ +.ant-select > div.ant-select-selector { + padding: 5px; + min-width: 300px; +} diff --git a/media-store/app-src/src/components/TracksPage.jsx b/media-store/app-src/src/components/TracksPage.jsx new file mode 100644 index 00000000..b3c5b22a --- /dev/null +++ b/media-store/app-src/src/components/TracksPage.jsx @@ -0,0 +1,215 @@ +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 }) => ( + + )); + +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, + 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 ( +
+ + + ); + }); + }; + + const trackElements = renderTracks(state.tracks); + const genreElements = renderGenres(state.genres); + + return ( + <> +
+ + +
+
+ {trackElements} +
+
+ +
+ + ); +}; + +export default TracksContainer; diff --git a/media-store/app-src/src/components/manage-store/AddAlbumForm.jsx b/media-store/app-src/src/components/manage-store/AddAlbumForm.jsx new file mode 100644 index 00000000..0a4978ae --- /dev/null +++ b/media-store/app-src/src/components/manage-store/AddAlbumForm.jsx @@ -0,0 +1,62 @@ +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 ( + <> +

Add album

+ + + + + + + + ); +}; + +export { AddAlbumForm }; diff --git a/media-store/app-src/src/components/manage-store/AddArtistForm.jsx b/media-store/app-src/src/components/manage-store/AddArtistForm.jsx new file mode 100644 index 00000000..3cc7567f --- /dev/null +++ b/media-store/app-src/src/components/manage-store/AddArtistForm.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Form, Input } from 'antd'; + +const REQUIRED = [ + { + required: true, + message: 'This filed is required!', + }, +]; + +const AddArtistForm = () => { + return ( + <> +

Add artist

+ + + + + ); +}; + +export { AddArtistForm }; diff --git a/media-store/app-src/src/components/manage-store/TrackForm.jsx b/media-store/app-src/src/components/manage-store/TrackForm.jsx new file mode 100644 index 00000000..317f3494 --- /dev/null +++ b/media-store/app-src/src/components/manage-store/TrackForm.jsx @@ -0,0 +1,93 @@ +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!', + }, +]; + +const getAlbums = function (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 ( +
+ + + + + + + + + + + + + + value.replace(/\$\s?|(,*)/g, '')} + /> + +
+ ); +}; + +TrackForm.propTypes = { + initialAlbumTitle: PropTypes.string, +}; + +export { TrackForm }; diff --git a/media-store/app-src/src/components/tracks/DeleteAction.jsx b/media-store/app-src/src/components/tracks/DeleteAction.jsx new file mode 100644 index 00000000..948fc7ab --- /dev/null +++ b/media-store/app-src/src/components/tracks/DeleteAction.jsx @@ -0,0 +1,44 @@ +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 ( + <> + Delete + +

Are You really want to delete this track?

+
+ + ); +}; + +DeleteAction.propTypes = { + ID: PropTypes.number.isRequired, + onDeleteTrack: PropTypes.func.isRequired, +}; + +export { DeleteAction }; diff --git a/media-store/app-src/src/components/tracks/EditAction.jsx b/media-store/app-src/src/components/tracks/EditAction.jsx new file mode 100644 index 00000000..6b156076 --- /dev/null +++ b/media-store/app-src/src/components/tracks/EditAction.jsx @@ -0,0 +1,113 @@ +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 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); + }; + + const afterCloseModal = () => { + setUpdateLoading(true); + getTrack(ID) + .then((response) => { + afterTrackUpdate(response.data); + setUpdateLoading(false); + }) + .catch(handleError); + }; + + return ( + <> + {updateLoading ? : } + + Cancel + , + , + ]} + > +
console.log('Not valid params provided')} + initialValues={{ + name: name, + composer: composer, + genreID: genre.ID, + albumID: album.ID, + unitPrice: unitPrice, + }} + > + + +
+ + ); +}; + +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 }; diff --git a/media-store/app-src/src/components/tracks/ManagedTrack.css b/media-store/app-src/src/components/tracks/ManagedTrack.css new file mode 100644 index 00000000..e2107b16 --- /dev/null +++ b/media-store/app-src/src/components/tracks/ManagedTrack.css @@ -0,0 +1,7 @@ +span > span.anticon.anticon-delete:hover { + color: #ff4d4f; +} + +.card-element { + transition: opacity 0.5s ease-in-out; +} diff --git a/media-store/app-src/src/components/tracks/ManagedTrack.jsx b/media-store/app-src/src/components/tracks/ManagedTrack.jsx new file mode 100644 index 00000000..8beaa37c --- /dev/null +++ b/media-store/app-src/src/components/tracks/ManagedTrack.jsx @@ -0,0 +1,42 @@ +import React, { useState, useRef } from "react"; +import { Card } from "antd"; +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 ( +
+ { + trackElement.current.style.opacity = 0; + setTimeout(() => onDeleteTrack(track.ID), 500); + }} + />, + setTrack(value)} + />, + ]} + title={track.name} + bordered={false} + > + + +
+ ); +}; + +export { ManagedTrack }; diff --git a/media-store/app-src/src/components/tracks/Track.jsx b/media-store/app-src/src/components/tracks/Track.jsx new file mode 100644 index 00000000..0ab57308 --- /dev/null +++ b/media-store/app-src/src/components/tracks/Track.jsx @@ -0,0 +1,60 @@ +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 ( +
+ + {!isAlreadyOrdered && ( + + )} + , + ]} + title={initialTrack.name} + bordered={false} + > + + +
+ ); +}; + +Track.propTypes = { + initialTrack: PropTypes.object, + isAlreadyOrdered: PropTypes.bool, +}; + +export { Track }; diff --git a/media-store/app-src/src/components/tracks/TrackCardBody.jsx b/media-store/app-src/src/components/tracks/TrackCardBody.jsx new file mode 100644 index 00000000..7587c99a --- /dev/null +++ b/media-store/app-src/src/components/tracks/TrackCardBody.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const TrackCardBody = ({ track }) => { + return ( + <> +
+ Artist: + {track.album.artist.name} +
+
+ Album: + {track.album.title} +
+
+ Genre: + {track.genre.name} +
+
+ {track.composer && ( + + Compositor: + {track.composer} + + )} +
+
+ + Price: + {track.unitPrice} + +
+ + ); +}; + +TrackCardBody.propTypes = { + track: PropTypes.object.isRequired, +}; + +export { TrackCardBody }; diff --git a/media-store/app-src/src/contexts/AppStateContext.jsx b/media-store/app-src/src/contexts/AppStateContext.jsx new file mode 100644 index 00000000..5b5dd180 --- /dev/null +++ b/media-store/app-src/src/contexts/AppStateContext.jsx @@ -0,0 +1,66 @@ +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 {children}; +}; + +AppStateContextProvider.propTypes = { + children: PropTypes.element.isRequired, +}; + +export { AppStateContextProvider, AppStateContext }; diff --git a/media-store/app-src/src/hocs/withRestrictions.jsx b/media-store/app-src/src/hocs/withRestrictions.jsx new file mode 100644 index 00000000..26eedba3 --- /dev/null +++ b/media-store/app-src/src/hocs/withRestrictions.jsx @@ -0,0 +1,16 @@ +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 }) ? ( + + ) : ( + + ); + }; +}; + +export { withRestrictions }; diff --git a/media-store/app-src/src/hooks/useAbortableEffect.js b/media-store/app-src/src/hooks/useAbortableEffect.js new file mode 100644 index 00000000..648d923d --- /dev/null +++ b/media-store/app-src/src/hooks/useAbortableEffect.js @@ -0,0 +1,22 @@ +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 }; diff --git a/media-store/app-src/src/hooks/useAppState.js b/media-store/app-src/src/hooks/useAppState.js new file mode 100644 index 00000000..831ae73d --- /dev/null +++ b/media-store/app-src/src/hooks/useAppState.js @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { AppStateContext } from '../contexts/AppStateContext'; + +const useAppState = () => useContext(AppStateContext); + +export { useAppState }; diff --git a/media-store/app-src/src/hooks/useErrors.js b/media-store/app-src/src/hooks/useErrors.js new file mode 100644 index 00000000..4ccdd694 --- /dev/null +++ b/media-store/app-src/src/hooks/useErrors.js @@ -0,0 +1,34 @@ +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 }; diff --git a/media-store/app-src/src/index.jsx b/media-store/app-src/src/index.jsx new file mode 100644 index 00000000..8110e69e --- /dev/null +++ b/media-store/app-src/src/index.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +// import * as serviceWorker from './serviceWorker'; + +ReactDOM.render(, 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(); diff --git a/media-store/app-src/src/logo.svg b/media-store/app-src/src/logo.svg new file mode 100644 index 00000000..6b60c104 --- /dev/null +++ b/media-store/app-src/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/media-store/app-src/src/serviceWorker.js b/media-store/app-src/src/serviceWorker.js new file mode 100644 index 00000000..e69de29b diff --git a/media-store/app-src/src/setupTests.js b/media-store/app-src/src/setupTests.js new file mode 100644 index 00000000..74b1a275 --- /dev/null +++ b/media-store/app-src/src/setupTests.js @@ -0,0 +1,5 @@ +// 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'; diff --git a/media-store/app-src/src/util/EventEmitter.js b/media-store/app-src/src/util/EventEmitter.js new file mode 100644 index 00000000..b28a45af --- /dev/null +++ b/media-store/app-src/src/util/EventEmitter.js @@ -0,0 +1,5 @@ +import EventEmitter from 'events'; + +const emitter = new EventEmitter(); + +export { emitter }; diff --git a/media-store/app-src/src/util/constants.js b/media-store/app-src/src/util/constants.js new file mode 100644 index 00000000..4b9b4460 --- /dev/null +++ b/media-store/app-src/src/util/constants.js @@ -0,0 +1,7 @@ +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'); diff --git a/media-store/app-src/src/util/localStorageService.js b/media-store/app-src/src/util/localStorageService.js new file mode 100644 index 00000000..853b4817 --- /dev/null +++ b/media-store/app-src/src/util/localStorageService.js @@ -0,0 +1,36 @@ +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 }; diff --git a/media-store/app-src/src/util/validateUser.js b/media-store/app-src/src/util/validateUser.js new file mode 100644 index 00000000..b2d52b29 --- /dev/null +++ b/media-store/app-src/src/util/validateUser.js @@ -0,0 +1,18 @@ +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 }; diff --git a/media-store/app-src/webpack/common-plugins.js b/media-store/app-src/webpack/common-plugins.js new file mode 100644 index 00000000..9ae39a04 --- /dev/null +++ b/media-store/app-src/webpack/common-plugins.js @@ -0,0 +1,33 @@ +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(), + ], +}; diff --git a/media-store/app-src/webpack/common-rules.js b/media-store/app-src/webpack/common-rules.js new file mode 100644 index 00000000..1afcc25e --- /dev/null +++ b/media-store/app-src/webpack/common-rules.js @@ -0,0 +1,18 @@ +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' }], + }, + ], +}; diff --git a/media-store/app-src/webpack/webpack-dev-server.js b/media-store/app-src/webpack/webpack-dev-server.js new file mode 100644 index 00000000..8d5b8030 --- /dev/null +++ b/media-store/app-src/webpack/webpack-dev-server.js @@ -0,0 +1,62 @@ +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', + react: ['react', 'react-dom'], + lodash: ['lodash'], + moment: ['moment'], + events: ['events'], + axios: ['axios'], + antd: ['antd'], + }, + 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'] }, +}; diff --git a/media-store/app-src/webpack/webpack.common.js b/media-store/app-src/webpack/webpack.common.js new file mode 100644 index 00000000..83b11f1f --- /dev/null +++ b/media-store/app-src/webpack/webpack.common.js @@ -0,0 +1,25 @@ +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'] }, +}; diff --git a/media-store/app-src/webpack/webpack.dev.js b/media-store/app-src/webpack/webpack.dev.js new file mode 100644 index 00000000..ef02a698 --- /dev/null +++ b/media-store/app-src/webpack/webpack.dev.js @@ -0,0 +1,25 @@ +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' }], + }, + ], + }, +}); diff --git a/media-store/app-src/webpack/webpack.prod.js b/media-store/app-src/webpack/webpack.prod.js new file mode 100644 index 00000000..bb047d76 --- /dev/null +++ b/media-store/app-src/webpack/webpack.prod.js @@ -0,0 +1,40 @@ +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'], + }, + ], + }, +}); diff --git a/media-store/app/favicon.ico b/media-store/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bcd5dfd67cd0361b78123e95c2dd96031f27f743 GIT binary patch literal 3150 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 MDeX2GIKR{Qf5okjU;qFB literal 0 HcmV?d00001 diff --git a/media-store/app/index.html b/media-store/app/index.html new file mode 100644 index 00000000..28e725c5 --- /dev/null +++ b/media-store/app/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + \ No newline at end of file diff --git a/media-store/app/logo192.png b/media-store/app/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/media-store/app/manifest.json b/media-store/app/manifest.json new file mode 100644 index 00000000..45979ace --- /dev/null +++ b/media-store/app/manifest.json @@ -0,0 +1,31 @@ +{ + "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" + } + } +} diff --git a/media-store/app/robots.txt b/media-store/app/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/media-store/app/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/media-store/app/xs-app.json b/media-store/app/xs-app.json new file mode 100644 index 00000000..930c40ee --- /dev/null +++ b/media-store/app/xs-app.json @@ -0,0 +1,10 @@ +{ + "welcomeFile": "/index.html", + "routes": [ + { + "source": "^(.*)", + "target": "$1", + "service": "html5-apps-repo-rt" + } + ] +} \ No newline at end of file diff --git a/media-store/deployers/approuter/package.json b/media-store/deployers/approuter/package.json new file mode 100644 index 00000000..d9a4cd89 --- /dev/null +++ b/media-store/deployers/approuter/package.json @@ -0,0 +1,11 @@ +{ + "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" + } +} diff --git a/media-store/deployers/approuter/xs-app.json b/media-store/deployers/approuter/xs-app.json new file mode 100644 index 00000000..4270e0cb --- /dev/null +++ b/media-store/deployers/approuter/xs-app.json @@ -0,0 +1,17 @@ +{ + "welcomeFile": "/index.html", + "authenticationMethod": "none", + "routes": [ + { + "source": "/api/(.*)", + "target": "$1", + "destination": "srv-binding", + "authenticationType": "none" + }, + { + "source": "^(.*)", + "target": "mediastore/$1", + "service": "html5-apps-repo-rt" + } + ] +} diff --git a/media-store/deployers/html5Deployer/package.json b/media-store/deployers/html5Deployer/package.json new file mode 100644 index 00000000..09f597b2 --- /dev/null +++ b/media-store/deployers/html5Deployer/package.json @@ -0,0 +1,12 @@ +{ + "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" + } +} diff --git a/media-store/srv/browse-invoices-service.js b/media-store/srv/browse-invoices-service.js index bf19d66a..a7e490cf 100644 --- a/media-store/srv/browse-invoices-service.js +++ b/media-store/srv/browse-invoices-service.js @@ -49,7 +49,6 @@ module.exports = async function () { ({ track_ID: curID }) => newInvoicedTracks.includes(curID) ); if (isNewInvoiceHasInvoicedTracks) { - await transaction.rollback(); req.reject(400, "Invoice contains already owned values"); } diff --git a/media-store/srv/browse-tracks-service.js b/media-store/srv/browse-tracks-service.js index 34173e1b..794e7b87 100644 --- a/media-store/srv/browse-tracks-service.js +++ b/media-store/srv/browse-tracks-service.js @@ -1,5 +1,7 @@ const cds = require("@sap/cds"); +const SHIPPED_STATUS = 1; + module.exports = async function () { const db = await cds.connect.to("db"); // connect to database service @@ -13,6 +15,7 @@ module.exports = async function () { "invoice_ID in", SELECT("ID").from(Invoices).where({ customer_ID: req.user.attr.ID, + status: SHIPPED_STATUS, }) ) ); diff --git a/media-store/srv/manage-store-service.js b/media-store/srv/manage-store-service.js index 43f5f77b..d32b48d4 100644 --- a/media-store/srv/manage-store-service.js +++ b/media-store/srv/manage-store-service.js @@ -4,8 +4,7 @@ module.exports = async function () { const db = await cds.connect.to("db"); // connect to database service this.on("CREATE", "*", async (req) => { - const selectLastQuery = SELECT.one(req.entity) - .orderBy({ ID: "desc" }); + const selectLastQuery = SELECT.one(req.entity).orderBy({ ID: "desc" }); const transaction = await db.tx(req); diff --git a/media-store/util/helpers.js b/media-store/util/helpers.js new file mode 100644 index 00000000..2587f977 --- /dev/null +++ b/media-store/util/helpers.js @@ -0,0 +1,28 @@ +const getDurationInMilliseconds = (start) => { + const NS_PER_SEC = 1e9; // convert to nanoseconds + const NS_TO_MS = 1e6; // convert to milliseconds + const diff = process.hrtime(start); + return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS; +}; + +const getFormattedDateTime = () => { + let currentDateTime = new Date(); + let formattedDateTime = + currentDateTime.getFullYear() + + "-" + + (currentDateTime.getMonth() + 1) + + "-" + + currentDateTime.getDate() + + " " + + currentDateTime.getHours() + + ":" + + currentDateTime.getMinutes() + + ":" + + currentDateTime.getSeconds(); + return formattedDateTime; +}; + +module.exports = { + getFormattedDateTime, + getDurationInMilliseconds, +};