setTheme(theme)}
27 | />
28 | ))}
29 | {languagesList.map(lang => (
30 |
setLocalization(lang)}
39 | />
40 | ))}
41 |
42 | );
43 | }
44 |
45 | export const TogglersConnected = observer(Togglers);
46 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/OrderBook/OffersTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | import { precise } from 'utils';
5 |
6 | import styles from './OrderBook.scss';
7 |
8 | export function OffersTable({
9 | dataArray,
10 | dataTotal,
11 | rowClassName,
12 | reverseTotal,
13 | maxTotal,
14 | tradedCurrency,
15 | }) {
16 | let currentTotal = reverseTotal ? dataTotal : 0;
17 |
18 | return (
19 |
20 | {dataArray.map(({ price, amount, total }, index) => {
21 | if (reverseTotal) {
22 | if (index > 0) {
23 | currentTotal -= total;
24 | }
25 | } else {
26 | currentTotal += total;
27 | }
28 |
29 | const barStyle = {
30 | width: `${(currentTotal / maxTotal) * 60}%`,
31 | };
32 |
33 | return (
34 |
35 |
{precise(price, tradedCurrency)}
36 |
{precise(amount, tradedCurrency)}
37 |
{currentTotal.toFixed(4)}
38 |
39 |
40 | );
41 | })}
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/eslint-custom/eslint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: 'babel-eslint',
3 | env: {
4 | browser: true,
5 | node: true,
6 | es6: true,
7 | 'cypress/globals': true,
8 | },
9 | settings: {
10 | react: {
11 | version: 'detect',
12 | },
13 | },
14 | extends: [
15 | './rules/best-practices.yaml',
16 | './rules/ecmascript-6.yaml',
17 | './rules/node-js-and-common-js.yaml',
18 | './rules/possible-errors.yaml',
19 | './rules/strict-mode.yaml',
20 | './rules/stylistic-issues.yaml',
21 | './rules/variables.yaml',
22 | './rules/react.yaml',
23 | 'prettier',
24 | 'prettier/babel',
25 | 'prettier/react',
26 | ],
27 | plugins: ['react', 'prettier', 'import', 'react-hooks', 'cypress'],
28 | rules: {
29 | 'prettier/prettier': [
30 | 'warn',
31 | {
32 | printWidth: 80,
33 | tabWidth: 2,
34 | singleQuote: true,
35 | semi: true,
36 | trailingComma: 'es5',
37 | bracketSpacing: true,
38 | jsxBracketSameLine: false,
39 | arrowParens: 'avoid',
40 | proseWrap: 'never',
41 | },
42 | ],
43 | },
44 | parserOptions: {
45 | ecmaFeatures: {
46 | legacyDecorators: true,
47 | },
48 | },
49 | globals: {
50 | SENTRY_URL: true,
51 | HOT_RELOAD: true,
52 | PUBLIC_URL: true,
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "exchange_habr",
3 | "homepage": "https://dkazakov8.github.io/exchange_habr/dist",
4 | "version": "1.0.0",
5 | "description": "An example of building Frontend for Exchange platform",
6 | "author": "Dmitry Kazakov",
7 | "license": "MIT",
8 | "scripts": {
9 | "upd": "yarn install --no-lockfile",
10 | "dev": "webpack-dev-server --config webpack-custom/webpack.config.js",
11 | "build": "webpack --config webpack-custom/webpack.config.js",
12 | "format:js": "eslint --ignore-path .formatignore --ext .js -c ./eslint-custom/eslint.config.js --fix",
13 | "format:style": "stylelint --ignore-path .formatignore --config ./eslint-custom/stylelint.config.js --fix"
14 | },
15 | "browserslist": [
16 | "last 2 versions",
17 | "not dead"
18 | ],
19 | "husky": {
20 | "hooks": {
21 | "pre-commit": "npm run format:js -- --max-warnings=0 ./ && npm run format:style ./**/*.scss"
22 | }
23 | },
24 | "dependencies": {
25 | "@hot-loader/react-dom": "16.8.6",
26 | "@sentry/browser": "5.1.1",
27 | "classnames": "2.2.6",
28 | "eslint-custom": "file:./eslint-custom",
29 | "highcharts": "7.1.1",
30 | "husky": "2.1.0",
31 | "lodash": "4.17.11",
32 | "mobx": "5.9.4",
33 | "mobx-react-lite": "1.3.0",
34 | "react": "16.8.6",
35 | "webpack-custom": "file:./webpack-custom"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/webpack-custom/config/configOptimization.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @docs: https://webpack.js.org/configuration/optimization
3 | *
4 | */
5 |
6 | const TerserPlugin = require('terser-webpack-plugin');
7 |
8 | module.exports = {
9 | runtimeChunk: {
10 | name: 'runtime',
11 | },
12 | chunkIds: 'named',
13 | moduleIds: 'hashed',
14 | mergeDuplicateChunks: true,
15 | splitChunks: {
16 | cacheGroups: {
17 | lodash: {
18 | test: module => module.context.indexOf('node_modules\\lodash') !== -1,
19 | name: 'lodash',
20 | chunks: 'all',
21 | enforce: true,
22 | },
23 | sentry: {
24 | test: module => module.context.indexOf('node_modules\\@sentry') !== -1,
25 | name: 'sentry',
26 | chunks: 'all',
27 | enforce: true,
28 | },
29 | highcharts: {
30 | test: module =>
31 | module.context.indexOf('node_modules\\highcharts') !== -1,
32 | name: 'highcharts',
33 | chunks: 'all',
34 | enforce: true,
35 | },
36 | vendor: {
37 | test: module => module.context.indexOf('node_modules') !== -1,
38 | priority: -1,
39 | name: 'vendor',
40 | chunks: 'all',
41 | enforce: true,
42 | },
43 | },
44 | },
45 | minimizer: [
46 | new TerserPlugin({
47 | terserOptions: {
48 | keep_fnames: true,
49 | },
50 | }),
51 | ],
52 | };
53 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/MarketList/MarketList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | import { observer } from 'utils';
5 | import { useLocalization } from 'hooks';
6 |
7 | import { TogglersConnected } from './Togglers';
8 | import { PairsListConnected } from './PairsList';
9 | import { MarketTabsConnected } from './MarketTabs';
10 |
11 | import styles from './MarketList.scss';
12 | import { messages } from './messages';
13 |
14 | function MarketsList() {
15 | const getLn = useLocalization(__filename, messages);
16 |
17 | const numericCellClassName = cn(styles.cell, styles.alignRight);
18 |
19 | return (
20 |
21 |
22 |
23 | {getLn(messages.marketsList)}
24 |
25 |
26 |
27 |
28 |
29 |
{getLn(messages.pair)}
30 |
{getLn(messages.price)}
31 |
{getLn(messages.change)}
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | export const MarketsListConnected = observer(MarketsList);
41 |
--------------------------------------------------------------------------------
/src/utils/observer.js:
--------------------------------------------------------------------------------
1 | import { useObserver } from 'mobx-react-lite';
2 | import React from 'react';
3 | import _ from 'lodash';
4 |
5 | function copyStaticProperties(base, target) {
6 | const hoistBlackList = {
7 | $$typeof: true,
8 | render: true,
9 | compare: true,
10 | type: true,
11 | };
12 |
13 | Object.keys(base).forEach(key => {
14 | if (base.hasOwnProperty(key) && !hoistBlackList[key]) {
15 | Object.defineProperty(
16 | target,
17 | key,
18 | Object.getOwnPropertyDescriptor(base, key)
19 | );
20 | }
21 | });
22 | }
23 |
24 | export function observer(baseComponent, options) {
25 | const baseComponentName = baseComponent.displayName || baseComponent.name;
26 |
27 | function wrappedComponent(props, ref) {
28 | return useObserver(function applyObserver() {
29 | return baseComponent(props, ref);
30 | }, baseComponentName);
31 | }
32 | wrappedComponent.displayName = baseComponentName;
33 |
34 | let memoComponent = null;
35 | if (HOT_RELOAD === 'true') {
36 | memoComponent = wrappedComponent;
37 | } else if (_.get(options, 'forwardRef')) {
38 | memoComponent = React.memo(React.forwardRef(wrappedComponent));
39 | } else {
40 | memoComponent = React.memo(wrappedComponent);
41 | }
42 |
43 | copyStaticProperties(baseComponent, memoComponent);
44 | memoComponent.displayName = baseComponentName;
45 |
46 | return memoComponent;
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Link/Link.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | import { useStore } from 'hooks';
5 | import { observer } from 'utils';
6 |
7 | function checkRouteParamsWithMasks(route, params) {
8 | if (route.masks) {
9 | Object.entries(route.masks).forEach(([paramName, paramMask]) => {
10 | const value = _.get(params, paramName);
11 |
12 | if (paramMask && !paramMask.test(value)) {
13 | console.error(
14 | `checkRouteParamsWithMasks: wrong param for ${paramName} in Link to ${
15 | route.name
16 | }: ${value}`
17 | );
18 | }
19 | });
20 | }
21 | }
22 |
23 | function Link(props) {
24 | const store = useStore();
25 | const { currentRoute } = store.router;
26 | const { route, params, children, onClick, ...otherProps } = props;
27 |
28 | checkRouteParamsWithMasks(route, params);
29 |
30 | const filledPath = store.router.replaceDynamicParams(route, params);
31 |
32 | return (
33 |
{
36 | e.preventDefault();
37 |
38 | if (currentRoute.isLoading) {
39 | return false;
40 | }
41 |
42 | store.router.goTo(route, params);
43 |
44 | if (onClick) {
45 | onClick();
46 | }
47 | }}
48 | {...otherProps}
49 | >
50 | {children}
51 |
52 | );
53 | }
54 |
55 | export const LinkConnected = observer(Link);
56 |
--------------------------------------------------------------------------------
/webpack-custom/utils/sassVariablesLoader.js:
--------------------------------------------------------------------------------
1 | function convertSourceToJsObject(source) {
2 | const themesObject = {};
3 | const fullThemesArray = source.match(/\.([^}]|\s)*}/g) || [];
4 |
5 | fullThemesArray.forEach(fullThemeStr => {
6 | const theme = fullThemeStr.match(/\.\w+\s{/g)[0].replace(/\W/g, '');
7 | themesObject[theme] = {};
8 |
9 | const variablesMatches = fullThemeStr.match(/--(.*:[^;]*)/g) || [];
10 |
11 | variablesMatches.forEach(varMatch => {
12 | const [key, value] = varMatch.split(': ');
13 | themesObject[theme][key] = value;
14 | });
15 | });
16 |
17 | return themesObject;
18 | }
19 |
20 | function checkThemesEquality(themes) {
21 | const themesArray = Object.keys(themes);
22 |
23 | themesArray.forEach(themeStr => {
24 | const themeObject = themes[themeStr];
25 | const otherThemesArray = themesArray.filter(t => t !== themeStr);
26 |
27 | Object.keys(themeObject).forEach(variableName => {
28 | otherThemesArray.forEach(otherThemeStr => {
29 | const otherThemeObject = themes[otherThemeStr];
30 |
31 | if (!otherThemeObject[variableName]) {
32 | throw new Error(
33 | `checkThemesEquality: theme ${otherThemeStr} has no variable ${variableName}`
34 | );
35 | }
36 | });
37 | });
38 | });
39 | }
40 |
41 | module.exports = function sassVariablesLoader(source) {
42 | const themes = convertSourceToJsObject(source);
43 |
44 | checkThemesEquality(themes);
45 |
46 | return `module.exports = ${JSON.stringify(themes)}`;
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/Router/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | import { useStore } from 'hooks';
5 | import { observer } from 'utils';
6 | import { routeComponents } from 'routeComponents';
7 |
8 | function getRouteComponent(route, isLoading) {
9 | const Component = routeComponents[route.name];
10 |
11 | if (!Component) {
12 | console.error(
13 | `getRouteComponent: component for ${
14 | route.name
15 | } is not defined in routeComponents`
16 | );
17 |
18 | return null;
19 | }
20 |
21 | return
;
22 | }
23 |
24 | function useBeforeEnter() {
25 | const store = useStore();
26 | const { currentRoute } = store.router;
27 |
28 | React.useEffect(() => {
29 | if (currentRoute.isLoading) {
30 | const beforeEnter = _.get(currentRoute, 'beforeEnter');
31 |
32 | if (_.isFunction(beforeEnter)) {
33 | Promise.resolve()
34 | .then(() => beforeEnter(currentRoute, store))
35 | .then(() => {
36 | currentRoute.isLoading = false;
37 | })
38 | .catch(error => console.error(error));
39 | } else {
40 | currentRoute.isLoading = false;
41 | }
42 | }
43 | });
44 |
45 | return currentRoute.isLoading;
46 | }
47 |
48 | function Router() {
49 | const {
50 | router: { currentRoute },
51 | } = useStore();
52 | const isLoading = useBeforeEnter();
53 |
54 | return getRouteComponent(currentRoute, isLoading);
55 | }
56 |
57 | export const RouterConnected = observer(Router);
58 |
--------------------------------------------------------------------------------
/src/utils/makeObservable.js:
--------------------------------------------------------------------------------
1 | import { action, computed, decorate, observable } from 'mobx';
2 |
3 | export function makeObservable(target) {
4 | /**
5 | * Для методов - биндим контекст this + все изменения сторов
6 | * выполняем в одной транзакции
7 | *
8 | * Для геттеров - оборачиваем в computed
9 | *
10 | */
11 |
12 | const classPrototype = target.prototype;
13 | const methodsAndGetters = Object.getOwnPropertyNames(classPrototype).filter(
14 | methodName => methodName !== 'constructor'
15 | );
16 |
17 | for (const methodName of methodsAndGetters) {
18 | const descriptor = Object.getOwnPropertyDescriptor(
19 | classPrototype,
20 | methodName
21 | );
22 |
23 | descriptor.value = decorate(classPrototype, {
24 | [methodName]:
25 | typeof descriptor.value === 'function' ? action.bound : computed,
26 | });
27 | }
28 |
29 | return (...constructorArguments) => {
30 | /**
31 | * Параметры, за исключением rootStore, трансформируем в
32 | * observable
33 | *
34 | */
35 |
36 | const store = new target(...constructorArguments);
37 | const staticProperties = Object.keys(store);
38 |
39 | staticProperties.forEach(propName => {
40 | if (propName === 'rootStore') {
41 | return false;
42 | }
43 |
44 | const descriptor = Object.getOwnPropertyDescriptor(store, propName);
45 |
46 | Object.defineProperty(
47 | store,
48 | propName,
49 | observable(store, propName, descriptor)
50 | );
51 | });
52 |
53 | return store;
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/webpack-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack-custom",
3 | "version": "1.0.0",
4 | "description": "Custom webpack rules for this project",
5 | "author": "Dmitry Kazakov",
6 | "license": "MIT",
7 | "dependencies": {
8 | "@babel/core": "7.4.4",
9 | "@babel/plugin-proposal-class-properties": "7.4.4",
10 | "@babel/plugin-proposal-decorators": "7.4.4",
11 | "@babel/preset-env": "7.4.4",
12 | "@babel/preset-react": "7.0.0",
13 | "@cypress/webpack-preprocessor": "4.0.3",
14 | "autoprefixer": "9.5.1",
15 | "babel-loader": "8.0.5",
16 | "babel-plugin-lodash": "3.3.4",
17 | "circular-dependency-plugin": "5.0.2",
18 | "clean-webpack-plugin": "2.0.1",
19 | "css-loader": "2.1.1",
20 | "dotenv-webpack": "1.7.0",
21 | "html-webpack-plugin": "3.2.0",
22 | "mini-css-extract-plugin": "0.6.0",
23 | "postcss-advanced-variables": "3.0.0",
24 | "postcss-automath": "1.0.1",
25 | "postcss-custom-properties": "8.0.10",
26 | "postcss-import": "12.0.1",
27 | "postcss-loader": "3.0.0",
28 | "postcss-nested": "4.1.2",
29 | "postcss-scss": "2.0.0",
30 | "postcss-strip-inline-comments": "0.1.5",
31 | "react-hot-loader": "4.8.4",
32 | "speed-measure-webpack-plugin": "1.3.1",
33 | "style-loader": "0.23.1",
34 | "svg-inline-loader": "0.8.0",
35 | "terser-webpack-plugin": "1.2.3",
36 | "thread-loader": "2.1.2",
37 | "url-loader": "1.1.2",
38 | "webpack": "4.30.0",
39 | "webpack-bundle-analyzer": "3.3.2",
40 | "webpack-cli": "3.3.1",
41 | "webpack-dev-server": "3.3.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/styles/themes.scss:
--------------------------------------------------------------------------------
1 | .light {
2 | --n0: rgb(255, 255, 255);
3 | --n100: rgb(186, 186, 186);
4 | --n10: rgb(249, 249, 249);
5 | --n10a3: rgba(249, 249, 249, 0.3);
6 | --n20: rgb(245, 245, 245);
7 | --n30: rgb(221, 221, 221);
8 | --n500: rgb(136, 136, 136);
9 | --n600: rgb(102, 102, 102);
10 | --n900: rgb(0, 0, 0);
11 |
12 | --b100: rgb(219, 237, 251);
13 | --b300: rgb(179, 214, 252);
14 | --b500: rgb(14, 123, 249);
15 | --b500a3: rgba(14, 123, 249, 0.3);
16 | --b900: rgb(32, 39, 57);
17 |
18 | --g400: rgb(71, 215, 141);
19 | --g500: rgb(61, 189, 125);
20 | --g500a1: rgba(61, 189, 125, 0.1);
21 | --g500a2: rgba(61, 189, 125, 0.2);
22 |
23 | --r400: rgb(255, 100, 100);
24 | --r500: rgb(255, 0, 0);
25 | --r500a1: rgba(255, 0, 0, 0.1);
26 | --r500a2: rgba(255, 0, 0, 0.2);
27 | }
28 |
29 | .dark {
30 | --n0: rgb(25, 32, 48);
31 | --n100: rgb(114, 126, 151);
32 | --n10: rgb(39, 46, 62);
33 | --n10a3: rgba(39, 46, 62, 0.3);
34 | --n20: rgb(25, 44, 74);
35 | --n30: rgb(67, 75, 111);
36 | --n500: rgb(117, 128, 154);
37 | --n600: rgb(255, 255, 255);
38 | --n900: rgb(255, 255, 255);
39 |
40 | --b100: rgb(219, 237, 251);
41 | --b300: rgb(39, 46, 62);
42 | --b500: rgb(14, 123, 249);
43 | --b500a3: rgba(14, 123, 249, 0.3);
44 | --b900: rgb(32, 39, 57);
45 |
46 | --g400: rgb(0, 220, 103);
47 | --g500: rgb(0, 197, 96);
48 | --g500a1: rgba(0, 197, 96, 0.1);
49 | --g500a2: rgba(0, 197, 96, 0.2);
50 |
51 | --r400: rgb(248, 23, 1);
52 | --r500: rgb(221, 23, 1);
53 | --r500a1: rgba(221, 23, 1, 0.1);
54 | --r500a2: rgba(221, 23, 1, 0.2);
55 | }
56 |
--------------------------------------------------------------------------------
/webpack-custom/webpack.config.js:
--------------------------------------------------------------------------------
1 | const pluginHot = require('./plugins/pluginHot');
2 | const pluginHtml = require('./plugins/pluginHtml');
3 | const pluginClean = require('./plugins/pluginClean');
4 | const pluginDefine = require('./plugins/pluginDefine');
5 | const pluginExtract = require('./plugins/pluginExtract');
6 | const pluginAnalyzer = require('./plugins/pluginAnalyzer');
7 | const pluginSpeedMeasure = require('./plugins/pluginSpeedMeasure');
8 | const pluginCircularDependency = require('./plugins/pluginCircularDependency');
9 |
10 | const {
11 | getParamAsBoolean,
12 | getParam,
13 | isProduction,
14 | } = require('./utils/envParams');
15 |
16 | module.exports = pluginSpeedMeasure.wrap({
17 | mode: getParam('NODE_ENV'),
18 | node: require('./config/configNode'),
19 | stats: require('./config/configStats'),
20 | entry: require('./config/configEntry'),
21 | output: require('./config/configOutput'),
22 | module: require('./config/configModule'),
23 | resolve: require('./config/configResolve'),
24 | devtool: require('./config/configDevTool'),
25 | devServer: require('./config/configDevServer'),
26 | performance: require('./config/configPerformance'),
27 | optimization: require('./config/configOptimization'),
28 | plugins: [
29 | isProduction && pluginClean,
30 | pluginHtml,
31 | pluginDefine,
32 | getParamAsBoolean('HOT_RELOAD') && pluginHot,
33 | getParamAsBoolean('CSS_EXTRACT') && pluginExtract,
34 | getParamAsBoolean('BUNDLE_ANALYZER') && pluginAnalyzer,
35 | getParamAsBoolean('CIRCULAR_CHECK') && pluginCircularDependency,
36 | ].filter(Boolean),
37 | });
38 |
--------------------------------------------------------------------------------
/src/api/utils/validateResponse.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import { validateObjects } from 'utils';
4 |
5 | export function validateResponse(route, requestParams) {
6 | return function promiseCallback(responseData) {
7 | // валидация ответов в виде массива
8 | if (_.isPlainObject(route.responseArray)) {
9 | if (!_.isArray(responseData)) {
10 | throw new Error('request: response expected to be an array');
11 | }
12 |
13 | responseData.forEach((responseItem, index) => {
14 | try {
15 | validateObjects(
16 | {
17 | validatorsObject: route.responseArray,
18 | targetObject: responseItem,
19 | },
20 | requestParams
21 | );
22 | } catch (error) {
23 | throw new Error(
24 | `request: responseItem[${index}] param ${
25 | error.message
26 | } has wrong value. Requested url: ${route.url}`
27 | );
28 | }
29 | });
30 | }
31 |
32 | // валидация ответов в виде объекта
33 | else if (_.isPlainObject(route.responseObject)) {
34 | try {
35 | validateObjects(
36 | {
37 | validatorsObject: route.responseObject,
38 | targetObject: responseData,
39 | },
40 | requestParams
41 | );
42 | } catch (error) {
43 | throw new Error(
44 | `request: response param ${
45 | error.message
46 | } has wrong value. Requested url: ${route.url}`
47 | );
48 | }
49 | }
50 |
51 | return responseData;
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/MarketList/MarketList.scss:
--------------------------------------------------------------------------------
1 | @import "mixins.scss";
2 |
3 | .block {
4 | @include block($col_width * 2);
5 | @include table;
6 |
7 | float: left;
8 | padding: $col_spacing;
9 |
10 | .row {
11 | user-select: none;
12 |
13 | &:hover {
14 | background: $N20;
15 | }
16 |
17 | &.active {
18 | background: $N20 !important;
19 | cursor: default;
20 | font-weight: bold;
21 | }
22 |
23 | .cell:first-child {
24 | text-transform: uppercase;
25 | }
26 | }
27 |
28 | .themeBlock {
29 | float: right;
30 |
31 | .themeToggler {
32 | @include inline-top;
33 |
34 | width: 16px;
35 | height: 16px;
36 | border: 1px solid #000;
37 | cursor: pointer;
38 | margin-right: 10px;
39 | margin-top: 2px;
40 | transition: border-color $trans;
41 |
42 | &.light {
43 | background: #fff;
44 | }
45 |
46 | &.dark {
47 | background: #000;
48 | margin-right: 20px;
49 | }
50 |
51 | &:hover,
52 | &.active {
53 | border-color: $B500;
54 | }
55 | }
56 |
57 | .lnToggler {
58 | @include inline-top;
59 |
60 | width: 32px;
61 | height: 16px;
62 | cursor: pointer;
63 | margin-right: 10px;
64 | margin-top: 2px;
65 | opacity: 0.5;
66 | transition: opacity $trans;
67 | border: 1px solid #000;
68 | background-size: 100% 100%;
69 |
70 | &.ru {
71 | background-image: url("assets/russia-flag-icon-32.png");
72 | }
73 |
74 | &.en {
75 | background-image: url("assets/united-kingdom-flag-icon-32.png");
76 | margin-right: 0;
77 | }
78 |
79 | &:hover,
80 | &.active {
81 | opacity: 1;
82 | }
83 | }
84 | }
85 | }
86 |
87 | .alignRight {
88 | text-align: right;
89 | }
90 |
--------------------------------------------------------------------------------
/dist/runtime.d370529eaf8353423ae4.js:
--------------------------------------------------------------------------------
1 | !function(e){function webpackJsonpCallback(r){for(var n,u,o=r[0],c=r[1],p=r[2],i=0,l=[];i
{
17 | messageWithValues = formattedMessage.replace(`{${paramName}}`, value);
18 | });
19 |
20 | return messageWithValues;
21 | }
22 |
23 | function replacePlurals(values, formattedMessage) {
24 | if (!_.isPlainObject(values)) {
25 | return formattedMessage;
26 | }
27 |
28 | let messageWithPlurals = formattedMessage;
29 |
30 | Object.entries(values).forEach(([paramName, value]) => {
31 | const pluralPattern = new RegExp(`{${paramName}:\\s([^}]*)}`);
32 | const pluralMatch = formattedMessage.match(pluralPattern);
33 |
34 | if (pluralMatch && pluralMatch[1]) {
35 | messageWithPlurals = formattedMessage.replace(
36 | pluralPattern,
37 | declOfNum(value, pluralMatch[1].split(','))
38 | );
39 | }
40 | });
41 |
42 | return messageWithPlurals;
43 | }
44 |
45 | export function useLocalization(filename, messages) {
46 | const {
47 | i18n: { i18n, currentLanguage },
48 | } = useStore();
49 |
50 | return function getLn(text, values) {
51 | const key = _.findKey(messages, message => message === text);
52 | const localizedText = _.get(i18n, [filename, key]);
53 |
54 | if (!localizedText && showNoTextMessage) {
55 | console.error(
56 | `useLocalization: no localization for lang '${currentLanguage}' in ${filename} ${key}`
57 | );
58 | }
59 |
60 | let formattedMessage = localizedText || text;
61 | formattedMessage = replaceDynamicParams(values, formattedMessage);
62 | formattedMessage = replacePlurals(values, formattedMessage);
63 |
64 | return formattedMessage;
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/MarketList/PairsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | import { useStore } from 'hooks';
5 | import { detectNilEquality, formatPercentage, precise, observer } from 'utils';
6 | import { LinkConnected } from 'components/Link';
7 |
8 | import styles from './MarketList.scss';
9 | import { routes } from 'routes';
10 |
11 | function PairsList() {
12 | const {
13 | marketsList: { list },
14 | currentTP,
15 | } = useStore();
16 | const numericCellClassName = cn(styles.cell, styles.alignRight);
17 |
18 | return (
19 |
20 | {list.map((TPInfo, index) => {
21 | const {
22 | currency,
23 | tradedCurrency,
24 | lastPrice,
25 | change24hPercentage,
26 | } = TPInfo;
27 | const highlightedCellClassName = detectNilEquality({
28 | num: change24hPercentage,
29 | onMore: styles.up,
30 | onLess: styles.down,
31 | });
32 |
33 | const isActive =
34 | currency === currentTP.currency &&
35 | tradedCurrency === currentTP.tradedCurrency;
36 |
37 | return (
38 |
47 |
48 | {currency} / {tradedCurrency}
49 |
50 |
51 | {precise(lastPrice, tradedCurrency)}
52 |
53 |
54 | {formatPercentage(change24hPercentage)}
55 |
56 |
57 | );
58 | })}
59 |
60 | );
61 | }
62 |
63 | export const PairsListConnected = observer(PairsList);
64 |
--------------------------------------------------------------------------------
/src/stores/MarketsListStore.js:
--------------------------------------------------------------------------------
1 | import { makeObservable } from 'utils';
2 | import { request, apiRoutes } from 'api';
3 |
4 | @makeObservable
5 | export class MarketsListStore {
6 | /**
7 | * @param rootStore {RootStore}
8 | */
9 | constructor(rootStore) {
10 | this.rootStore = rootStore;
11 | }
12 |
13 | list = [];
14 | symbolsList = [];
15 | currentMarket = null;
16 | availableMarkets = ['btc', 'eth', 'usd'];
17 |
18 | fetchSymbolsList() {
19 | if (this.symbolsList.length > 0) {
20 | return Promise.resolve();
21 | }
22 |
23 | return request(apiRoutes.symbolsList)
24 | .then(this.fetchSymbolsSuccess)
25 | .catch(this.fetchSymbolsError);
26 | }
27 | fetchSymbolsSuccess(data) {
28 | this.symbolsList = data;
29 | }
30 | fetchSymbolsError(error) {
31 | console.error(error);
32 | }
33 |
34 | fetchMarketList(market, prevMarket) {
35 | const requestParams = {
36 | vs_currency: market.toLocaleLowerCase(),
37 | order: 'market_cap_desc',
38 | per_page: 13,
39 | page: 1,
40 | sparkline: false,
41 | };
42 |
43 | return request(apiRoutes.marketsList, requestParams)
44 | .then(data => this.fetchMarketListSuccess(data, requestParams))
45 | .catch(error => this.fetchMarketListError(error, prevMarket));
46 | }
47 | fetchMarketListSuccess(data, requestParams) {
48 | let hasDuplicatedValues = false;
49 |
50 | const listData = data.reduce((arr, symbolData) => {
51 | const { symbol, current_price, price_change_percentage_24h } = symbolData;
52 |
53 | if (symbol === requestParams.vs_currency) {
54 | hasDuplicatedValues = true;
55 |
56 | return arr;
57 | }
58 |
59 | arr.push({
60 | currency: symbol,
61 | tradedCurrency: requestParams.vs_currency,
62 | change24hPercentage: price_change_percentage_24h,
63 | lastPrice: current_price,
64 | });
65 |
66 | return arr;
67 | }, []);
68 |
69 | this.list = hasDuplicatedValues ? listData : listData.slice(1);
70 | }
71 | fetchMarketListError(error, prevMarket) {
72 | this.currentMarket = prevMarket;
73 |
74 | console.error(error);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/styles/reset.scss:
--------------------------------------------------------------------------------
1 | @-webkit-keyframes bugfix {
2 | from {
3 | padding: 0;
4 | }
5 |
6 | to {
7 | padding: 0;
8 | }
9 | }
10 |
11 | *,
12 | *:before,
13 | *:after {
14 | -webkit-box-sizing: border-box;
15 | -moz-box-sizing: border-box;
16 | box-sizing: border-box;
17 | }
18 |
19 | html,
20 | body {
21 | height: 100%;
22 | -webkit-text-size-adjust: 100%;
23 | -ms-text-size-adjust: 100%;
24 | -webkit-animation: bugfix infinite 1s;
25 | font-family: "PT Sans", sans-serif;
26 | }
27 |
28 | html,
29 | body,
30 | div,
31 | span,
32 | cite,
33 | code,
34 | figure,
35 | h1,
36 | h2,
37 | h3,
38 | h4,
39 | h5,
40 | h6,
41 | p,
42 | blockquote,
43 | pre,
44 | small,
45 | sub,
46 | sup,
47 | b,
48 | i,
49 | a,
50 | ol,
51 | ul,
52 | li,
53 | footer,
54 | fieldset,
55 | form,
56 | label,
57 | legend,
58 | table,
59 | caption,
60 | tr,
61 | th,
62 | td,
63 | audio,
64 | video {
65 | margin: 0;
66 | padding: 0;
67 | border: 0;
68 | font-size: 100%;
69 | font: inherit;
70 | vertical-align: baseline;
71 | background: transparent;
72 | }
73 |
74 | b {
75 | font-weight: bold;
76 | }
77 |
78 | i {
79 | font-style: italic;
80 | }
81 |
82 | input,
83 | textarea,
84 | select {
85 | font: inherit;
86 | font-size: 100%;
87 | margin: 0;
88 | line-height: normal;
89 | }
90 |
91 | input,
92 | select {
93 | vertical-align: top;
94 | -webkit-appearance: none;
95 |
96 | &::-moz-focus-inner {
97 | padding: 0;
98 | border: 0;
99 | }
100 | }
101 |
102 | input[type="search"] {
103 | -webkit-appearance: textfield;
104 |
105 | &::-webkit-search-cancel-button,
106 | &::-webkit-search-decoration {
107 | -webkit-appearance: none;
108 | }
109 | }
110 |
111 | audio,
112 | video {
113 | display: inline-block;
114 | }
115 |
116 | sup {
117 | vertical-align: text-top;
118 | }
119 |
120 | sub {
121 | vertical-align: text-bottom;
122 | }
123 |
124 | img {
125 | border: 0;
126 | vertical-align: top;
127 | }
128 |
129 | table {
130 | border-collapse: collapse;
131 | border-spacing: 0;
132 | }
133 |
134 | textarea {
135 | overflow: auto;
136 | vertical-align: top;
137 | }
138 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/Chart/Chart.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Highcharts from 'highcharts/highstock';
3 | import cn from 'classnames';
4 |
5 | import { observer } from 'utils';
6 | import { useLocalization, useStore } from 'hooks';
7 |
8 | import { chartConfig } from './config/chartConfig';
9 | import fill from './assets/fill.svg';
10 | import styles from './Chart.scss';
11 | import { messages } from './messages';
12 |
13 | function Chart() {
14 | const {
15 | currentTP: { chartPrices, chartVolumes },
16 | i18n: { currentLanguage },
17 | } = useStore();
18 | const chartEl = React.useRef(null);
19 | const chartInstance = React.useRef(null);
20 |
21 | const getLn = useLocalization(__filename, messages);
22 |
23 | React.useEffect(() => {
24 | const localizedButtons = chartConfig.rangeSelector.buttons.map(
25 | buttonConfig => ({
26 | ...buttonConfig,
27 | text: getLn(buttonConfig.text, {
28 | count: buttonConfig.count,
29 | }),
30 | })
31 | );
32 | const rangeSelector = {
33 | ...chartConfig.rangeSelector,
34 | buttons: localizedButtons,
35 | };
36 |
37 | if (!chartInstance.current) {
38 | chartInstance.current = Highcharts.stockChart(chartEl.current, {
39 | ...chartConfig,
40 | rangeSelector,
41 | });
42 | } else {
43 | chartInstance.current.series[0].setData(chartPrices);
44 | chartInstance.current.series[1].setData(chartVolumes);
45 |
46 | chartInstance.current.update({
47 | rangeSelector,
48 | });
49 | }
50 | }, [chartPrices, currentLanguage]);
51 |
52 | return (
53 |
73 | );
74 | }
75 |
76 | export const ChartConnected = observer(Chart);
77 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/OrderBook/OrderBook.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | import { detectNilEquality, precise, observer } from 'utils';
5 | import { useLocalization, useStore } from 'hooks';
6 | import { OffersTable } from './OffersTable';
7 |
8 | import styles from './OrderBook.scss';
9 | import { messages } from './messages';
10 |
11 | function OrderBook() {
12 | const {
13 | currentTP: {
14 | currency,
15 | tradedCurrency,
16 | lastPrice,
17 | offersSell,
18 | offersBuy,
19 | offersSellTotal,
20 | offersBuyTotal,
21 | },
22 | } = useStore();
23 | const getLn = useLocalization(__filename, messages);
24 |
25 | const currentPriceClassname = detectNilEquality({
26 | num: lastPrice,
27 | onLess: styles.down,
28 | onMore: styles.up,
29 | });
30 |
31 | const maxTotal = Math.max(offersSellTotal, offersBuyTotal);
32 |
33 | return (
34 |
35 |
36 |
{getLn(messages.orderBook)}
37 |
38 |
39 |
40 | {getLn(messages.price)} ({tradedCurrency})
41 |
42 |
43 | {getLn(messages.amount)} ({currency})
44 |
45 |
46 | {getLn(messages.total)} ({tradedCurrency})
47 |
48 |
49 |
57 |
58 | {precise(lastPrice, tradedCurrency)}
59 |
60 |
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | export const OrderBookConnected = observer(OrderBook);
74 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/TPInfo/TPInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | import { detectNilEquality, formatPercentage, precise, observer } from 'utils';
5 | import { useLocalization, useStore } from 'hooks';
6 |
7 | import styles from './TPInfo.scss';
8 | import { messages } from './messages';
9 |
10 | function TPInfo() {
11 | const {
12 | currentTP: {
13 | usdValue,
14 | name,
15 | lastPrice,
16 | change24h,
17 | change24hPercentage,
18 | high24h,
19 | low24h,
20 | tradedCurrency,
21 | },
22 | } = useStore();
23 | const getLn = useLocalization(__filename, messages);
24 |
25 | const columnsArray = [
26 | {
27 | label: getLn(messages.lastPrice),
28 | data: precise(lastPrice, tradedCurrency),
29 | appendElement:
30 | tradedCurrency === 'usd' ? null : (
31 | {usdValue}
32 | ),
33 | },
34 | {
35 | label: getLn(messages.change24h),
36 | data: precise(change24h, tradedCurrency),
37 | classname: detectNilEquality({
38 | num: change24hPercentage,
39 | onMore: styles.up,
40 | onLess: styles.down,
41 | }),
42 | appendElement: (
43 |
44 | {formatPercentage(change24hPercentage)}
45 |
46 | ),
47 | },
48 | {
49 | label: getLn(messages.high24h),
50 | data: precise(high24h, tradedCurrency),
51 | },
52 | {
53 | label: getLn(messages.low24h),
54 | data: precise(low24h, tradedCurrency),
55 | },
56 | ];
57 |
58 | return (
59 |
60 |
61 |
{name}
62 |
63 | {columnsArray.map(({ label, data, classname, appendElement }, i) => (
64 |
65 |
{label}
66 |
67 | {data}
68 | {appendElement}
69 |
70 |
71 | ))}
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
79 | export const TPInfoConnected = observer(TPInfo);
80 |
--------------------------------------------------------------------------------
/eslint-custom/rules/stylistic-issues.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | array-bracket-spacing: [error, never]
3 | block-spacing: [error, always]
4 | brace-style: [error, 1tbs]
5 | camelcase: off
6 | capitalized-comments: off
7 | comma-dangle: [error, always-multiline]
8 | comma-spacing: error
9 | comma-style: [error, last]
10 | computed-property-spacing: [error, never]
11 | consistent-this: [error, that]
12 | eol-last: off
13 | func-call-spacing: [error, never]
14 | func-name-matching: off
15 | func-names: [error, always]
16 | func-style: [error, declaration]
17 | id-match: off
18 | indent:
19 | - error
20 | - 2
21 | - SwitchCase: 1
22 | jsx-quotes: [error, prefer-double]
23 | key-spacing: error
24 | keyword-spacing:
25 | - error
26 | -
27 | before: true
28 | after: true
29 | line-comment-position: off
30 | linebreak-style: off
31 | lines-around-comment: off
32 | lines-around-directive: error
33 | max-depth:
34 | - error
35 | - max: 4
36 | max-len:
37 | - warn
38 | -
39 | code: 100
40 | ignoreComments: true
41 | ignorePattern: "^import\\W.*"
42 | max-lines: off
43 | max-nested-callbacks:
44 | - error
45 | - max: 5
46 | max-params:
47 | - error
48 | - max: 8
49 | max-statements:
50 | - error
51 | - max: 50
52 | max-statements-per-line:
53 | - error
54 | - max: 1
55 | multiline-ternary: ["error", "always"]
56 | new-cap: off
57 | new-parens: error
58 | newline-per-chained-call:
59 | - error
60 | - ignoreChainWithDepth: 3
61 | no-array-constructor: error
62 | no-bitwise: error
63 | no-continue: error
64 | no-lonely-if: error
65 | no-mixed-operators: warn
66 | no-mixed-spaces-and-tabs: error
67 | no-multi-assign: error
68 | no-multiple-empty-lines:
69 | - error
70 | -
71 | max: 1
72 | maxBOF: 0
73 | maxEOF: 0
74 | no-negated-condition: off
75 | no-nested-ternary: off
76 | no-new-object: error
77 | no-plusplus: off
78 | no-restricted-syntax:
79 | - error
80 | - WithStatement
81 | no-tabs: error
82 | no-ternary: off
83 | no-trailing-spaces: error
84 | no-underscore-dangle: off
85 | no-unneeded-ternary: error
86 | no-whitespace-before-property: error
87 | nonblock-statement-body-position: off
88 | object-curly-newline:
89 | - error
90 | - multiline: true
91 | object-curly-spacing: [error, always]
92 | object-property-newline: off
93 | one-var: [error, never]
94 | one-var-declaration-per-line: error
95 | operator-assignment: [error, always]
96 | operator-linebreak:
97 | - error
98 | - after
99 | - overrides:
100 | "?": before
101 | ":": before
102 | padded-blocks: [error, never]
103 | quote-props: [error, as-needed]
104 | quotes: [error, single]
105 | require-jsdoc: off
106 | semi: off
107 | semi-spacing: off
108 | sort-keys: off
109 | sort-vars: off
110 | space-before-blocks: error
111 | space-before-function-paren: [error, never]
112 | space-in-parens: [error, never]
113 | space-infix-ops: error
114 | space-unary-ops: error
115 | spaced-comment: [error, always]
116 | template-tag-spacing: [error, never]
117 | unicode-bom: off
118 | wrap-regex: error
119 |
--------------------------------------------------------------------------------
/src/styles/layout.scss:
--------------------------------------------------------------------------------
1 | @import "constants.scss";
2 | @import "mixins.scss";
3 |
4 | @mixin order-book-cell-widths {
5 | float: left;
6 |
7 | &:nth-child(1) {
8 | width: 40%;
9 | }
10 |
11 | &:nth-child(2) {
12 | width: 30%;
13 | }
14 |
15 | &:nth-child(3) {
16 | width: 30%;
17 | }
18 | }
19 |
20 | @mixin block($width) {
21 | float: left;
22 | width: $width;
23 | padding: $col_spacing;
24 |
25 | .blockInner {
26 | border: 1px solid $border_color;
27 | border-radius: $border_radius;
28 | overflow: hidden;
29 | position: relative;
30 | }
31 |
32 | .header {
33 | font-weight: bold;
34 | border-bottom: 1px solid $border_color;
35 | padding: $table_col_padding_top $table_col_padding_side;
36 | font-size: 13px;
37 | color: $N900;
38 | }
39 |
40 | .body {
41 | @include clearfix;
42 | }
43 | }
44 |
45 | @mixin table() {
46 | .tableHeader {
47 | @include clearfix;
48 |
49 | border-bottom: 1px solid $border_color;
50 |
51 | .cell {
52 | @include order-book-cell-widths;
53 |
54 | background: $light_bg_color;
55 | color: $N600;
56 | font-size: 11px;
57 | padding: $table_col_padding_top $table_col_padding_side;
58 | }
59 | }
60 |
61 | .tableBody {
62 | .row {
63 | @include clearfix;
64 |
65 | display: block;
66 | border-bottom: 1px solid $N20;
67 | position: relative;
68 | height: $line_height + $table_col_padding_top * 2;
69 | color: $N600;
70 |
71 | &:last-child {
72 | border-bottom: 0;
73 | }
74 | }
75 |
76 | .cell {
77 | @include order-book-cell-widths;
78 |
79 | font-size: 11px;
80 | padding: $table_col_padding_top $table_col_padding_side;
81 |
82 | &.down {
83 | color: $R500;
84 | }
85 |
86 | &.up {
87 | color: $G500;
88 | }
89 | }
90 | }
91 | }
92 |
93 | @mixin tabs() {
94 | @include clearfix;
95 |
96 | border-bottom: 1px solid $B300;
97 | width: 100%;
98 | text-transform: uppercase;
99 |
100 | .tab {
101 | float: left;
102 | padding: $table_col_padding_top + 1 0;
103 | cursor: pointer;
104 | user-select: none;
105 | font-size: 12px;
106 | color: $N100;
107 | transition: color $trans;
108 | border-left: 1px solid transparent;
109 | border-right: 1px solid transparent;
110 | position: relative;
111 | margin: -1px 0;
112 | text-align: center;
113 |
114 | &:after {
115 | content: "";
116 | position: absolute;
117 | display: block;
118 | width: 100%;
119 | height: 1px;
120 | background: none;
121 | top: 0;
122 | left: 0;
123 | z-index: 1;
124 | }
125 |
126 | &:hover {
127 | color: $B500;
128 | }
129 |
130 | &.active {
131 | color: $B500 !important;
132 | background: $light_bg_color;
133 | border-color: $B300;
134 | cursor: default;
135 | font-weight: bold;
136 |
137 | &:after {
138 | background: $B300;
139 | }
140 | }
141 |
142 | &:first-child {
143 | border-left: 0 !important;
144 | }
145 |
146 | &:last-child {
147 | border-right: 0 !important;
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/stores/RouterStore.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import { makeObservable } from 'utils';
4 | import { routes } from 'routes';
5 |
6 | @makeObservable
7 | export class RouterStore {
8 | /**
9 | * @param rootStore {RootStore}
10 | */
11 | constructor(rootStore) {
12 | this.rootStore = rootStore;
13 | this.currentRoute = this._fillRouteSchemaFromUrl();
14 |
15 | window.addEventListener('popstate', () => {
16 | this.currentRoute = this._fillRouteSchemaFromUrl();
17 | });
18 | }
19 |
20 | currentRoute = null;
21 |
22 | _fillRouteSchemaFromUrl() {
23 | const { pathname } = window.location;
24 | const pathnameArray = pathname.split('/');
25 | const routeName = this._getRouteNameMatchingUrl(pathnameArray);
26 |
27 | if (!routeName) {
28 | // Default page
29 | if (pathname.indexOf(PUBLIC_URL) === 0) {
30 | return this.goTo(routes.marketDetailed, {
31 | market: 'usd',
32 | pair: 'eth-usd',
33 | });
34 | }
35 |
36 | const currentRoute = routes.error404;
37 | window.history.pushState(null, null, currentRoute.path);
38 |
39 | return currentRoute;
40 | }
41 |
42 | const route = routes[routeName];
43 | const routePathnameArray = route.path.split('/');
44 |
45 | const params = {};
46 |
47 | routePathnameArray.forEach((pathParam, i) => {
48 | const urlParam = pathnameArray[i];
49 |
50 | if (pathParam.indexOf(':') === 0) {
51 | const paramName = pathParam.replace(':', '');
52 | params[paramName] = urlParam;
53 | }
54 | });
55 |
56 | return Object.assign({}, route, { params, isLoading: true });
57 | }
58 |
59 | _getRouteNameMatchingUrl(pathnameArray) {
60 | return _.findKey(routes, route => {
61 | const routePathnameArray = route.path.split('/');
62 |
63 | if (routePathnameArray.length !== pathnameArray.length) {
64 | return false;
65 | }
66 |
67 | for (let i = 0; i < routePathnameArray.length; i++) {
68 | const pathParam = routePathnameArray[i];
69 | const urlParam = pathnameArray[i];
70 |
71 | if (pathParam.indexOf(':') !== 0) {
72 | if (pathParam !== urlParam) {
73 | return false;
74 | }
75 | } else {
76 | const paramName = pathParam.replace(':', '');
77 | const paramMask = _.get(route.masks, paramName);
78 |
79 | if (paramMask && !paramMask.test(urlParam)) {
80 | return false;
81 | }
82 | }
83 | }
84 |
85 | return true;
86 | });
87 | }
88 |
89 | replaceDynamicParams(route, params) {
90 | return Object.entries(params).reduce((pathname, [paramName, value]) => {
91 | return pathname.replace(`:${paramName}`, value);
92 | }, route.path);
93 | }
94 |
95 | goTo(route, params) {
96 | if (this.currentRoute && route.name === this.currentRoute.name) {
97 | if (_.isEqual(this.currentRoute.params, params)) {
98 | return null;
99 | }
100 |
101 | this.currentRoute.isLoading = true;
102 | this.currentRoute.params = params;
103 |
104 | const newPathname = this.replaceDynamicParams(this.currentRoute, params);
105 |
106 | window.history.pushState(null, null, newPathname);
107 |
108 | return null;
109 | }
110 |
111 | const newPathname = this.replaceDynamicParams(route, params);
112 |
113 | window.history.pushState(null, null, newPathname);
114 |
115 | this.currentRoute = this._fillRouteSchemaFromUrl();
116 |
117 | return this.currentRoute;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/pages/MarketDetailed/Chart/Chart.scss:
--------------------------------------------------------------------------------
1 | @import "mixins.scss";
2 |
3 | .block {
4 | @include block($col_width * 4);
5 |
6 | padding: $col_spacing;
7 |
8 | .header.absolute {
9 | border-bottom: 0;
10 | position: absolute;
11 | z-index: 3;
12 | top: 0;
13 | left: 0;
14 | }
15 |
16 | .credits {
17 | color: $N100;
18 | font-weight: normal;
19 | font-size: 12px;
20 | }
21 | }
22 |
23 | .chart {
24 | :global {
25 | .highcharts-root {
26 | font-family: "Roboto", sans-serif !important;
27 | font-size: 12px !important;
28 | line-height: $line_height;
29 | }
30 |
31 | .highcharts-background {
32 | fill: $N0;
33 | }
34 |
35 | .highcharts-scrollbar {
36 | transform: translate(0, 351px) !important;
37 | }
38 |
39 | .highcharts-scrollbar-button,
40 | .highcharts-scrollbar-thumb {
41 | fill: $border_color;
42 | cursor: pointer;
43 | transition: fill $trans;
44 |
45 | &:hover {
46 | fill: $N100;
47 | }
48 | }
49 |
50 | .highcharts-scrollbar-track {
51 | stroke: $border_color;
52 | fill: none;
53 | }
54 |
55 | .highcharts-scrollbar-rifles {
56 | cursor: pointer;
57 | }
58 |
59 | .highcharts-button {
60 | $rightOffset: 67px;
61 |
62 | .highcharts-button-box {
63 | fill: none;
64 | }
65 |
66 | text {
67 | fill: $N100 !important;
68 | transition: fill $trans;
69 | font-size: 12px;
70 | }
71 |
72 | &.highcharts-button-hover {
73 | text {
74 | fill: $B500 !important;
75 | }
76 | }
77 |
78 | &.highcharts-button-pressed {
79 | text {
80 | fill: $B500 !important;
81 | font-weight: bold;
82 | }
83 | }
84 |
85 | &:nth-of-type(1) {
86 | transform: translateX($rightOffset - 67px);
87 | }
88 |
89 | &:nth-of-type(2) {
90 | transform: translateX($rightOffset);
91 | }
92 |
93 | &:nth-of-type(3) {
94 | transform: translateX($rightOffset + 70px);
95 | }
96 | }
97 |
98 | .highcharts-tooltip {
99 | .highcharts-tooltip-box {
100 | > span {
101 | background: $N20;
102 | padding: $table_col_padding_side;
103 | font-family: inherit !important;
104 | font-size: 12px !important;
105 | line-height: $line_height;
106 | color: $N900 !important;
107 | }
108 | }
109 | }
110 |
111 | .highcharts-point {
112 | stroke-width: 0.5;
113 | stroke: $N600;
114 |
115 | &.highcharts-point-up {
116 | fill: $G500;
117 |
118 | &.highcharts-point-hover {
119 | fill: $G400;
120 | }
121 | }
122 |
123 | &.highcharts-point-down {
124 | fill: $R500;
125 |
126 | &.highcharts-point-hover {
127 | fill: $R400;
128 | }
129 | }
130 |
131 | &.highcharts-color-1 {
132 | stroke-width: 0;
133 | fill: $border_color;
134 |
135 | &.highcharts-point-hover {
136 | fill: $N100;
137 | }
138 | }
139 | }
140 |
141 | .highcharts-series-1 {
142 | path:first-child {
143 | fill: url(#chartVolumeGradient) !important;
144 | }
145 |
146 | path:nth-child(2) {
147 | stroke: $B500 !important;
148 | stroke-width: 1;
149 | }
150 | }
151 |
152 | .highcharts-grid-line {
153 | stroke: $N20;
154 | }
155 |
156 | .highcharts-axis-labels {
157 | text {
158 | fill: $N900 !important;
159 | }
160 | }
161 | }
162 | }
163 |
164 | .svgGradient {
165 | position: absolute;
166 | left: -99999px;
167 |
168 | stop:first-child {
169 | stop-color: $B500a3;
170 | }
171 |
172 | stop:last-child {
173 | stop-color: $N0;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/api/_api.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import {
4 | omitParam,
5 | validateRequestParams,
6 | makeRequestUrl,
7 | makeRequest,
8 | validateResponse,
9 | startMetrics,
10 | stopMetrics,
11 | } from 'api/utils';
12 |
13 | export const apiRoutes = {
14 | symbolInfo: {
15 | url: params => `https://api.coingecko.com/api/v3/coins/${params.id}`,
16 | params: {
17 | id: omitParam,
18 | localization: _.isBoolean,
19 | community_data: _.isBoolean,
20 | developer_data: _.isBoolean,
21 | tickers: _.isBoolean,
22 | },
23 | responseObject: {
24 | id: _.isString,
25 | name: _.isString,
26 | symbol: _.isString,
27 | genesis_date: v => _.isString(v) || _.isNil(v),
28 | last_updated: _.isString,
29 | country_origin: _.isString,
30 |
31 | coingecko_rank: _.isNumber,
32 | coingecko_score: _.isNumber,
33 | community_score: _.isNumber,
34 | developer_score: _.isNumber,
35 | liquidity_score: _.isNumber,
36 | market_cap_rank: _.isNumber,
37 | block_time_in_minutes: _.isNumber,
38 | public_interest_score: _.isNumber,
39 |
40 | image: _.isPlainObject,
41 | links: _.isPlainObject,
42 | description: _.isPlainObject,
43 | market_data: _.isPlainObject,
44 | localization(value, requestParams) {
45 | if (requestParams.localization === false) {
46 | return true;
47 | }
48 |
49 | return _.isPlainObject(value);
50 | },
51 | community_data(value, requestParams) {
52 | if (requestParams.community_data === false) {
53 | return true;
54 | }
55 |
56 | return _.isPlainObject(value);
57 | },
58 | developer_data(value, requestParams) {
59 | if (requestParams.developer_data === false) {
60 | return true;
61 | }
62 |
63 | return _.isPlainObject(value);
64 | },
65 | public_interest_stats: _.isPlainObject,
66 |
67 | tickers(value, requestParams) {
68 | if (requestParams.tickers === false) {
69 | return true;
70 | }
71 |
72 | return _.isArray(value);
73 | },
74 | categories: _.isArray,
75 | status_updates: _.isArray,
76 | },
77 | },
78 | symbolsList: {
79 | url: 'https://api.coingecko.com/api/v3/coins/list',
80 | responseArray: {
81 | id: _.isString,
82 | symbol: _.isString,
83 | name: _.isString,
84 | },
85 | },
86 | rates: {
87 | url: 'https://api.coingecko.com/api/v3/exchange_rates',
88 | responseObject: {
89 | rates: _.isPlainObject,
90 | },
91 | },
92 | chartData: {
93 | url: 'https://api.coingecko.com/api/v3/coins/bitcoin/market_chart',
94 | params: {
95 | vs_currency: _.isString,
96 | id: _.isString,
97 | days: v => _.isNumber(v) || _.isString(v),
98 | },
99 | responseObject: {
100 | prices: _.isArray,
101 | total_volumes: _.isArray,
102 | },
103 | },
104 | marketsList: {
105 | url: 'https://api.coingecko.com/api/v3/coins/markets',
106 | params: {
107 | vs_currency: _.isString,
108 | order: _.isString,
109 | per_page: _.isNumber,
110 | page: _.isNumber,
111 | sparkline: _.isBoolean,
112 | },
113 | responseArray: {
114 | id: _.isString,
115 | symbol: _.isString,
116 | name: _.isString,
117 | image: _.isString,
118 | current_price: _.isNumber,
119 | market_cap: _.isNumber,
120 | market_cap_rank: _.isNumber,
121 | total_volume: _.isNumber,
122 | high_24h: _.isNumber,
123 | low_24h: _.isNumber,
124 | price_change_24h: _.isNumber,
125 | price_change_percentage_24h: _.isNumber,
126 | market_cap_change_24h: _.isNumber,
127 | market_cap_change_percentage_24h: _.isNumber,
128 | circulating_supply: _.isNumber,
129 | },
130 | },
131 | };
132 |
133 | export function request(route, params) {
134 | return Promise.resolve()
135 | .then(startMetrics(route, apiRoutes))
136 | .then(validateRequestParams(route, params))
137 | .then(makeRequestUrl(route, params))
138 | .then(makeRequest)
139 | .then(validateResponse(route, params))
140 | .then(stopMetrics(route, apiRoutes))
141 | .catch(error => {
142 | stopMetrics(route, apiRoutes)();
143 |
144 | throw error;
145 | });
146 | }
147 |
--------------------------------------------------------------------------------
/src/stores/CurrentTPStore.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | import { sum, makeObservable, precise } from 'utils';
4 | import { apiRoutes, request } from 'api';
5 |
6 | @makeObservable
7 | export class CurrentTPStore {
8 | /**
9 | * @param rootStore {RootStore}
10 | */
11 | constructor(rootStore) {
12 | this.rootStore = rootStore;
13 | }
14 |
15 | id = '';
16 | symbol = '';
17 | fullName = '';
18 | currency = '';
19 | tradedCurrency = '';
20 | low24h = 0;
21 | high24h = 0;
22 | lastPrice = 0;
23 | marketCap = 0;
24 | change24h = 0;
25 | change24hPercentage = 0;
26 | offersSell = [];
27 | offersBuy = [];
28 | chartPrices = [];
29 | chartVolumes = [];
30 |
31 | get name() {
32 | if (!this.currency) {
33 | return '';
34 | }
35 |
36 | return `${this.currency} / ${this.tradedCurrency}`;
37 | }
38 | get urlName() {
39 | if (!this.currency) {
40 | return '';
41 | }
42 |
43 | return `${this.currency.toLocaleLowerCase()}-${this.tradedCurrency.toLocaleLowerCase()}`;
44 | }
45 | get usdValue() {
46 | const { rates } = this.rootStore;
47 |
48 | if (!this.symbol || !rates.rates[this.symbol]) {
49 | return 0;
50 | }
51 |
52 | const usdRate = rates.rates.usd.value;
53 | const currentRate = rates.rates[this.symbol].value;
54 |
55 | return `$${precise(usdRate / currentRate, 'usd')}`;
56 | }
57 | get offersSellTotal() {
58 | if (this.offersSell.length === 0) {
59 | return 0;
60 | }
61 |
62 | return this.offersSell.map(({ total }) => total).reduce(sum);
63 | }
64 | get offersBuyTotal() {
65 | if (this.offersSell.length === 0) {
66 | return 0;
67 | }
68 |
69 | return this.offersBuy.map(({ total }) => total).reduce(sum);
70 | }
71 |
72 | fetchSymbol(params) {
73 | const { symbol, tradedCurrency } = params;
74 | const { marketsList } = this.rootStore;
75 |
76 | if (this.symbol === symbol) {
77 | return Promise.resolve();
78 | }
79 |
80 | const symbolData = marketsList.symbolsList.find(v => v.symbol === symbol);
81 |
82 | if (!symbolData) {
83 | console.error(`fetchSymbol: no symbol data for ${symbol}`);
84 |
85 | return Promise.resolve();
86 | }
87 |
88 | const requestParams = {
89 | id: symbolData.id,
90 | localization: false,
91 | community_data: false,
92 | developer_data: false,
93 | tickers: false,
94 | };
95 |
96 | return request(apiRoutes.symbolInfo, requestParams)
97 | .then(data => this.fetchSymbolSuccess(data, tradedCurrency))
98 | .catch(this.fetchSymbolError);
99 | }
100 | fetchSymbolSuccess(data, tradedCurrency) {
101 | const {
102 | id,
103 | symbol,
104 | name,
105 | market_data: {
106 | high_24h,
107 | low_24h,
108 | price_change_24h_in_currency,
109 | price_change_percentage_24h_in_currency,
110 | market_cap,
111 | current_price,
112 | },
113 | } = data;
114 |
115 | this.id = id;
116 | this.symbol = symbol;
117 | this.fullName = name;
118 | this.currency = symbol;
119 | this.tradedCurrency = tradedCurrency;
120 | this.lastPrice = current_price[tradedCurrency];
121 | this.high24h = high_24h[tradedCurrency];
122 | this.low24h = low_24h[tradedCurrency];
123 | this.change24h = price_change_24h_in_currency[tradedCurrency];
124 | this.change24hPercentage =
125 | price_change_percentage_24h_in_currency[tradedCurrency];
126 | this.marketCap = market_cap[tradedCurrency];
127 |
128 | this._generateOrders();
129 |
130 | return this.fetchChartData();
131 | }
132 | fetchSymbolError(error) {
133 | console.error(error);
134 | }
135 |
136 | fetchChartData() {
137 | const requestParams = {
138 | id: this.id,
139 | vs_currency: this.tradedCurrency,
140 | days: 200,
141 | };
142 |
143 | return request(apiRoutes.chartData, requestParams)
144 | .then(this.fetchChartDataSuccess)
145 | .catch(this.fetchChartDataError);
146 | }
147 | fetchChartDataSuccess(data) {
148 | const prices = [];
149 |
150 | for (let i = 0; i < data.prices.length; i += 1) {
151 | const prevClose = _.get(data.prices[i - 1], '[1]') || 0;
152 | const [date, close] = data.prices[i];
153 | const open = prevClose;
154 |
155 | let high = _.random(0.00001, open / 10);
156 | if (open > close) {
157 | high += open;
158 | } else {
159 | high += close;
160 | }
161 |
162 | let low = _.random(0.00001, open / 10);
163 | if (open > close) {
164 | low = close - low;
165 | } else {
166 | low = open - low;
167 | }
168 |
169 | prices.push([date, open, high, low, close]);
170 | }
171 |
172 | this.chartPrices = prices;
173 | this.chartVolumes = data.total_volumes;
174 | }
175 | fetchChartDataError(error) {
176 | console.error(error);
177 | }
178 |
179 | _generateOrders() {
180 | const offersSell = [];
181 | const offersBuy = [];
182 |
183 | let priceSell = this.lastPrice;
184 | let priceBuy = this.lastPrice;
185 |
186 | _.range(6).forEach(() => {
187 | priceSell += _.random(0.00001, priceSell / 100);
188 | priceBuy -= _.random(0.00001, priceBuy / 100);
189 |
190 | const amountSell = _.random(1, 100);
191 | const amountBuy = _.random(1, 100);
192 |
193 | offersSell.push({
194 | price: priceSell,
195 | amount: amountSell,
196 | total: priceSell * amountSell,
197 | });
198 |
199 | offersBuy.push({
200 | price: priceBuy,
201 | amount: amountBuy,
202 | total: priceBuy * amountBuy,
203 | });
204 | });
205 |
206 | this.offersSell = offersSell.sort((a, b) => b.price - a.price);
207 | this.offersBuy = offersBuy;
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/dist/app.6aeef33d963c36044219.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Roboto);
2 | @-webkit-keyframes styles__bugfix {
3 | from {
4 | padding: 0;
5 | }
6 |
7 | to {
8 | padding: 0;
9 | }
10 | }
11 |
12 | *,
13 | *:before,
14 | *:after {
15 | box-sizing: border-box;
16 | }
17 |
18 | html,
19 | body {
20 | height: 100%;
21 | -webkit-text-size-adjust: 100%;
22 | -ms-text-size-adjust: 100%;
23 | -webkit-animation: styles__bugfix infinite 1s;
24 | font-family: "PT Sans", sans-serif;
25 | }
26 |
27 | html,
28 | body,
29 | div,
30 | span,
31 | cite,
32 | code,
33 | figure,
34 | h1,
35 | h2,
36 | h3,
37 | h4,
38 | h5,
39 | h6,
40 | p,
41 | blockquote,
42 | pre,
43 | small,
44 | sub,
45 | sup,
46 | b,
47 | i,
48 | a,
49 | ol,
50 | ul,
51 | li,
52 | footer,
53 | fieldset,
54 | form,
55 | label,
56 | legend,
57 | table,
58 | caption,
59 | tr,
60 | th,
61 | td,
62 | audio,
63 | video {
64 | margin: 0;
65 | padding: 0;
66 | border: 0;
67 | font-size: 100%;
68 | font: inherit;
69 | vertical-align: baseline;
70 | background: transparent;
71 | }
72 |
73 | b {
74 | font-weight: bold;
75 | }
76 |
77 | i {
78 | font-style: italic;
79 | }
80 |
81 | input,
82 | textarea,
83 | select {
84 | font: inherit;
85 | font-size: 100%;
86 | margin: 0;
87 | line-height: normal;
88 | }
89 |
90 | input,
91 | select {
92 | vertical-align: top;
93 | -webkit-appearance: none;
94 | }
95 |
96 | input::-moz-focus-inner, select::-moz-focus-inner {
97 | padding: 0;
98 | border: 0;
99 | }
100 |
101 | input[type="search"] {
102 | -webkit-appearance: textfield;
103 | }
104 |
105 | input[type="search"]::-webkit-search-cancel-button,
106 | input[type="search"]::-webkit-search-decoration {
107 | -webkit-appearance: none;
108 | }
109 |
110 | audio,
111 | video {
112 | display: inline-block;
113 | }
114 |
115 | sup {
116 | vertical-align: text-top;
117 | }
118 |
119 | sub {
120 | vertical-align: text-bottom;
121 | }
122 |
123 | img {
124 | border: 0;
125 | vertical-align: top;
126 | }
127 |
128 | table {
129 | border-collapse: collapse;
130 | border-spacing: 0;
131 | }
132 |
133 | textarea {
134 | overflow: auto;
135 | vertical-align: top;
136 | }
137 |
138 |
139 | body {
140 | font-family: "Roboto", sans-serif;
141 | font-size: 14px;
142 | line-height: 20px;
143 | overflow-x: hidden;
144 | background: var(--n0);
145 | }::-moz-selection {
146 | background: var(--n900);
147 | color: var(--n0);
148 | fill: var(--n0);
149 | }::selection {
150 | background: var(--n900);
151 | color: var(--n0);
152 | fill: var(--n0);
153 | }a {
154 | text-decoration: none;
155 | }
156 |
157 | .GlobalLoader__loader {
158 | position: fixed;
159 | z-index: 100;
160 | left: 0;
161 | top: 0;
162 | width: 100%;
163 | height: 6px;
164 | background: var(--g500);
165 | opacity: 0;
166 | -webkit-transform: translateX(-100%);
167 | transform: translateX(-100%);
168 | }
169 |
170 | .GlobalLoader__loader.GlobalLoader__firstHalf {
171 | opacity: 1;
172 | -webkit-transform: translateX(-30%);
173 | transform: translateX(-30%);
174 | transition: opacity 0.5s ease,
175 | -webkit-transform 4s cubic-bezier(0.075, 0.85, 0.085, 0.885);
176 | transition: transform 4s cubic-bezier(0.075, 0.85, 0.085, 0.885),
177 | opacity 0.5s ease;
178 | transition: transform 4s cubic-bezier(0.075, 0.85, 0.085, 0.885), opacity 0.5s ease;
179 | }
180 |
181 | .GlobalLoader__loader.GlobalLoader__lastHalf {
182 | opacity: 0;
183 | -webkit-transform: translateX(50%);
184 | transform: translateX(50%);
185 | transition: opacity 0.5s ease, -webkit-transform 0.5s ease;
186 | transition: opacity 0.5s ease, transform 0.5s ease;
187 | transition: opacity 0.5s ease, transform 0.5s ease, -webkit-transform 0.5s ease;
188 | }
189 |
190 | .Chart__block {
191 | float: left;
192 | width: 50%;
193 | padding: 8px;
194 |
195 | padding: 8px;
196 | }
197 |
198 | .Chart__block .Chart__blockInner {
199 | border: 1px solid var(--n30);
200 | border-radius: 4px;
201 | overflow: hidden;
202 | position: relative;
203 | }
204 |
205 | .Chart__block .Chart__header {
206 | font-weight: bold;
207 | border-bottom: 1px solid var(--n30);
208 | padding: 2px 5px;
209 | font-size: 13px;
210 | color: var(--n900);
211 | }
212 |
213 | .Chart__block .Chart__body:after {
214 | content: "";
215 | display: table;
216 | clear: both;
217 | }
218 |
219 | .Chart__block .Chart__header.Chart__absolute {
220 | border-bottom: 0;
221 | position: absolute;
222 | z-index: 3;
223 | top: 0;
224 | left: 0;
225 | }
226 |
227 | .Chart__block .Chart__credits {
228 | color: var(--n100);
229 | font-weight: normal;
230 | font-size: 12px;
231 | }
232 | .Chart__chart .highcharts-root {
233 | font-family: "Roboto", sans-serif !important;
234 | font-size: 12px !important;
235 | line-height: 20px;
236 | }
237 | .Chart__chart .highcharts-background {
238 | fill: var(--n0);
239 | }
240 | .Chart__chart .highcharts-scrollbar {
241 | -webkit-transform: translate(0, 351px) !important;
242 | transform: translate(0, 351px) !important;
243 | }
244 | .Chart__chart .highcharts-scrollbar-button,
245 | .Chart__chart .highcharts-scrollbar-thumb {
246 | fill: var(--n30);
247 | cursor: pointer;
248 | transition: fill 0.2s ease;
249 | }
250 | .Chart__chart .highcharts-scrollbar-button:hover, .Chart__chart .highcharts-scrollbar-thumb:hover {
251 | fill: var(--n100);
252 | }
253 | .Chart__chart .highcharts-scrollbar-track {
254 | stroke: var(--n30);
255 | fill: none;
256 | }
257 | .Chart__chart .highcharts-scrollbar-rifles {
258 | cursor: pointer;
259 | }
260 | .Chart__chart .highcharts-button .highcharts-button-box {
261 | fill: none;
262 | }
263 | .Chart__chart .highcharts-button text {
264 | fill: var(--n100) !important;
265 | transition: fill 0.2s ease;
266 | font-size: 12px;
267 | }
268 | .Chart__chart .highcharts-button.highcharts-button-hover text {
269 | fill: var(--b500) !important;
270 | }
271 | .Chart__chart .highcharts-button.highcharts-button-pressed text {
272 | fill: var(--b500) !important;
273 | font-weight: bold;
274 | }
275 | .Chart__chart .highcharts-button:nth-of-type(1) {
276 | -webkit-transform: translateX(67px - 67px);
277 | transform: translateX(0px);
278 | }
279 | .Chart__chart .highcharts-button:nth-of-type(2) {
280 | -webkit-transform: translateX(67px);
281 | transform: translateX(67px);
282 | }
283 | .Chart__chart .highcharts-button:nth-of-type(3) {
284 | -webkit-transform: translateX(67px + 70px);
285 | transform: translateX(137px);
286 | }
287 | .Chart__chart .highcharts-tooltip .highcharts-tooltip-box > span {
288 | background: var(--n20);
289 | padding: 5px;
290 | font-family: inherit !important;
291 | font-size: 12px !important;
292 | line-height: 20px;
293 | color: var(--n900) !important;
294 | }
295 | .Chart__chart .highcharts-point {
296 | stroke-width: 0.5;
297 | stroke: var(--n600);
298 | }
299 | .Chart__chart .highcharts-point.highcharts-point-up {
300 | fill: var(--g500);
301 | }
302 | .Chart__chart .highcharts-point.highcharts-point-up.highcharts-point-hover {
303 | fill: var(--g400);
304 | }
305 | .Chart__chart .highcharts-point.highcharts-point-down {
306 | fill: var(--r500);
307 | }
308 | .Chart__chart .highcharts-point.highcharts-point-down.highcharts-point-hover {
309 | fill: var(--r400);
310 | }
311 | .Chart__chart .highcharts-point.highcharts-color-1 {
312 | stroke-width: 0;
313 | fill: var(--n30);
314 | }
315 | .Chart__chart .highcharts-point.highcharts-color-1.highcharts-point-hover {
316 | fill: var(--n100);
317 | }
318 | .Chart__chart .highcharts-series-1 path:first-child {
319 | fill: url(#chartVolumeGradient) !important;
320 | }
321 | .Chart__chart .highcharts-series-1 path:nth-child(2) {
322 | stroke: var(--b500) !important;
323 | stroke-width: 1;
324 | }
325 | .Chart__chart .highcharts-grid-line {
326 | stroke: var(--n20);
327 | }
328 | .Chart__chart .highcharts-axis-labels text {
329 | fill: var(--n900) !important;
330 | }
331 | .Chart__svgGradient {
332 | position: absolute;
333 | left: -99999px;
334 | }
335 | .Chart__svgGradient stop:first-child {
336 | stop-color: var(--b500a3);
337 | }
338 | .Chart__svgGradient stop:last-child {
339 | stop-color: var(--n0);
340 | }
341 |
342 | .TPInfo__infoWrapper {
343 | float: left;
344 | width: 100%;
345 | }
346 |
347 | .TPInfo__infoWrapper .TPInfo__info {
348 |
349 | padding: 30px 0 12px 0;
350 | }
351 |
352 | .TPInfo__infoWrapper .TPInfo__info:after {
353 | content: "";
354 | display: table;
355 | clear: both;
356 | }
357 |
358 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__currencyName {
359 | float: left;
360 | width: 25%;
361 | font-weight: bold;
362 | color: var(--n900);
363 | font-size: 16px;
364 | text-transform: uppercase;
365 | padding: 12px 14px 0 14px;
366 | }
367 |
368 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns {
369 | float: left;
370 | width: 50%;
371 | display: flex;
372 | flex-flow: row nowrap;
373 | justify-content: space-between;
374 | }
375 |
376 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column {
377 | padding: 0 14px;
378 | }
379 |
380 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column .TPInfo__columnLabel {
381 | color: var(--n500);
382 | font-size: 12px;
383 | }
384 |
385 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column .TPInfo__columnData {
386 | color: var(--n900);
387 | font-weight: bold;
388 | font-size: 12px;
389 | }
390 |
391 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column .TPInfo__columnData span {
392 | display: inline-block;
393 | vertical-align: top;
394 | }
395 |
396 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column .TPInfo__columnData .TPInfo__changePercentage {
397 | display: inline-block;
398 | vertical-align: top;
399 |
400 | font-size: 11px;
401 | font-weight: normal;
402 | line-height: 11px;
403 | margin-left: 5px;
404 | }
405 |
406 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column.TPInfo__up .TPInfo__columnData {
407 | color: var(--g500);
408 | }
409 |
410 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__dataColumns .TPInfo__column.TPInfo__down .TPInfo__columnData {
411 | color: var(--r500);
412 | }
413 |
414 | .TPInfo__infoWrapper .TPInfo__info .TPInfo__options {
415 | float: left;
416 | width: 25%;
417 | font-weight: bold;
418 | color: var(--n900);
419 | font-size: 14px;
420 | padding: 12px 14px 0 14px;
421 | }
422 |
423 | .OrderBook__block {
424 | float: left;
425 | width: 25%;
426 | padding: 8px;
427 | }
428 |
429 | .OrderBook__block .OrderBook__blockInner {
430 | border: 1px solid var(--n30);
431 | border-radius: 4px;
432 | overflow: hidden;
433 | position: relative;
434 | }
435 |
436 | .OrderBook__block .OrderBook__header {
437 | font-weight: bold;
438 | border-bottom: 1px solid var(--n30);
439 | padding: 2px 5px;
440 | font-size: 13px;
441 | color: var(--n900);
442 | }
443 |
444 | .OrderBook__block .OrderBook__body:after {
445 | content: "";
446 | display: table;
447 | clear: both;
448 | }
449 |
450 | .OrderBook__block .OrderBook__tableHeader {
451 |
452 | border-bottom: 1px solid var(--n30);
453 | }
454 |
455 | .OrderBook__block .OrderBook__tableHeader:after {
456 | content: "";
457 | display: table;
458 | clear: both;
459 | }
460 |
461 | .OrderBook__block .OrderBook__tableHeader .OrderBook__cell {
462 | float: left;
463 |
464 | background: var(--n10);
465 | color: var(--n600);
466 | font-size: 11px;
467 | padding: 2px 5px;
468 | }
469 |
470 | .OrderBook__block .OrderBook__tableHeader .OrderBook__cell:nth-child(1) {
471 | width: 40%;
472 | }
473 |
474 | .OrderBook__block .OrderBook__tableHeader .OrderBook__cell:nth-child(2) {
475 | width: 30%;
476 | }
477 |
478 | .OrderBook__block .OrderBook__tableHeader .OrderBook__cell:nth-child(3) {
479 | width: 30%;
480 | }
481 |
482 | .OrderBook__block .OrderBook__tableBody .OrderBook__row {
483 |
484 | display: block;
485 | border-bottom: 1px solid var(--n20);
486 | position: relative;
487 | height: 24px;
488 | color: var(--n600);
489 | }
490 |
491 | .OrderBook__block .OrderBook__tableBody .OrderBook__row:after {
492 | content: "";
493 | display: table;
494 | clear: both;
495 | }
496 |
497 | .OrderBook__block .OrderBook__tableBody .OrderBook__row:last-child {
498 | border-bottom: 0;
499 | }
500 |
501 | .OrderBook__block .OrderBook__tableBody .OrderBook__cell {
502 | float: left;
503 |
504 | font-size: 11px;
505 | padding: 2px 5px;
506 | }
507 |
508 | .OrderBook__block .OrderBook__tableBody .OrderBook__cell:nth-child(1) {
509 | width: 40%;
510 | }
511 |
512 | .OrderBook__block .OrderBook__tableBody .OrderBook__cell:nth-child(2) {
513 | width: 30%;
514 | }
515 |
516 | .OrderBook__block .OrderBook__tableBody .OrderBook__cell:nth-child(3) {
517 | width: 30%;
518 | }
519 |
520 | .OrderBook__block .OrderBook__tableBody .OrderBook__cell.OrderBook__down {
521 | color: var(--r500);
522 | }
523 |
524 | .OrderBook__block .OrderBook__tableBody .OrderBook__cell.OrderBook__up {
525 | color: var(--g500);
526 | }
527 |
528 | .OrderBook__block .OrderBook__bar {
529 | position: absolute;
530 | z-index: -1;
531 | height: 100%;
532 | background: red;
533 | top: 0;
534 | right: 0;
535 | }
536 |
537 | .OrderBook__block .OrderBook__tableBody .OrderBook__row.OrderBook__sell .OrderBook__cell:first-child {
538 | color: var(--r500);
539 | }
540 |
541 | .OrderBook__block .OrderBook__tableBody .OrderBook__row.OrderBook__sell .OrderBook__bar {
542 | background: linear-gradient(var(--r500a2), var(--r500a1));
543 | }
544 |
545 | .OrderBook__block .OrderBook__tableBody .OrderBook__row.OrderBook__buy .OrderBook__cell:first-child {
546 | color: var(--g500);
547 | }
548 |
549 | .OrderBook__block .OrderBook__tableBody .OrderBook__row.OrderBook__buy .OrderBook__bar {
550 | background: linear-gradient(var(--g500a2), var(--g500a1));
551 | }
552 |
553 | .OrderBook__block .OrderBook__currentPriceRow {
554 | border-top: 1px solid var(--n30);
555 | border-bottom: 1px solid var(--n30);
556 | background: var(--n10);
557 | font-size: 14px;
558 | font-weight: bold;
559 | padding: 2px 5px;
560 | }
561 |
562 | .OrderBook__block .OrderBook__currentPriceRow.OrderBook__down {
563 | color: var(--r500);
564 | }
565 |
566 | .OrderBook__block .OrderBook__currentPriceRow.OrderBook__up {
567 | color: var(--g500);
568 | }
569 |
570 | .MarketList__block {
571 | float: left;
572 | width: 25%;
573 | padding: 8px;
574 |
575 | float: left;
576 | padding: 8px;
577 | }
578 |
579 | .MarketList__block .MarketList__blockInner {
580 | border: 1px solid var(--n30);
581 | border-radius: 4px;
582 | overflow: hidden;
583 | position: relative;
584 | }
585 |
586 | .MarketList__block .MarketList__header {
587 | font-weight: bold;
588 | border-bottom: 1px solid var(--n30);
589 | padding: 2px 5px;
590 | font-size: 13px;
591 | color: var(--n900);
592 | }
593 |
594 | .MarketList__block .MarketList__body:after {
595 | content: "";
596 | display: table;
597 | clear: both;
598 | }
599 |
600 | .MarketList__block .MarketList__tableHeader {
601 |
602 | border-bottom: 1px solid var(--n30);
603 | }
604 |
605 | .MarketList__block .MarketList__tableHeader:after {
606 | content: "";
607 | display: table;
608 | clear: both;
609 | }
610 |
611 | .MarketList__block .MarketList__tableHeader .MarketList__cell {
612 | float: left;
613 |
614 | background: var(--n10);
615 | color: var(--n600);
616 | font-size: 11px;
617 | padding: 2px 5px;
618 | }
619 |
620 | .MarketList__block .MarketList__tableHeader .MarketList__cell:nth-child(1) {
621 | width: 40%;
622 | }
623 |
624 | .MarketList__block .MarketList__tableHeader .MarketList__cell:nth-child(2) {
625 | width: 30%;
626 | }
627 |
628 | .MarketList__block .MarketList__tableHeader .MarketList__cell:nth-child(3) {
629 | width: 30%;
630 | }
631 |
632 | .MarketList__block .MarketList__tableBody .MarketList__row {
633 |
634 | display: block;
635 | border-bottom: 1px solid var(--n20);
636 | position: relative;
637 | height: 24px;
638 | color: var(--n600);
639 | }
640 |
641 | .MarketList__block .MarketList__tableBody .MarketList__row:after {
642 | content: "";
643 | display: table;
644 | clear: both;
645 | }
646 |
647 | .MarketList__block .MarketList__tableBody .MarketList__row:last-child {
648 | border-bottom: 0;
649 | }
650 |
651 | .MarketList__block .MarketList__tableBody .MarketList__cell {
652 | float: left;
653 |
654 | font-size: 11px;
655 | padding: 2px 5px;
656 | }
657 |
658 | .MarketList__block .MarketList__tableBody .MarketList__cell:nth-child(1) {
659 | width: 40%;
660 | }
661 |
662 | .MarketList__block .MarketList__tableBody .MarketList__cell:nth-child(2) {
663 | width: 30%;
664 | }
665 |
666 | .MarketList__block .MarketList__tableBody .MarketList__cell:nth-child(3) {
667 | width: 30%;
668 | }
669 |
670 | .MarketList__block .MarketList__tableBody .MarketList__cell.MarketList__down {
671 | color: var(--r500);
672 | }
673 |
674 | .MarketList__block .MarketList__tableBody .MarketList__cell.MarketList__up {
675 | color: var(--g500);
676 | }
677 |
678 | .MarketList__block .MarketList__row {
679 | -webkit-user-select: none;
680 | -moz-user-select: none;
681 | -ms-user-select: none;
682 | user-select: none;
683 | }
684 |
685 | .MarketList__block .MarketList__row:hover {
686 | background: var(--n20);
687 | }
688 |
689 | .MarketList__block .MarketList__row.MarketList__active {
690 | background: var(--n20) !important;
691 | cursor: default;
692 | font-weight: bold;
693 | }
694 |
695 | .MarketList__block .MarketList__row .MarketList__cell:first-child {
696 | text-transform: uppercase;
697 | }
698 |
699 | .MarketList__block .MarketList__themeBlock {
700 | float: right;
701 | }
702 |
703 | .MarketList__block .MarketList__themeBlock .MarketList__themeToggler {
704 | display: inline-block;
705 | vertical-align: top;
706 |
707 | width: 16px;
708 | height: 16px;
709 | border: 1px solid #000;
710 | cursor: pointer;
711 | margin-right: 10px;
712 | margin-top: 2px;
713 | transition: border-color 0.2s ease;
714 | }
715 |
716 | .MarketList__block .MarketList__themeBlock .MarketList__themeToggler.MarketList__light {
717 | background: #fff;
718 | }
719 |
720 | .MarketList__block .MarketList__themeBlock .MarketList__themeToggler.MarketList__dark {
721 | background: #000;
722 | margin-right: 20px;
723 | }
724 |
725 | .MarketList__block .MarketList__themeBlock .MarketList__themeToggler:hover,
726 | .MarketList__block .MarketList__themeBlock .MarketList__themeToggler.MarketList__active {
727 | border-color: var(--b500);
728 | }
729 |
730 | .MarketList__block .MarketList__themeBlock .MarketList__lnToggler {
731 | display: inline-block;
732 | vertical-align: top;
733 |
734 | width: 32px;
735 | height: 16px;
736 | cursor: pointer;
737 | margin-right: 10px;
738 | margin-top: 2px;
739 | opacity: 0.5;
740 | transition: opacity 0.2s ease;
741 | border: 1px solid #000;
742 | background-size: 100% 100%;
743 | }
744 |
745 | .MarketList__block .MarketList__themeBlock .MarketList__lnToggler.MarketList__ru {
746 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVCAIAAACor3u9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjczMEExNkExMEQwRTExRTY4MzM4RUQxQTY1ODVGRkFFIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjczMEExNkEyMEQwRTExRTY4MzM4RUQxQTY1ODVGRkFFIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzMwQTE2OUYwRDBFMTFFNjgzMzhFRDFBNjU4NUZGQUUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzMwQTE2QTAwRDBFMTFFNjgzMzhFRDFBNjU4NUZGQUUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4XLy0KAAAAV0lEQVR42mL8//8/Ay0BEwONwagFA28By7tPv2hqAaOwx1ra+uDt2++0tYCBhWk0FREIIgZm2lrA//8LbZPpUR0N2vpA5P9n2lrwh8ZxMFqaDrwFAAEGALQmEA+T9rNSAAAAAElFTkSuQmCCOTk5);
747 | }
748 |
749 | .MarketList__block .MarketList__themeBlock .MarketList__lnToggler.MarketList__en {
750 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAYAAAB3AH1ZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjFFODhFQjJFMEQyRDExRTZBMjM0Rjc1Q0QzODFGMTY3IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjFFODhFQjJGMEQyRDExRTZBMjM0Rjc1Q0QzODFGMTY3Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MUU4OEVCMkMwRDJEMTFFNkEyMzRGNzVDRDM4MUYxNjciIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MUU4OEVCMkQwRDJEMTFFNkEyMzRGNzVDRDM4MUYxNjciLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5XLj5qAAAEj0lEQVR42sRVfUxVZRj/nXPPuZ9A4BZbW5SlAi1GWHDBIIHL5QLNcnWRGeIK1uzCAlfSHK2t9Y/ipmuZE9QtsWmkAhMXeLkffOi88mXMlRGIUdOyeSkj5HK/b885B0zwOl3/9Nzd7d15n/f9/Z7f8/EyN8pME6qS12Ki1hlgH/kNO/eY0X32ChAIAWoeYBgsMpcXLV+/A73tOCZ2NQLktrKuEhZdCUreOEBn5Iv9Q+Qw5wMi1DDkJKA6WYnkkV74/5zG8u1VU5wjRReb0WGN9Jw8hezNG6A/VgHz4DXs3muDvW/8/kQeZAvAGgJel4TKZzhoxwfADdxG9CYj5AV5sAxdY1jjoVFfBVLhSMmFs7UDU+XvIn/mKmxHy9HZUgVdbgLgCQCzXunShwEmlcDyBLwabVXP4jD3LTJH7IhdX4DYL/ejZ1kC8jc3ocDY4Ofg86Hr9Ai6IgWJ0mBKYpF+sgP80RbklxWj6Fg5zAOkyOfzihCRcDzEbwJJjWo+Yl6MmB+ZQUypERxFbB68jt1lh+eVDYrp4uAmmQR1Z1ywnBiAhXKVmZOMykQZ0praID/UDP2WUhR+sQkd5ydR/1mPkPZFJBbWWaTW+9mPImNiEOzFaSiNrxKwHu1CSo0H0b8ArJJLKaUUMSf6fvqDzi67c1kwBBdtqKI0yM18HNGjV+Ca+BnReVnEWCXVIf1vfbgDv+85IBbhY7UmxOyog7g7OIxZXxCaTK3oe8MVRK/jKjy+ANQUMbO4lpzMXx/XLyKwYEEiMufxI8TzYGQyBN1uhIi9jJUumD4/BPflMZGAMikRj2SmilGFFARCP9Gf7uBkDJQKHiwbtoidzLDiybAEHmSsUgFGLrVcyOslQA/+gzlZ/M/GrdxuuvcrSSmkwO3xwU9zgCH5GKVS2vJ4hEKhFAzfmwIINUb+MpYUUlLKAmCpy5QKTkpBmPbhbPqNS9qJcj/nhZyTIfvFFYhTSbmbdQxBw9M6LRVzgh8VoWv4kkggKjvj3yIU9lwuTPc4oIl/CrdWrYDVcR0e6jKVil9ahOCKNzZKLREUJhf1MbFPXxuPD2r0iIQf0+12uFtPIxARAcsqLT7deRE17xmg59g701HDs/imbxL79lpQV6ND4ZonoJGz+PWjXfAq1fDEp6Nh1I8LvaTYrEtqw3lFWBAr0puAGeheTkJnx1b0t23Bet8vcJm2YtZsR//zeagIvoDXGy7jXLck+92BCGtB3LP2MRTRoDG83YzemHgsP34QT5e+gsKxc2iJ+h4t21ZDX6wVh5UYMGFzoFbT6RJRW61HUXocAtZuON+qh1+twUBKHhrH/OgiYNx2iQcotLDPgviN9qCQwWr7EdbeceTnxKOWlDTQ+PV32bG2uRVr1BHorxIU8cFGinDm1iq+QBsHn60HN98UgNUYei4XDWMBdDXeBbz0lbufCUwEIiSv1TYqEcklItX5MDTth++MFVlftSE9MgoXTFqOM7DOm5MbPvFxUZG4lKbHvu+8BPyDOJrFV/BhgcMSUUhErESkR1KkblsBco7o8Xf7Gbx0qnPqHwEGAMUZ6nw0OFRDAAAAAElFTkSuQmCCMjA3OQ==);
751 | margin-right: 0;
752 | }
753 |
754 | .MarketList__block .MarketList__themeBlock .MarketList__lnToggler:hover,
755 | .MarketList__block .MarketList__themeBlock .MarketList__lnToggler.MarketList__active {
756 | opacity: 1;
757 | }
758 | .MarketList__alignRight {
759 | text-align: right;
760 | }
761 |
762 | .Tabs__tabs {
763 |
764 | border-bottom: 1px solid var(--b300);
765 | width: 100%;
766 | text-transform: uppercase;
767 | }
768 | .Tabs__tabs:after {
769 | content: "";
770 | display: table;
771 | clear: both;
772 | }
773 | .Tabs__tabs .Tabs__tab {
774 | float: left;
775 | padding: 3px 0;
776 | cursor: pointer;
777 | -webkit-user-select: none;
778 | -moz-user-select: none;
779 | -ms-user-select: none;
780 | user-select: none;
781 | font-size: 12px;
782 | color: var(--n100);
783 | transition: color 0.2s ease;
784 | border-left: 1px solid transparent;
785 | border-right: 1px solid transparent;
786 | position: relative;
787 | margin: -1px 0;
788 | text-align: center;
789 | }
790 | .Tabs__tabs .Tabs__tab:after {
791 | content: "";
792 | position: absolute;
793 | display: block;
794 | width: 100%;
795 | height: 1px;
796 | background: none;
797 | top: 0;
798 | left: 0;
799 | z-index: 1;
800 | }
801 | .Tabs__tabs .Tabs__tab:hover {
802 | color: var(--b500);
803 | }
804 | .Tabs__tabs .Tabs__tab.Tabs__active {
805 | color: var(--b500) !important;
806 | background: var(--n10);
807 | border-color: var(--b300);
808 | cursor: default;
809 | font-weight: bold;
810 | }
811 | .Tabs__tabs .Tabs__tab.Tabs__active:after {
812 | background: var(--b300);
813 | }
814 | .Tabs__tabs .Tabs__tab:first-child {
815 | border-left: 0 !important;
816 | }
817 | .Tabs__tabs .Tabs__tab:last-child {
818 | border-right: 0 !important;
819 | }
820 |
821 | .MarketDetailed__contentWrapper {
822 | padding: 0 20px;
823 | }
824 |
825 | .Error404__contentWrapper {
826 | text-align: center;
827 | }
828 |
829 | .Error404__contentWrapper .Error404__title {
830 | font-size: 40px;
831 | line-height: 46px;
832 | padding-top: 50px;
833 | }
834 |
835 | .Error404__contentWrapper .Error404__text {
836 | padding-top: 20px;
837 | }
838 |
839 |
--------------------------------------------------------------------------------
/dist/lodash.b3ab72b27800aa0a5a97.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([["lodash"],{"+6XX":function(t,n,e){var r=e("y1pI");t.exports=function listCacheHas(t){return r(this.__data__,t)>-1}},"+c4W":function(t,n,e){var r=e("711d"),o=e("4/ic"),u=e("9ggG"),a=e("9Nap");t.exports=function property(t){return u(t)?r(a(t)):o(t)}},"/9aa":function(t,n,e){var r=e("NykK"),o=e("ExA7"),u="[object Symbol]";t.exports=function isSymbol(t){return"symbol"==typeof t||o(t)&&r(t)==u}},"03A+":function(t,n,e){var r=e("JTzB"),o=e("ExA7"),u=Object.prototype,a=u.hasOwnProperty,i=u.propertyIsEnumerable,c=r(function(){return arguments}())?r:function(t){return o(t)&&a.call(t,"callee")&&!i.call(t,"callee")};t.exports=c},"0Cz8":function(t,n,e){var r=e("Xi7e"),o=e("ebwN"),u=e("e4Nc"),a=200;t.exports=function stackSet(t,n){var e=this.__data__;if(e instanceof r){var i=e.__data__;if(!o||i.length0){if(++n>=e)return arguments[0]}else n=0;return t.apply(void 0,arguments)}}},"9Nap":function(t,n,e){var r=e("/9aa"),o=1/0;t.exports=function toKey(t){if("string"==typeof t||r(t))return t;var n=t+"";return"0"==n&&1/t==-o?"-0":n}},"9ggG":function(t,n,e){var r=e("Z0cm"),o=e("/9aa"),u=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,a=/^\w*$/;t.exports=function isKey(t,n){if(r(t))return!1;var e=typeof t;return!("number"!=e&&"symbol"!=e&&"boolean"!=e&&null!=t&&!o(t))||a.test(t)||!u.test(t)||null!=n&&t in Object(n)}},A90E:function(t,n,e){var r=e("6sVZ"),o=e("V6Ve"),u=Object.prototype.hasOwnProperty;t.exports=function baseKeys(t){if(!r(t))return o(t);var n=[];for(var e in Object(t))u.call(t,e)&&"constructor"!=e&&n.push(e);return n}},AP2z:function(t,n,e){var r=e("nmnc"),o=Object.prototype,u=o.hasOwnProperty,a=o.toString,i=r?r.toStringTag:void 0;t.exports=function getRawTag(t){var n=u.call(t,i),e=t[i];try{t[i]=void 0;var r=!0}catch(t){}var o=a.call(t);return r&&(n?t[i]=e:delete t[i]),o}},B8du:function(t,n){t.exports=function stubFalse(){return!1}},BiGR:function(t,n,e){var r=e("nmnc"),o=e("03A+"),u=e("Z0cm"),a=r?r.isConcatSpreadable:void 0;t.exports=function isFlattenable(t){return u(t)||o(t)||!!(a&&t&&t[a])}},CH3K:function(t,n){t.exports=function arrayPush(t,n){for(var e=-1,r=n.length,o=t.length;++en){var f=t;t=n,n=f}if(e||t%1||n%1){var s=c();return i(t+s*(n-t+a("1e-"+((s+"").length-1))),n)}return r(t,n)}},DSRE:function(t,n,e){(function(t){var r=e("Kz5y"),o=e("B8du"),u=n&&!n.nodeType&&n,a=u&&"object"==typeof t&&t&&!t.nodeType&&t,i=a&&a.exports===u?r.Buffer:void 0,c=(i?i.isBuffer:void 0)||o;t.exports=c}).call(this,e("YuTi")(t))},E2jh:function(t,n,e){var r,o=e("2gN3"),u=(r=/[^.]+$/.exec(o&&o.keys&&o.keys.IE_PROTO||""))?"Symbol(src)_1."+r:"";t.exports=function isMasked(t){return!!u&&u in t}},EpBk:function(t,n){t.exports=function isKeyable(t){var n=typeof t;return"string"==n||"number"==n||"symbol"==n||"boolean"==n?"__proto__"!==t:null===t}},ExA7:function(t,n){t.exports=function isObjectLike(t){return null!=t&&"object"==typeof t}},FZoo:function(t,n,e){var r=e("MrPd"),o=e("4uTw"),u=e("wJg7"),a=e("GoyQ"),i=e("9Nap");t.exports=function baseSet(t,n,e,c){if(!a(t))return t;for(var f=-1,s=(n=o(n,t)).length,p=s-1,l=t;null!=l&&++f0&&e(f)?n>1?baseFlatten(f,n-1,e,u,a):r(a,f):u||(a[a.length]=f)}return a}},XKAG:function(t,n,e){var r=e("ut/Y"),o=e("MMmD"),u=e("7GkX");t.exports=function createFind(t){return function(n,e,a){var i=Object(n);if(!o(n)){var c=r(e,3);n=u(n),e=function(t){return c(i[t],t,i)}}var f=t(n,e,a);return f>-1?i[c?n[f]:f]:void 0}}},Xi7e:function(t,n,e){var r=e("KMkd"),o=e("adU4"),u=e("tMB7"),a=e("+6XX"),i=e("Z8oC");function ListCache(t){var n=-1,e=null==t?0:t.length;for(this.clear();++nl))return!1;var h=s.get(t);if(h&&s.get(n))return h==n;var y=-1,b=!0,x=e&i?new r:void 0;for(s.set(t,n),s.set(n,t);++y-1&&t%1==0&&t<=e}},tLB3:function(t,n,e){var r=e("GoyQ"),o=e("/9aa"),u=NaN,a=/^\s+|\s+$/g,i=/^[-+]0x[0-9a-f]+$/i,c=/^0b[01]+$/i,f=/^0o[0-7]+$/i,s=parseInt;t.exports=function toNumber(t){if("number"==typeof t)return t;if(o(t))return u;if(r(t)){var n="function"==typeof t.valueOf?t.valueOf():t;t=r(n)?n+"":n}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(a,"");var e=c.test(t);return e||f.test(t)?s(t.slice(2),e?2:8):i.test(t)?u:+t}},tMB7:function(t,n,e){var r=e("y1pI");t.exports=function listCacheGet(t){var n=this.__data__,e=r(n,t);return e<0?void 0:n[e][1]}},tadb:function(t,n,e){var r=e("Cwc5")(e("Kz5y"),"DataView");t.exports=r},u8Dt:function(t,n,e){var r=e("YESw"),o="__lodash_hash_undefined__",u=Object.prototype.hasOwnProperty;t.exports=function hashGet(t){var n=this.__data__;if(r){var e=n[t];return e===o?void 0:e}return u.call(n,t)?n[t]:void 0}},"ut/Y":function(t,n,e){var r=e("ZCpW"),o=e("GDhZ"),u=e("zZ0H"),a=e("Z0cm"),i=e("+c4W");t.exports=function baseIteratee(t){return"function"==typeof t?t:null==t?u:"object"==typeof t?a(t)?o(t[0],t[1]):r(t):i(t)}},vlbB:function(t,n){var e=Math.floor,r=Math.random;t.exports=function baseRandom(t,n){return t+e(r()*(n-t+1))}},"wF/u":function(t,n,e){var r=e("e5cp"),o=e("ExA7");t.exports=function baseIsEqual(t,n,e,u,a){return t===n||(null==t||null==n||!o(t)&&!o(n)?t!=t&&n!=n:r(t,n,e,u,baseIsEqual,a))}},wJg7:function(t,n){var e=9007199254740991,r=/^(?:0|[1-9]\d*)$/;t.exports=function isIndex(t,n){var o=typeof t;return!!(n=null==n?e:n)&&("number"==o||"symbol"!=o&&r.test(t))&&t>-1&&t%1==0&&t