add app to gitignore

This commit is contained in:
Dzmitry_Tamashevich@epam.com
2020-11-29 21:13:18 +03:00
committed by Daniel Hutzel
parent dbe4b8a7bd
commit 00474edffe
60 changed files with 4 additions and 2645 deletions

View File

@@ -9,7 +9,10 @@ target/
package-lock.json
# html5Deployer
html5Deployer/resources/app/
deployers/html5Deployer/resources/app/
# app
app/
# Web IDE, App Studio
.che/

View File

@@ -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*

View File

@@ -1,13 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceRoot}/src"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -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="/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="/manifest.json" />
<!--
Notice the use of 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", "/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`.
-->
<script src="/static/app.75bbe376e4ccdece0eed.js"></script><script src="/static/react.75bbe376e4ccdece0eed.js"></script><script src="/static/lodash.75bbe376e4ccdece0eed.js"></script><script src="/static/moment.75bbe376e4ccdece0eed.js"></script><script src="/static/events.75bbe376e4ccdece0eed.js"></script><script src="/static/axios.75bbe376e4ccdece0eed.js"></script><script src="/static/antd.75bbe376e4ccdece0eed.js"></script></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -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"
}
}
}

View File

@@ -1,42 +0,0 @@
{
"name": "mediastore",
"version": "0.1.0",
"private": true,
"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",
"events": "^3.2.0",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-inline-editing": "^1.0.10",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0"
},
"scripts": {
"start": "react-scripts start --no-cache",
"build": "react-scripts build",
"test": "react-scripts test --no-cache",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<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

View File

@@ -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"
}
}
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,10 +0,0 @@
{
"welcomeFile": "/index.html",
"routes": [
{
"source": "^(.*)",
"target": "$1",
"service": "html5-apps-repo-rt"
}
]
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -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);
}
}

View File

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

View File

@@ -1,21 +0,0 @@
import axios from "axios";
import { getUserFromLS, getLocaleFromLS } from "../util/localStorageService";
import {
changeUserDefaults,
changeLocaleDefaults,
} from "./changeAxiosDefaults";
import { API } from "../util/constants";
import { responseErrorInterceptor } from "./responseErrorInterceptor";
const axiosInstance = axios.create({
baseURL: API,
timeout: 2000,
});
const user = getUserFromLS();
const locale = getLocaleFromLS();
changeUserDefaults(user);
changeLocaleDefaults(locale);
axiosInstance.interceptors.response.use(null, responseErrorInterceptor);
export { axiosInstance, changeLocaleDefaults, changeUserDefaults };

View File

@@ -1,188 +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.tracksEntity;
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})`);
};
const refreshTokens = (refreshToken) => {
return axiosInstance.post(
`${USER_SERVICE}/refreshTokens`,
{ refreshToken },
{
headers: { "content-type": "application/json" },
}
);
};
export {
fetchTacks,
countTracks,
fetchGenres,
invoice,
fetchPerson,
confirmPerson,
fetchInvoices,
cancelInvoice,
fetchAlbumsByName,
addTrack,
addArtist,
addAlbum,
fetchArtistsByName,
login,
updateTrack,
getTrack,
deleteTrack,
refreshTokens,
};

View File

@@ -1,27 +0,0 @@
import { axiosInstance } from "./axiosInstance";
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";
}
}
function changeLocaleDefaults(locale) {
if (locale) {
axiosInstance.defaults.headers.common["Accept-language"] = locale;
}
}
export { changeLocaleDefaults, changeUserDefaults };

View File

@@ -1,62 +0,0 @@
import { emitter } from "../util/EventEmitter";
import { axiosInstance } from "./axiosInstance";
import { refreshTokens } from "./calls";
import { getUserFromLS } from "../util/localStorageService";
let isRefreshing = false;
let subscribers = [];
function responseErrorInterceptor(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 { responseErrorInterceptor };

View File

@@ -1,3 +0,0 @@
.ant-menu-item .anticon {
margin: 0;
}

View File

@@ -1,158 +0,0 @@
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) => (
<Menu.Item
key={`${index}${curLocale}`}
onClick={() => onChangeLocale(curLocale)}
>
{curLocale}
</Menu.Item>
));
const onUserLogout = () => {
emitter.emit("UPDATE_USER", undefined);
history.go(0);
};
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")}>
Manage
</Menu.Item>
)}
<span>
{loading && (
<Spin
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
/>
)}
</span>
</Menu>
<Menu
style={{ width: "50%", display: "flex", justifyContent: "flex-end" }}
theme="light"
mode="horizontal"
selectedKeys={currentKey}
>
{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>
) : (
<Menu.Item
key="/login"
onClick={() => history.push("/login")}
icon={<LoginOutlined style={{ fontSize: 16 }} />}
></Menu.Item>
)}
</Menu>
</div>
);
};
export { Header };

View File

@@ -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 "../pages/TracksPage";
import { Header } from "../components/Header";
import { PersonPage } from "../pages/PersonPage";
import { ErrorPage } from "../pages/ErrorPage";
import { Login } from "../pages/Login";
import { withRestrictions } from "../hocs/withRestrictions";
import { InvoicePage } from "../pages/InvoicePage";
import { ManageStore } from "../pages/ManageStore";
import { MyInvoicesPage } from "../pages/MyInvoicesPage";
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" }}>
<Switch>
<Route exact path={["/", "/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>
</div>
</Router>
);
};
export { MyRouter };

View File

@@ -1,71 +0,0 @@
import React, { useMemo, createContext, useState, useEffect } from "react";
import {
getUserFromLS,
getLocaleFromLS,
setUserToLS,
} from "../util/localStorageService";
import { changeUserDefaults } from "../api/axiosInstance";
import { emitter } from "../util/EventEmitter";
let counter = 0;
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", counter++);
changeUserDefaults(newUser);
setUserToLS(newUser);
setUser(newUser);
};
emitter.on("UPDATE_USER", updateUser);
return () => {
emitter.removeListener("UPDATE_USER", updateUser);
};
}, []);
const value = useMemo(
() => ({
error: error,
loading: loading,
invoicedItems: invoicedItems,
user: user,
locale: locale,
setLoading,
setError,
setInvoicedItems,
setUser: setUser,
setLocale: setLocale,
}),
[locale, user, loading, error, invoicedItems]
);
return (
<AppStateContext.Provider value={value}>
{children}
</AppStateContext.Provider>
);
};
export { AppStateContextProvider, AppStateContext };

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import { useContext } from "react";
import { AppStateContext } from "../contexts/AppStateContext";
const useAppState = () => useContext(AppStateContext);
export { useAppState };

View File

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

View File

@@ -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();

View File

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

View File

@@ -1,50 +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 { ErrorPage };

View File

@@ -1,107 +0,0 @@
import React from "react";
import { Table, Button, message } from "antd";
import { useAppState } from "../hooks/useAppState";
import { useHistory } from "react-router-dom";
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 (
<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 { InvoicePage };

View File

@@ -1,107 +0,0 @@
import React from "react";
import { Form, Input, Button, Checkbox, message } from "antd";
import { login } from "../api/calls";
import { useHistory } from "react-router-dom";
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 { Login };

View File

@@ -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 { ManageStore };

View File

@@ -1,185 +0,0 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
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>
);
};
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 { MyInvoicesPage };

View File

@@ -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: personData }) => {
personData = omit(personData, "@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 { PersonPage };

View File

@@ -1,4 +0,0 @@
.ant-select > div.ant-select-selector {
padding: 5px;
min-width: 300px;
}

View File

@@ -1,219 +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,
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.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 { TracksContainer };

View File

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

View File

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

View File

@@ -1,93 +0,0 @@
import React, { useEffect, useState } from "react";
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 (
<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>
);
};
export { TrackForm };

View File

@@ -1,43 +0,0 @@
import React, { useState } from "react";
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>
</>
);
};
export { DeleteAction };

View File

@@ -1,119 +0,0 @@
import React from "react";
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 ? (
<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: name,
composer: composer,
genreID: genre.ID,
albumID: album.ID,
unitPrice: unitPrice,
}}
>
<TrackForm initialAlbumTitle={album.title} />
</Form>
</Modal>
</>
);
};
export { EditAction };

View File

@@ -1,7 +0,0 @@
span > span.anticon.anticon-delete:hover {
color: #ff4d4f;
}
.card-element {
transition: opacity 0.5s ease-in-out;
}

View File

@@ -1,42 +0,0 @@
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 (
<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>
);
};
export { ManagedTrack };

View File

@@ -1,56 +0,0 @@
import React, { useState, useRef } from "react";
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>
);
};
export { Track };

View File

@@ -1,33 +0,0 @@
import React from "react";
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>
</>
);
};
export { TrackCardBody };

View File

@@ -1,141 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@@ -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';

View File

@@ -1,5 +0,0 @@
import EventEmitter from "events";
const emitter = new EventEmitter();
export { emitter };

View File

@@ -1,14 +0,0 @@
export const AVAILABLE_LOCALES = ["en", "fr", "de"];
export const MESSAGE_TIMEOUT = 2;
// in dev mode using provided api
// in prod mode using proxy
export const API =
process.env.NODE_ENV === "development" ? "http://localhost:4004/" : "api/";
export const requireEmployee = (user) =>
!!user && user.roles.includes("employee");
export const requireCustomer = (user) =>
!!user && user.roles.includes("customer");

View File

@@ -1,35 +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) {}
};
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 };

View File

@@ -1,20 +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 };

View File

@@ -1,10 +0,0 @@
{
"welcomeFile": "/index.html",
"routes": [
{
"source": "^(.*)",
"target": "$1",
"service": "html5-apps-repo-rt"
}
]
}

View File

@@ -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"
}
}

View File

@@ -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"
}
]
}

View File

@@ -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"
}
}