From 9536d734545d563a831895d00704ee5dcf923c6c Mon Sep 17 00:00:00 2001 From: mahdiyeh-fs Date: Thu, 17 Mar 2022 17:07:15 +0330 Subject: [PATCH 01/64] add initial config of reports monorepo --- packages/reports/.eslintignore | 5 + packages/reports/.eslintrc.ts | 16 + packages/reports/README.md | 97 ++ packages/reports/build/config.js | 59 + packages/reports/build/constants.js | 149 ++ packages/reports/build/helpers.js | 33 + packages/reports/build/loaders-config.js | 115 ++ packages/reports/build/postcss.config.js | 6 + packages/reports/build/webpack.config-test.js | 33 + packages/reports/build/webpack.config.js | 58 + packages/reports/jest.config.js | 19 + packages/reports/package.json | 128 ++ .../src/Components/Errors/error-component.jsx | 46 + .../reports/src/Components/Errors/index.js | 1 + .../__tests__/route-with-sub-routes.spec.js | 32 + .../src/Components/Routes/binary-link.jsx | 32 + .../src/Components/Routes/binary-routes.jsx | 30 + .../reports/src/Components/Routes/helpers.js | 37 + .../reports/src/Components/Routes/index.js | 4 + .../Routes/route-with-sub-routes.jsx | 50 + .../src/Components/account-statistics.jsx | 70 + .../reports/src/Components/amount-cell.jsx | 15 + .../src/Components/currency-wrapper.jsx | 17 + .../Components/empty-portfolio-message.jsx | 24 + .../empty-trade-history-message.jsx | 28 + .../src/Components/filter-component.jsx | 79 + .../src/Components/indicative-cell.jsx | 53 + .../src/Components/market-symbol-icon-row.jsx | 93 + .../src/Components/placeholder-component.jsx | 29 + .../src/Components/profit_loss_cell.jsx | 15 + .../reports/src/Components/reports-meta.jsx | 19 + .../src/Constants/data-table-constants.js | 404 +++++ .../reports/src/Constants/routes-config.js | 57 + .../__tests__/open-positions.spec.js | 31 + packages/reports/src/Containers/index.js | 11 + .../reports/src/Containers/open-positions.jsx | 441 +++++ .../reports/src/Containers/profit-table.jsx | 224 +++ packages/reports/src/Containers/reports.jsx | 131 ++ packages/reports/src/Containers/routes.jsx | 39 + packages/reports/src/Containers/statement.jsx | 282 +++ .../reports/src/Helpers/market-underlying.js | 44 + packages/reports/src/Helpers/profit-loss.js | 1 + .../Modules/Page404/Components/Page404.jsx | 21 + packages/reports/src/Modules/Page404/index.js | 1 + .../Stores/Modules/CFD/Helpers/cfd-config.ts | 108 ++ .../src/Stores/Modules/CFD/cfd-store.js | 514 ++++++ packages/reports/src/Stores/Modules/index.js | 7 + packages/reports/src/Stores/base-store.js | 537 ++++++ packages/reports/src/Stores/connect.js | 31 + packages/reports/src/Stores/index.js | 14 + .../reports/src/_common/__tests__/utility.js | 16 + .../reports/src/_common/base/server_time.js | 25 + packages/reports/src/_common/utility.js | 52 + packages/reports/src/app.jsx | 26 + packages/reports/src/index.jsx | 14 + packages/reports/src/init-store.js | 20 + packages/reports/src/sass/app.scss | 38 + .../app/_common/components/allow-equals.scss | 19 + .../sass/app/_common/components/amount.scss | 8 + .../app/_common/components/card-list.scss | 20 + .../components/composite-calendar.scss | 183 ++ .../_common/components/contract-audit.scss | 101 ++ .../_common/components/contract-replay.scss | 24 + .../components/contract-type-dialog.scss | 101 ++ .../components/contract-type-info.scss | 110 ++ .../components/contract-type-list.scss | 94 + .../components/contract-type-no-result.scss | 24 + .../components/contract-type-widget.scss | 107 ++ .../components/market-is-closed-overlay.scss | 54 + .../components/market-symbol-icon.scss | 38 + .../app/_common/components/message-box.scss | 103 ++ .../_common/components/number-selector.scss | 56 + .../app/_common/components/popconfirm.scss | 201 +++ .../_common/components/positions-toggle.scss | 115 ++ .../_common/components/purchase-button.scss | 327 ++++ .../app/_common/components/range-slider.scss | 183 ++ .../app/_common/components/toggle-button.scss | 28 + .../app/_common/drawer/contract-drawer.scss | 131 ++ .../app/_common/drawer/positions-drawer.scss | 124 ++ .../_common/drawer/positions-modal-card.scss | 259 +++ .../sass/app/_common/form/time-picker.scss | 93 + .../app/_common/layout/trader-layouts.scss | 797 +++++++++ .../src/sass/app/_common/mobile-widget.scss | 159 ++ .../src/sass/app/modules/contract.scss | 165 ++ .../app/modules/contract/bottom-widgets.scss | 10 + .../src/sass/app/modules/contract/digits.scss | 464 +++++ .../sass/app/modules/mt5/cfd-dashboard.scss | 1539 +++++++++++++++++ .../reports/src/sass/app/modules/mt5/cfd.scss | 394 +++++ .../src/sass/app/modules/portfolio.scss | 87 + .../reports/src/sass/app/modules/reports.scss | 983 +++++++++++ .../src/sass/app/modules/smart-chart.scss | 247 +++ .../src/sass/app/modules/trading-mobile.scss | 409 +++++ .../reports/src/sass/app/modules/trading.scss | 675 ++++++++ 93 files changed, 12913 insertions(+) create mode 100644 packages/reports/.eslintignore create mode 100644 packages/reports/.eslintrc.ts create mode 100644 packages/reports/README.md create mode 100644 packages/reports/build/config.js create mode 100644 packages/reports/build/constants.js create mode 100644 packages/reports/build/helpers.js create mode 100644 packages/reports/build/loaders-config.js create mode 100644 packages/reports/build/postcss.config.js create mode 100644 packages/reports/build/webpack.config-test.js create mode 100644 packages/reports/build/webpack.config.js create mode 100644 packages/reports/jest.config.js create mode 100644 packages/reports/package.json create mode 100644 packages/reports/src/Components/Errors/error-component.jsx create mode 100644 packages/reports/src/Components/Errors/index.js create mode 100644 packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js create mode 100644 packages/reports/src/Components/Routes/binary-link.jsx create mode 100644 packages/reports/src/Components/Routes/binary-routes.jsx create mode 100644 packages/reports/src/Components/Routes/helpers.js create mode 100644 packages/reports/src/Components/Routes/index.js create mode 100644 packages/reports/src/Components/Routes/route-with-sub-routes.jsx create mode 100644 packages/reports/src/Components/account-statistics.jsx create mode 100644 packages/reports/src/Components/amount-cell.jsx create mode 100644 packages/reports/src/Components/currency-wrapper.jsx create mode 100644 packages/reports/src/Components/empty-portfolio-message.jsx create mode 100644 packages/reports/src/Components/empty-trade-history-message.jsx create mode 100644 packages/reports/src/Components/filter-component.jsx create mode 100644 packages/reports/src/Components/indicative-cell.jsx create mode 100644 packages/reports/src/Components/market-symbol-icon-row.jsx create mode 100644 packages/reports/src/Components/placeholder-component.jsx create mode 100644 packages/reports/src/Components/profit_loss_cell.jsx create mode 100644 packages/reports/src/Components/reports-meta.jsx create mode 100644 packages/reports/src/Constants/data-table-constants.js create mode 100644 packages/reports/src/Constants/routes-config.js create mode 100644 packages/reports/src/Containers/__tests__/open-positions.spec.js create mode 100644 packages/reports/src/Containers/index.js create mode 100644 packages/reports/src/Containers/open-positions.jsx create mode 100644 packages/reports/src/Containers/profit-table.jsx create mode 100644 packages/reports/src/Containers/reports.jsx create mode 100644 packages/reports/src/Containers/routes.jsx create mode 100644 packages/reports/src/Containers/statement.jsx create mode 100644 packages/reports/src/Helpers/market-underlying.js create mode 100644 packages/reports/src/Helpers/profit-loss.js create mode 100644 packages/reports/src/Modules/Page404/Components/Page404.jsx create mode 100644 packages/reports/src/Modules/Page404/index.js create mode 100644 packages/reports/src/Stores/Modules/CFD/Helpers/cfd-config.ts create mode 100644 packages/reports/src/Stores/Modules/CFD/cfd-store.js create mode 100644 packages/reports/src/Stores/Modules/index.js create mode 100644 packages/reports/src/Stores/base-store.js create mode 100644 packages/reports/src/Stores/connect.js create mode 100644 packages/reports/src/Stores/index.js create mode 100644 packages/reports/src/_common/__tests__/utility.js create mode 100644 packages/reports/src/_common/base/server_time.js create mode 100644 packages/reports/src/_common/utility.js create mode 100644 packages/reports/src/app.jsx create mode 100644 packages/reports/src/index.jsx create mode 100644 packages/reports/src/init-store.js create mode 100644 packages/reports/src/sass/app.scss create mode 100644 packages/reports/src/sass/app/_common/components/allow-equals.scss create mode 100644 packages/reports/src/sass/app/_common/components/amount.scss create mode 100644 packages/reports/src/sass/app/_common/components/card-list.scss create mode 100644 packages/reports/src/sass/app/_common/components/composite-calendar.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-audit.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-replay.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-type-dialog.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-type-info.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-type-list.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-type-no-result.scss create mode 100644 packages/reports/src/sass/app/_common/components/contract-type-widget.scss create mode 100644 packages/reports/src/sass/app/_common/components/market-is-closed-overlay.scss create mode 100644 packages/reports/src/sass/app/_common/components/market-symbol-icon.scss create mode 100644 packages/reports/src/sass/app/_common/components/message-box.scss create mode 100644 packages/reports/src/sass/app/_common/components/number-selector.scss create mode 100644 packages/reports/src/sass/app/_common/components/popconfirm.scss create mode 100644 packages/reports/src/sass/app/_common/components/positions-toggle.scss create mode 100644 packages/reports/src/sass/app/_common/components/purchase-button.scss create mode 100644 packages/reports/src/sass/app/_common/components/range-slider.scss create mode 100644 packages/reports/src/sass/app/_common/components/toggle-button.scss create mode 100644 packages/reports/src/sass/app/_common/drawer/contract-drawer.scss create mode 100644 packages/reports/src/sass/app/_common/drawer/positions-drawer.scss create mode 100644 packages/reports/src/sass/app/_common/drawer/positions-modal-card.scss create mode 100644 packages/reports/src/sass/app/_common/form/time-picker.scss create mode 100644 packages/reports/src/sass/app/_common/layout/trader-layouts.scss create mode 100644 packages/reports/src/sass/app/_common/mobile-widget.scss create mode 100644 packages/reports/src/sass/app/modules/contract.scss create mode 100644 packages/reports/src/sass/app/modules/contract/bottom-widgets.scss create mode 100644 packages/reports/src/sass/app/modules/contract/digits.scss create mode 100644 packages/reports/src/sass/app/modules/mt5/cfd-dashboard.scss create mode 100644 packages/reports/src/sass/app/modules/mt5/cfd.scss create mode 100644 packages/reports/src/sass/app/modules/portfolio.scss create mode 100644 packages/reports/src/sass/app/modules/reports.scss create mode 100644 packages/reports/src/sass/app/modules/smart-chart.scss create mode 100644 packages/reports/src/sass/app/modules/trading-mobile.scss create mode 100644 packages/reports/src/sass/app/modules/trading.scss diff --git a/packages/reports/.eslintignore b/packages/reports/.eslintignore new file mode 100644 index 000000000000..669c57635d27 --- /dev/null +++ b/packages/reports/.eslintignore @@ -0,0 +1,5 @@ +build/*.js +src/**/__tests__/*.js +src/_common/lib/**/*.js +.eslintrc.js +.stylelintrc.js \ No newline at end of file diff --git a/packages/reports/.eslintrc.ts b/packages/reports/.eslintrc.ts new file mode 100644 index 000000000000..8f5eba9b0529 --- /dev/null +++ b/packages/reports/.eslintrc.ts @@ -0,0 +1,16 @@ +const webpackConfig = require('./build/webpack.config-test.js'); + +module.exports = { + extends: [ + '../../.eslintrc.js', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + settings: { + 'import/resolver': { + webpack: { config: webpackConfig({}) }, + }, + }, +}; \ No newline at end of file diff --git a/packages/reports/README.md b/packages/reports/README.md new file mode 100644 index 000000000000..321f3a03fac1 --- /dev/null +++ b/packages/reports/README.md @@ -0,0 +1,97 @@ +# Deriv App + +This repository contains the static HTML, Javascript, CSS, and images content of the [Deriv](http://app.deriv.com) website. + +**In this document** + +- [Other documents](#other-documents) +- [Installation](#installation) +- [How to work with this project](#how-to-work-with-this-project) + - [Deploy to your gh-pages for the first time](#deploy-to-your-gh-pages-for-the-first-time) + - [Deploy to the root of the gh-pages](#deploy-to-the-root-of-the-gh-pages) + - [Clean root and deploy to it](#clean-root-and-deploy-to-it) + - [Deploy to test folder](#deploy-to-test-folder) + - [Preview on your local machine](#preview-on-your-local-machine) +- [Miscellaneous](#Miscellaneous) +- [Selenium tests](#selenium-tests) + +## Other documents + +- [Modules docs](docs/Modules/README.md) - Contains implementation guides (i.e., scaffolding, code usage) + +## Installation + +In order to work on your own version of the Deriv Javascript and CSS, please **fork this project**. + +You will also need to install the following on your development machine: + +- Node.js (10.14.2 or higher is recommended) and NPM (see ) +- Go to project root, then run `npm install` + +### Use a custom domain + +In order to use your custom domain, please put it in a file named `CNAME` inside `scripts` folder of your local clone of deriv-app. + +## How to work with this project + +### Deploy to your gh-pages for the first time + +1. Register your application [here](https://developers.binary.com/applications/). This will give you the ability to redirect back to your github pages after login. + Use `https://YOUR_GITHUB_USERNAME.github.io/deriv-app/` for the Redirect URL and `https://YOUR_GITHUB_USERNAME.github.io/deriv-app/en/redirect` for the Verification URL. + + If you're using a custom domain, replace the github URLs above with your domain and remove the `deriv-app` base path. + +2. In `src/config.js`: Insert the `Application ID` of your registered application in `user_app_id`. + + - **NOTE:** In order to avoid accidentally committing personal changes to this file, use `git update-index --assume-unchanged src/config.js` + +3. Set `NODE_ENV` to `development` with `export NODE_ENV=development` + +4. Run `npm run deploy:clean` + +### Deploy to the root of the gh-pages + +This will overwrite modified files and only clear the content of `js` folder before pushing changes. It will leave other folders as they are. + +```sh +npm run deploy +``` + +### Clean root and deploy to it + +This removes all files and folders and deploys your `dist` folder to the root. + +```sh +npm run deploy:clean +``` + +### Deploy to test folder + +This will add all your changes to the test folder specified. +Please ensure it is prefixed with `br_`. + +```sh +npm run deploy:folder "br_my_test_folder" +``` + +### Preview on your local machine + +- Edit your `/etc/hosts` file to include this domain: + +``` +127.0.0.1 localhost.binary.sx +``` + +- To preview your changes locally for the first time, run `sudo npm start`: + - It will run all tests, compile all CSS, and JS/JSX as well as watch for further js/jsx/css changes and rebuild on every change you make. +- To preview your changes locally without any tests, run `npm run serve` + - It will watch for JS/JSX/CSS changes and rebuild on every change you make. +- To run all tests, run `npm run test` + +## Miscellaneous + +- In Webstorm, right-click on `src`, hover over `Mark directory as`, and click `Resource root` to enable import alias resolution. + +## Selenium tests + +Elements for selenium test purposes should have `id` attributes, with the value of `dt_[element_name]_[unique_name|id]_[element_type]`. (e.g. `dt_settings_dark_button`) diff --git a/packages/reports/build/config.js b/packages/reports/build/config.js new file mode 100644 index 000000000000..3e8e4a47414f --- /dev/null +++ b/packages/reports/build/config.js @@ -0,0 +1,59 @@ +const path = require('path'); +const stylelintFormatter = require('stylelint-formatter-pretty'); +const { IS_RELEASE } = require('./constants'); +// const { transformContentUrlBase } = require('./helpers'); + +const generateSWConfig = () => ({ + importWorkboxFrom: 'local', + cleanupOutdatedCaches: true, + exclude: [/CNAME$/, /index\.html$/, /404\.html$/], + skipWaiting: true, + clientsClaim: true, +}); + +const htmlOutputConfig = () => ({ + template: 'index.html', + filename: 'index.html', + minify: !IS_RELEASE + ? false + : { + collapseWhitespace: true, + removeComments: true, + removeRedundantAttributes: true, + useShortDoctype: true, + }, +}); + +const htmlInjectConfig = () => ({ + links: [ + { + path: 'public/images/favicons', + glob: '*', + globPath: path.resolve(__dirname, '../src/public/images/favicons'), + attributes: { + rel: 'icon', + }, + }, + ], + append: false, +}); + +const cssConfig = () => ({ + filename: 'reports/css/reports.main.[contenthash].css', + chunkFilename: 'reports/css/reports.[name].[contenthash].css', +}); + +const stylelintConfig = () => ({ + configFile: path.resolve(__dirname, '../.stylelintrc.js'), + formatter: stylelintFormatter, + files: 'sass/**/*.s?(a|c)ss', + failOnError: false, // Even though it's false, it will fail on error, and we need this to be false to display trace +}); + +module.exports = { + htmlOutputConfig, + htmlInjectConfig, + cssConfig, + stylelintConfig, + generateSWConfig, +}; diff --git a/packages/reports/build/constants.js b/packages/reports/build/constants.js new file mode 100644 index 000000000000..898a8794ae56 --- /dev/null +++ b/packages/reports/build/constants.js @@ -0,0 +1,149 @@ +const CircularDependencyPlugin = require('circular-dependency-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); +// const HtmlWebPackPlugin = require('html-webpack-plugin'); +// const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin'); +const IgnorePlugin = require('webpack').IgnorePlugin; +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const path = require('path'); +const StylelintPlugin = require('stylelint-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + +const { + copyConfig, + cssConfig, + // htmlInjectConfig, + // htmlOutputConfig, + stylelintConfig, +} = require('./config'); +const { + css_loaders, + file_loaders, + html_loaders, + js_loaders, + svg_file_loaders, + svg_loaders, +} = require('./loaders-config'); + +const IS_RELEASE = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging'; + +const ALIASES = { + _common: path.resolve(__dirname, '../src/_common'), + Constants: path.resolve(__dirname, '../src/Constants'), + Components: path.resolve(__dirname, '../src/Components'), + Containers: path.resolve(__dirname, '../src/Containers'), + Modules: path.resolve(__dirname, '../src/Modules'), + Sass: path.resolve(__dirname, '../src/sass'), + Stores: path.resolve(__dirname, '../src/Stores'), +}; + +const rules = (is_test_env = false, is_mocha_only = false) => [ + ...(is_test_env && !is_mocha_only + ? [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules|__tests__|(build\/.*\.js$)|(_common\/lib)/, + include: /src/, + loader: 'eslint-loader', + enforce: 'pre', + options: { + formatter: require('eslint-formatter-pretty'), + configFile: path.resolve(__dirname, '../.eslintrc.js'), + ignorePath: path.resolve(__dirname, '../.eslintignore'), + }, + }, + { + test: /\.(ts|tsx)$/, + exclude: /node_modules|__tests__|(build\/.*\.js$)|(_common\/lib)/, + include: /src/, + loader: 'eslint-loader', + enforce: 'pre', + options: { + formatter: require('eslint-formatter-pretty'), + configFile: path.resolve(__dirname, '../.eslintrc.ts'), + ignorePath: path.resolve(__dirname, '../.eslintignore'), + }, + }, + ] + : []), + { + test: /\.(js|jsx|ts|tsx)$/, + exclude: is_test_env ? /node_modules/ : /node_modules|__tests__/, + include: is_test_env ? /__tests__|src/ : /src/, + use: js_loaders, + }, + { + test: /\.html$/, + exclude: /node_modules/, + use: html_loaders, + }, + { + test: /\.(png|jpg|gif|woff|woff2|eot|ttf|otf)$/, + exclude: /node_modules/, + use: file_loaders, + }, + { + test: /\.svg$/, + exclude: /node_modules/, + include: /public\//, + use: svg_file_loaders, + }, + { + test: /\.svg$/, + exclude: /node_modules|public\//, + use: svg_loaders, + }, + is_test_env + ? { + test: /\.(sc|sa|c)ss$/, + loaders: 'null-loader', + } + : { + test: /\.(sc|sa|c)ss$/, + use: css_loaders, + }, +]; + +const MINIMIZERS = !IS_RELEASE + ? [] + : [ + new TerserPlugin({ + test: /\.js$/, + exclude: /(smartcharts)/, + parallel: 2, + }), + new CssMinimizerPlugin(), + ]; + +const plugins = (base, is_test_env, is_mocha_only) => [ + new CleanWebpackPlugin(), + new IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), + new MiniCssExtractPlugin(cssConfig()), + new CircularDependencyPlugin({ exclude: /node_modules/, failOnError: true }), + new ForkTsCheckerWebpackPlugin(), + ...(IS_RELEASE + ? [] + : [ + new WebpackManifestPlugin({ + fileName: 'reports/asset-manifest.json', + filter: file => file.name !== 'CNAME', + }), + ]), + ...(is_test_env && !is_mocha_only + ? [new StylelintPlugin(stylelintConfig())] + : [ + // ...(!IS_RELEASE ? [ new BundleAnalyzerPlugin({ analyzerMode: 'static' }) ] : []), + ]), +]; + +module.exports = { + IS_RELEASE, + ALIASES, + plugins, + rules, + MINIMIZERS, +}; diff --git a/packages/reports/build/helpers.js b/packages/reports/build/helpers.js new file mode 100644 index 000000000000..ff63efbbbfae --- /dev/null +++ b/packages/reports/build/helpers.js @@ -0,0 +1,33 @@ +/** + * @param {Buffer} content + * @param {string} path + * @param {string|RegExp} base + * */ +const transformContentUrlBase = (content, path, base) => { + return content.toString().replace(/{root_url}|{start_url_base}/g, base); +}; + +/** + * @returns {string} Chrome browser string + * */ +const openChromeBasedOnPlatform = platform => { + switch (platform) { + case 'win32': { + return 'chrome'; + } + case 'linux': { + return 'google-chrome'; + } + case 'darwin': { + return 'Google Chrome'; + } + default: { + return 'Google Chrome'; + } + } +}; + +module.exports = { + transformContentUrlBase, + openChromeBasedOnPlatform, +}; diff --git a/packages/reports/build/loaders-config.js b/packages/reports/build/loaders-config.js new file mode 100644 index 000000000000..244d0aa2d1b8 --- /dev/null +++ b/packages/reports/build/loaders-config.js @@ -0,0 +1,115 @@ +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const path = require('path'); + +const js_loaders = [ + { + loader: '@deriv/shared/src/loaders/react-import-loader.js', + }, + { + loader: '@deriv/shared/src/loaders/deriv-account-loader.js', + }, + { + loader: 'babel-loader', + options: { + cacheDirectory: true, + rootMode: 'upward', + }, + }, +]; + +const html_loaders = [ + { + loader: 'html-loader', + }, +]; + +const file_loaders = [ + { + loader: 'file-loader', + options: { + name: '[path][name].[ext]', + }, + }, +]; + +const svg_file_loaders = [ + { + loader: 'file-loader', + options: { + name: '[path][name].[ext]', + }, + }, +]; + +const svg_loaders = [ + { + loader: 'babel-loader', + options: { + cacheDirectory: true, + rootMode: 'upward', + }, + }, + { + loader: 'react-svg-loader', + options: { + jsx: true, + svgo: { + plugins: [ + { removeTitle: false }, + { removeUselessStrokeAndFill: false }, + { removeUknownsAndDefaults: false }, + ], + floatPrecision: 2, + }, + }, + }, +]; + +const css_loaders = [ + { + loader: MiniCssExtractPlugin.loader, + }, + { + loader: 'css-loader', + options: { + sourceMap: true, + }, + }, + { + loader: 'postcss-loader', + options: { + sourceMap: true, + postcssOptions: { + config: path.resolve(__dirname), + }, + }, + }, + { + loader: 'resolve-url-loader', + options: { + sourceMap: true, + keepQuery: true, + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: true, + }, + }, + { + loader: 'sass-resources-loader', + options: { + resources: require('@deriv/shared/src/styles/index.js'), + }, + }, +]; + +module.exports = { + js_loaders, + html_loaders, + file_loaders, + svg_loaders, + svg_file_loaders, + css_loaders, +}; diff --git a/packages/reports/build/postcss.config.js b/packages/reports/build/postcss.config.js new file mode 100644 index 000000000000..ce1fe7ec00d9 --- /dev/null +++ b/packages/reports/build/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + parser: 'postcss-scss', + plugins: { + 'postcss-preset-env': {}, + }, +}; diff --git a/packages/reports/build/webpack.config-test.js b/packages/reports/build/webpack.config-test.js new file mode 100644 index 000000000000..3fb503cb753c --- /dev/null +++ b/packages/reports/build/webpack.config-test.js @@ -0,0 +1,33 @@ +const { ALIASES, IS_RELEASE, MINIMIZERS, plugins, rules } = require('./constants'); +const path = require('path'); +const nodeExternals = require('webpack-node-externals'); + +module.exports = function (env) { + const base = env && env.base && env.base !== true ? `/${env.base}/` : '/'; + + return { + context: path.resolve(__dirname, '../src'), + devtool: IS_RELEASE ? undefined : 'eval-cheap-module-source-map', + entry: './index.js', + externals: [nodeExternals()], + mode: IS_RELEASE ? 'development' : 'production', + module: { + rules: rules(true, env && env.mocha_only), + }, + optimization: { + chunkIds: 'named', + minimize: true, + minimizer: MINIMIZERS, + }, + output: { + filename: 'js/[name].[hash].js', + publicPath: '/', + }, + plugins: plugins(base, true, env && env.mocha_only), + resolve: { + alias: ALIASES, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + target: 'node', + }; +}; diff --git a/packages/reports/build/webpack.config.js b/packages/reports/build/webpack.config.js new file mode 100644 index 000000000000..08ee9a20df71 --- /dev/null +++ b/packages/reports/build/webpack.config.js @@ -0,0 +1,58 @@ +const path = require('path'); +const { ALIASES, IS_RELEASE, MINIMIZERS, plugins, rules } = require('./constants'); + +module.exports = function (env) { + const base = env && env.base && env.base !== true ? `/${env.base}/` : '/'; + + return { + context: path.resolve(__dirname, '../'), + devtool: IS_RELEASE ? undefined : 'eval-cheap-module-source-map', + entry: { + reports: path.resolve(__dirname, '../src', 'index.jsx'), + }, + mode: IS_RELEASE ? 'production' : 'development', + module: { + rules: rules(), + }, + resolve: { + alias: ALIASES, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + optimization: { + chunkIds: 'named', + moduleIds: 'named', + minimize: IS_RELEASE, + minimizer: MINIMIZERS, + }, + output: { + filename: 'reports/js/[name].js', + publicPath: base, + path: path.resolve(__dirname, '../dist'), + chunkFilename: 'reports/js/reports.[name].[contenthash].js', + libraryExport: 'default', + library: '@deriv/reports', + libraryTarget: 'umd', + }, + externals: [ + { + react: 'react', + 'react-dom': 'react-dom', + 'react-router-dom': 'react-router-dom', + 'react-router': 'react-router', + mobx: 'mobx', + 'mobx-react': 'mobx-react', + '@deriv/shared': '@deriv/shared', + '@deriv/components': '@deriv/components', + '@deriv/translations': '@deriv/translations', + '@deriv/deriv-charts': '@deriv/deriv-charts', + '@deriv/account': '@deriv/account', + }, + /^@deriv\/shared\/.+$/, + /^@deriv\/components\/.+$/, + /^@deriv\/translations\/.+$/, + /^@deriv\/account\/.+$/, + ], + target: 'web', + plugins: plugins(base, false), + }; +}; diff --git a/packages/reports/jest.config.js b/packages/reports/jest.config.js new file mode 100644 index 000000000000..4319119f4329 --- /dev/null +++ b/packages/reports/jest.config.js @@ -0,0 +1,19 @@ +const baseConfigForPackages = require('../../jest.config.base'); + +module.exports = { + ...baseConfigForPackages, + moduleNameMapper: { + "\\.s(c|a)ss$": "/../../__mocks__/styleMock.js", + "^.+\\.svg$": "/../../__mocks__/styleMock.js", + '^_common\/(.*)$': "/src/_common/$1", + '^App\/(.*)$': "/src/App/$1", + '^Assets\/(.*)$': "/src/Assets/$1", + '^Constants\/(.*)$': "/src/Constants/$1", + '^Constants$': "/src/Constants/index.js", + '^Documents\/(.*)$': "/src/Documents/$1", + '^Modules\/(.*)$': "/src/Modules/$1", + '^Utils\/(.*)$': "/src/Utils/$1", + '^Services\/(.*)$': "/src/Services/$1", + '^Stores\/(.*)$': "/src/Stores/$1", + }, +}; \ No newline at end of file diff --git a/packages/reports/package.json b/packages/reports/package.json new file mode 100644 index 000000000000..8ebfa110df83 --- /dev/null +++ b/packages/reports/package.json @@ -0,0 +1,128 @@ +{ + "name": "@deriv/reports", + "version": "1.0.0", + "description": "Deriv content", + "main": "dist/reports/js/reports.js", + "private": true, + "scripts": { + "start": "npm run test && npm run serve", + "serve": "echo \"Serving...\" && webpack --progress --watch --config \"./build/webpack.config.js\"", + "build": "f () { webpack --config \"./build/webpack.config.js\" --env base=$1;}; f", + "build:travis": "echo \"No build:travis specified\"", + "test": "echo \"No mocha:test specified\"", + "test:eslint": "eslint \"./src/**/*.?(js|jsx)\"", + "test:mocha": "mochapack -r babel-polyfill -r jsdom-global/register -r mock-local-storage --webpack-config \"./build/webpack.config-test.js\" \"src/**/__tests__/*.js\" --webpack-env.mocha_only --require ignore-styles", + "deploy": "echo \"No deploy specified\"", + "deploy:clean": "echo \"No deploy:clean specified\"", + "deploy:folder": "echo \"No deploy:folder specified\"", + "deploy:staging": "echo \"No deploy:staging specified\"", + "deploy:production": "echo \"No deploy:production specified\"" + }, + "engines": { + "node": "^14.17.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/binary-com/deriv-app.git" + }, + "keywords": [ + "deriv" + ], + "author": "Deriv", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/binary-com/deriv-app/issues" + }, + "homepage": "https://github.com/binary-com/deriv-app", + "devDependencies": { + "babel-eslint": "^10.1.0", + "babel-loader": "^8.1.0", + "chai": "^4.2.0", + "circular-dependency-plugin": "^5.2.2", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^9.0.1", + "css-loader": "^5.0.1", + "css-minimizer-webpack-plugin": "^3.0.1", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.14.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-config-binary": "^1.0.2", + "eslint-config-prettier": "^7.2.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", + "file-loader": "^6.2.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "html-loader": "^1.3.2", + "html-webpack-plugin": "^5.0.0-beta.5", + "html-webpack-tags-plugin": "^2.0.17", + "ignore-styles": "^5.0.1", + "jsdom": "^16.2.1", + "jsdom-global": "^2.1.1", + "mini-css-extract-plugin": "^1.3.4", + "mocha": "^7.1.1", + "mochapack": "^2.1.2", + "mock-local-storage": "^1.1.8", + "node-sass": "^4.12.0", + "postcss-loader": "^6.1.1", + "postcss-preset-env": "^6.7.0", + "postcss-scss": "^4.0.0", + "react-svg-loader": "^3.0.3", + "resolve-url-loader": "^3.1.2", + "sass-loader": "^10.1.1", + "sass-resources-loader": "^2.1.1", + "sinon": "^7.3.2", + "stylelint-formatter-pretty": "^2.1.1", + "svgo": "^1.3.2", + "terser-webpack-plugin": "^5.1.1", + "webpack": "^5.46.0", + "webpack-bundle-analyzer": "^4.3.0", + "webpack-cli": "^4.7.2", + "webpack-manifest-plugin": "^4.0.2", + "webpack-node-externals": "^2.5.2" + }, + "dependencies": { + "@deriv/account": "^1.0.0", + "@deriv/api-types": "1.0.48", + "@deriv/components": "^1.0.0", + "@deriv/deriv-api": "^1.0.8", + "@deriv/shared": "^1.0.0", + "@deriv/translations": "^1.0.0", + "@types/classnames": "^2.2.11", + "@types/react": "^16.14.21", + "@types/react-dom": "^16.9.14", + "@types/react-loadable": "^5.5.6", + "acorn": "^6.1.1", + "babel-polyfill": "^6.26.0", + "canvas-toBlob": "^1.0.0", + "classnames": "^2.2.6", + "crc-32": "^1.2.0", + "event-source-polyfill": "^1.0.5", + "extend": "^3.0.2", + "formik": "^2.1.4", + "i18next": "^20.3.2", + "js-cookie": "^2.2.1", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "mobx": "^5.15.7", + "mobx-react": "6.3.1", + "mobx-utils": "^5.5.5", + "moment": "^2.24.0", + "null-loader": "^4.0.1", + "object.fromentries": "^2.0.0", + "promise-polyfill": "^8.1.3", + "prop-types": "^15.7.2", + "react": "^16.14.0", + "react-content-loader": "^4.3.2", + "react-dom": "^16.14.0", + "react-i18next": "^11.11.0", + "react-loadable": "^5.5.0", + "react-pose": "^4.0.10", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", + "react-transition-group": "^4.3.0", + "typescript": "^4.5.4", + "workbox-webpack-plugin": "^6.0.2" + } + } \ No newline at end of file diff --git a/packages/reports/src/Components/Errors/error-component.jsx b/packages/reports/src/Components/Errors/error-component.jsx new file mode 100644 index 000000000000..868959ab38b8 --- /dev/null +++ b/packages/reports/src/Components/Errors/error-component.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { PageError, Dialog } from '@deriv/components'; +import { routes } from '@deriv/shared'; +import { localize } from '@deriv/translations'; + +const ErrorComponent = ({ + header, + message, + is_dialog, + redirect_label, + redirectOnClick, + should_show_refresh = true, +}) => { + const refresh_message = should_show_refresh ? localize('Please refresh this page to continue.') : ''; + + return is_dialog ? ( + location.reload())} + > + {message || localize('Sorry, an error occured while processing your request.')} + + ) : ( + location.reload())} + /> + ); +}; + +ErrorComponent.propTypes = { + message: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]), + type: PropTypes.string, +}; + +export default ErrorComponent; diff --git a/packages/reports/src/Components/Errors/index.js b/packages/reports/src/Components/Errors/index.js new file mode 100644 index 000000000000..dc12ba08a1e7 --- /dev/null +++ b/packages/reports/src/Components/Errors/index.js @@ -0,0 +1 @@ +export default from './error-component.jsx'; diff --git a/packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js b/packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js new file mode 100644 index 000000000000..9e9946f2265f --- /dev/null +++ b/packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { expect } from 'chai'; +import { configure, shallow } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { RouteWithSubRoutesRender } from '../route-with-sub-routes.jsx'; +import { Redirect } from 'react-router-dom'; +import { PlatformContext } from '@deriv/shared'; + +configure({ adapter: new Adapter() }); + +describe('', () => { + it('should render one component', () => { + const comp = ( + + + + ); + const wrapper = shallow(comp); + expect(wrapper).to.have.length(1); + }); + it('should have props as passed as route', () => { + const route = { path: '/', component: Redirect, title: '', exact: true, to: '/root' }; + const comp = ( + + + + ); + const wrapper = shallow(comp); + expect(wrapper.prop('exact')).to.equal(true); + expect(wrapper.prop('path')).to.equal('/'); + }); +}); diff --git a/packages/reports/src/Components/Routes/binary-link.jsx b/packages/reports/src/Components/Routes/binary-link.jsx new file mode 100644 index 000000000000..c92428bdea66 --- /dev/null +++ b/packages/reports/src/Components/Routes/binary-link.jsx @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { PlatformContext } from '@deriv/shared'; +import getRoutesConfig from '../../Constants/routes-config'; +import { findRouteByPath, normalizePath } from './helpers'; + +const BinaryLink = ({ active_class, to, children, ...props }) => { + const { is_dashboard } = React.useContext(PlatformContext); + const path = normalizePath(to); + const route = findRouteByPath(path, getRoutesConfig({ is_dashboard })); + + if (!route) { + throw new Error(`Route not found: ${to}`); + } + + return to ? ( + + {children} + + ) : ( + {children} + ); +}; + +BinaryLink.propTypes = { + active_class: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]), + to: PropTypes.string, +}; + +export default BinaryLink; diff --git a/packages/reports/src/Components/Routes/binary-routes.jsx b/packages/reports/src/Components/Routes/binary-routes.jsx new file mode 100644 index 000000000000..98f67846902a --- /dev/null +++ b/packages/reports/src/Components/Routes/binary-routes.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; +import { PlatformContext } from '@deriv/shared'; +import { Localize } from '@deriv/translations'; +import getRoutesConfig from '../../Constants/routes-config'; +import RouteWithSubRoutes from './route-with-sub-routes.jsx'; + +const BinaryRoutes = props => { + const { is_dashboard } = React.useContext(PlatformContext); + + return ( + { + return ( +
+ +
+ ); + }} + > + + {getRoutesConfig({ is_dashboard }).map(route => ( + + ))} + +
+ ); +}; + +export default BinaryRoutes; diff --git a/packages/reports/src/Components/Routes/helpers.js b/packages/reports/src/Components/Routes/helpers.js new file mode 100644 index 000000000000..8d8bcc421c1d --- /dev/null +++ b/packages/reports/src/Components/Routes/helpers.js @@ -0,0 +1,37 @@ +import { matchPath } from 'react-router'; +import { routes } from '@deriv/shared'; + +export const normalizePath = path => (/^\//.test(path) ? path : `/${path || ''}`); // Default to '/' + +export const findRouteByPath = (path, routes_config) => { + let result; + + routes_config.some(route_info => { + let match_path; + try { + match_path = matchPath(path, route_info); + } catch (e) { + if (/undefined/.test(e.message)) { + return undefined; + } + } + + if (match_path) { + result = route_info; + return true; + } else if (route_info.routes) { + result = findRouteByPath(path, route_info.routes); + return result; + } + return false; + }); + + return result; +}; + +export const isRouteVisible = (route, is_logged_in) => !(route && route.is_authenticated && !is_logged_in); + +export const getPath = (route_path, params = {}) => + Object.keys(params).reduce((p, name) => p.replace(`:${name}`, params[name]), route_path); + +export const getContractPath = contract_id => getPath(routes.contract, { contract_id }); diff --git a/packages/reports/src/Components/Routes/index.js b/packages/reports/src/Components/Routes/index.js new file mode 100644 index 000000000000..061bdf961719 --- /dev/null +++ b/packages/reports/src/Components/Routes/index.js @@ -0,0 +1,4 @@ +export BinaryLink from './binary-link.jsx'; +export default from './binary-routes.jsx'; +export * from './helpers'; +export RouteWithSubRoutes from './route-with-sub-routes.jsx'; diff --git a/packages/reports/src/Components/Routes/route-with-sub-routes.jsx b/packages/reports/src/Components/Routes/route-with-sub-routes.jsx new file mode 100644 index 000000000000..5b4d7ad1ec08 --- /dev/null +++ b/packages/reports/src/Components/Routes/route-with-sub-routes.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; +import { redirectToLogin, isEmptyObject, routes, removeBranchName, default_title } from '@deriv/shared'; +import { getLanguage } from '@deriv/translations'; + +const RouteWithSubRoutes = route => { + const renderFactory = props => { + let result = null; + + if (route.component === Redirect) { + let to = route.to; + + // This if clause has been added just to remove '/index' from url in localhost env. + if (route.path === routes.index) { + const { location } = props; + to = location.pathname.toLowerCase().replace(route.path, ''); + } + result = ; + } else if (route.is_authenticated && !route.is_logged_in && !route.is_logging_in) { + redirectToLogin(route.is_logged_in, getLanguage()); + } else { + const default_subroute = (route.routes ?? []).reduce( + (acc, cur) => ({ + ...acc, + ...cur.subroutes.find(subroute => subroute.default), + }), + {} + ); + const has_default_subroute = !isEmptyObject(default_subroute); + const pathname = removeBranchName(location.pathname); + + result = ( + + {has_default_subroute && pathname === route.path && } + + + ); + } + + const title = route.getTitle?.() || ''; + document.title = `${title} | ${default_title}`; + return result; + }; + + return ; +}; + +export { RouteWithSubRoutes as RouteWithSubRoutesRender }; // For tests + +export default RouteWithSubRoutes; diff --git a/packages/reports/src/Components/account-statistics.jsx b/packages/reports/src/Components/account-statistics.jsx new file mode 100644 index 000000000000..2e2c3e3c5863 --- /dev/null +++ b/packages/reports/src/Components/account-statistics.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { MobileWrapper, Money, Text } from '@deriv/components'; +import { localize } from '@deriv/translations'; +import { connect } from 'Stores/connect'; + +const AccountStatistics = ({ account_statistics, currency }) => { + return ( +
+
+
+ + {localize('Total deposits')} ({currency}) + + + + +
+
+
+
+ + {localize('Total withdrawals')} ({currency}) + + + + +
+
+
+
+ + {localize('Net deposits')} ({currency}) + + + + +
+
+
+ ); +}; + +AccountStatistics.propTypes = { + account_statistics: PropTypes.object, + currency: PropTypes.string, +}; + +export default connect(({ modules, client }) => ({ + account_statistics: modules.statement.account_statistics, + currency: client.currency, +}))(AccountStatistics); diff --git a/packages/reports/src/Components/amount-cell.jsx b/packages/reports/src/Components/amount-cell.jsx new file mode 100644 index 000000000000..74342892adb0 --- /dev/null +++ b/packages/reports/src/Components/amount-cell.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { getProfitOrLoss } from 'Modules/Reports/Helpers/profit-loss'; + +const AmountCell = ({ value }) => { + const status = getProfitOrLoss(value); + + return {value}; +}; + +AmountCell.propTypes = { + value: PropTypes.string, +}; + +export default AmountCell; diff --git a/packages/reports/src/Components/currency-wrapper.jsx b/packages/reports/src/Components/currency-wrapper.jsx new file mode 100644 index 000000000000..bb4273052781 --- /dev/null +++ b/packages/reports/src/Components/currency-wrapper.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Text } from '@deriv/components'; + +const CurrencyWrapper = ({ currency }) => ( +
+ + {currency} + +
+); + +CurrencyWrapper.propTypes = { + currency: PropTypes.string, +}; + +export default CurrencyWrapper; diff --git a/packages/reports/src/Components/empty-portfolio-message.jsx b/packages/reports/src/Components/empty-portfolio-message.jsx new file mode 100644 index 000000000000..c2066318b49d --- /dev/null +++ b/packages/reports/src/Components/empty-portfolio-message.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Icon, Text } from '@deriv/components'; +import { localize } from '@deriv/translations'; + +const EmptyPortfolioMessage = ({ error }) => ( +
+
+ {error ? ( + + {error} + + ) : ( + + + + {localize('No open positions')} + + + )} +
+
+); + +export default EmptyPortfolioMessage; diff --git a/packages/reports/src/Components/empty-trade-history-message.jsx b/packages/reports/src/Components/empty-trade-history-message.jsx new file mode 100644 index 000000000000..8f1ec53e4fe3 --- /dev/null +++ b/packages/reports/src/Components/empty-trade-history-message.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Icon, Text } from '@deriv/components'; + +const EmptyTradeHistoryMessage = ({ + has_selected_date, + component_icon, + localized_message, + localized_period_message, +}) => ( + +
+ + + {!has_selected_date ? localized_message : localized_period_message} + +
+
+); + +EmptyTradeHistoryMessage.propTypes = { + component_icon: PropTypes.string, + has_selected_date: PropTypes.bool, + localized_message: PropTypes.string, + localized_period_message: PropTypes.string, +}; + +export default EmptyTradeHistoryMessage; diff --git a/packages/reports/src/Components/filter-component.jsx b/packages/reports/src/Components/filter-component.jsx new file mode 100644 index 000000000000..9f9588b6a130 --- /dev/null +++ b/packages/reports/src/Components/filter-component.jsx @@ -0,0 +1,79 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FilterDropdown } from '@deriv/components'; +import { localize } from '@deriv/translations'; +import { connect } from 'Stores/connect'; +import CompositeCalendar from 'App/Components/Form/CompositeCalendar/composite-calendar.jsx'; + +const FilterComponent = ({ + action_type, + date_from, + date_to, + handleFilterChange, + handleDateChange, + filtered_date_range, +}) => { + const filter_list = [ + { + text: localize('All transactions'), + value: 'all', + }, + { + text: localize('Buy'), + value: 'buy', + }, + { + text: localize('Sell'), + value: 'sell', + }, + { + text: localize('Deposit'), + value: 'deposit', + }, + { + text: localize('Withdrawal'), + value: 'withdrawal', + }, + { + text: localize('Transfer'), + value: 'transfer', + }, + ]; + + return ( + + + + + ); +}; + +FilterComponent.propTypes = { + action_type: PropTypes.string, + date_from: PropTypes.number, + date_to: PropTypes.number, + filtered_date_range: PropTypes.object, + handleDateChange: PropTypes.func, + handleFilterChange: PropTypes.func, + suffix_icon: PropTypes.string, +}; + +export default connect(({ modules }) => ({ + action_type: modules.statement.action_type, + data: modules.statement.data, + date_from: modules.statement.date_from, + date_to: modules.statement.date_to, + filtered_date_range: modules.statement.filtered_date_range, + handleDateChange: modules.statement.handleDateChange, + handleFilterChange: modules.statement.handleFilterChange, +}))(FilterComponent); diff --git a/packages/reports/src/Components/indicative-cell.jsx b/packages/reports/src/Components/indicative-cell.jsx new file mode 100644 index 000000000000..af1b3521a4f6 --- /dev/null +++ b/packages/reports/src/Components/indicative-cell.jsx @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Icon, Money, DesktopWrapper, ContractCard } from '@deriv/components'; +import { getCardLabels } from 'Constants/contract'; +import { connect } from 'Stores/connect'; + +const IndicativeCell = ({ amount, currency, contract_info, is_footer, onClickSell, is_sell_requested }) => { + const [movement, setMovement] = React.useState(null); + const [amount_state, setAmountState] = React.useState(0); + + React.useEffect(() => { + setMovement(() => { + return amount >= amount_state ? 'profit' : 'loss'; + }); + setAmountState(amount); + }, [amount, amount_state]); + + return ( +
+
+ + {status !== 'no-resale' && amount !== 0 && ( + + {movement === 'profit' && } + {movement === 'loss' && } + + )} +
+ + {!is_footer && ( + + )} + +
+ ); +}; + +IndicativeCell.propTypes = { + amount: PropTypes.number, + currency: PropTypes.string, + status: PropTypes.string, + is_footer: PropTypes.bool, + onClickSell: PropTypes.func, +}; + +export default connect(({ modules }) => ({ + onClickSell: modules.portfolio.onClickSell, +}))(IndicativeCell); diff --git a/packages/reports/src/Components/market-symbol-icon-row.jsx b/packages/reports/src/Components/market-symbol-icon-row.jsx new file mode 100644 index 000000000000..5ada1db081e5 --- /dev/null +++ b/packages/reports/src/Components/market-symbol-icon-row.jsx @@ -0,0 +1,93 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { extractInfoFromShortcode, isHighLow } from '@deriv/shared'; +import { Icon, Popover, IconTradeTypes } from '@deriv/components'; +import { getMarketName, getTradeTypeName } from '../Helpers/market-underlying'; + +const MarketSymbolIconRow = ({ icon, payload, show_description, should_show_multiplier = true }) => { + const should_show_category_icon = typeof payload.shortcode === 'string'; + const info_from_shortcode = extractInfoFromShortcode(payload.shortcode); + + if (should_show_category_icon && info_from_shortcode) { + return ( +
+
+ + + + {show_description && payload.display_name} +
+ +
+ + + + {show_description && info_from_shortcode.category} +
+ {should_show_multiplier && info_from_shortcode.multiplier && ( +
x{info_from_shortcode.multiplier}
+ )} +
+ ); + } else if (['deposit', 'hold', 'release', 'withdrawal', 'transfer'].includes(payload.action_type)) { + return ( +
+ {payload.action_type === 'deposit' && } + {payload.action_type === 'withdrawal' && } + {payload.action_type === 'transfer' && } + {(payload.action_type === 'hold' || payload.action_type === 'release') && ( + + )} +
+ ); + } else if (['adjustment'].includes(payload.action_type)) { + return ( +
+ +
+ ); + } + + return ( + + + + ); +}; + +MarketSymbolIconRow.propTypes = { + action: PropTypes.string, + payload: PropTypes.object, + show_description: PropTypes.bool, + should_show_multiplier: PropTypes.bool, +}; + +export default MarketSymbolIconRow; diff --git a/packages/reports/src/Components/placeholder-component.jsx b/packages/reports/src/Components/placeholder-component.jsx new file mode 100644 index 000000000000..54d43f05c528 --- /dev/null +++ b/packages/reports/src/Components/placeholder-component.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Loading from '../../../templates/_common/components/loading'; + +const PlaceholderComponent = props => { + const EmptyMessageComponent = props.empty_message_component; + return ( + + {props.is_empty && ( + + )} + {props.is_loading && } + + ); +}; + +PlaceholderComponent.propTypes = { + component_icon: PropTypes.string, + empty_message_component: PropTypes.func, + has_selected_date: PropTypes.bool, + localized_message: PropTypes.string, +}; + +export default PlaceholderComponent; diff --git a/packages/reports/src/Components/profit_loss_cell.jsx b/packages/reports/src/Components/profit_loss_cell.jsx new file mode 100644 index 000000000000..b968cb81d03f --- /dev/null +++ b/packages/reports/src/Components/profit_loss_cell.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { getProfitOrLoss } from 'Modules/Reports/Helpers/profit-loss'; + +const ProfitLossCell = ({ value, children }) => { + const status = getProfitOrLoss(value); + + return {children}; +}; + +ProfitLossCell.propTypes = { + value: PropTypes.string, +}; + +export default ProfitLossCell; diff --git a/packages/reports/src/Components/reports-meta.jsx b/packages/reports/src/Components/reports-meta.jsx new file mode 100644 index 000000000000..aec529493d5e --- /dev/null +++ b/packages/reports/src/Components/reports-meta.jsx @@ -0,0 +1,19 @@ +import classNames from 'classnames'; +import React from 'react'; + +const ReportsMeta = ({ filter_component, className, optional_component, is_statement }) => ( +
+ {optional_component} + {filter_component && ( +
+ {filter_component} +
+ )} +
+); + +export { ReportsMeta }; diff --git a/packages/reports/src/Constants/data-table-constants.js b/packages/reports/src/Constants/data-table-constants.js new file mode 100644 index 000000000000..75d0bb5dc178 --- /dev/null +++ b/packages/reports/src/Constants/data-table-constants.js @@ -0,0 +1,404 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Icon, Label, Money, ContractCard } from '@deriv/components'; +import { isMobile, getCurrencyDisplayCode, getTotalProfit, shouldShowCancellation } from '@deriv/shared'; +import { localize, Localize } from '@deriv/translations'; +import ProgressSliderStream from 'App/Containers/ProgressSliderStream'; +import { getCardLabels } from 'Constants/contract'; +import { getProfitOrLoss } from 'Modules/Reports/Helpers/profit-loss'; +import IndicativeCell from '../Components/indicative-cell.jsx'; +import MarketSymbolIconRow from '../Components/market-symbol-icon-row.jsx'; +import ProfitLossCell from '../Components/profit_loss_cell.jsx'; +import CurrencyWrapper from '../Components/currency-wrapper.jsx'; + +const getModeFromValue = key => { + const map = { + buy: 'success', + deposit: 'success', + hold: 'warn', + release: 'success', + sell: 'danger', + withdrawal: 'info', + default: 'default', + adjustment: 'adjustment', + transfer: 'transfer', + }; + + if (Object.keys(map).find(x => x === key)) { + return map[key]; + } + + return map.default; +}; + +/* eslint-disable react/display-name, react/prop-types */ +export const getStatementTableColumnsTemplate = currency => [ + { + key: 'icon', + title: isMobile() ? '' : localize('Type'), + col_index: 'icon', + renderCellContent: ({ cell_value, passthrough, row_obj }) => { + const icon = passthrough.isTopUp(row_obj) ? 'icCashierTopUp' : null; + return ( + + ); + }, + }, + { + title: localize('Ref. ID'), + col_index: 'refid', + }, + { + title: localize('Currency'), + col_index: 'currency', + renderCellContent: () => , + }, + { + title: localize('Transaction time'), + col_index: 'date', + renderCellContent: ({ cell_value }) => { + return {cell_value} GMT; + }, + }, + { + key: 'mode', + title: localize('Transaction'), + col_index: 'action_type', + renderCellContent: ({ cell_value, passthrough, row_obj }) => ( + + ), + }, + { + title: localize('Credit/Debit'), + col_index: 'amount', + renderCellContent: ({ cell_value }) => ( +
+ +
+ ), + }, + { + title: localize('Balance'), + col_index: 'balance', + renderCellContent: ({ cell_value }) => , + }, +]; +export const getProfitTableColumnsTemplate = (currency, items_count) => [ + { + key: 'icon', + title: isMobile() ? '' : localize('Type'), + col_index: 'action_type', + renderCellContent: ({ cell_value, row_obj, is_footer }) => { + if (is_footer) { + return localize('Profit/loss on the last {{item_count}} contracts', { item_count: items_count }); + } + return ; + }, + }, + { + title: localize('Ref. ID'), + col_index: 'transaction_id', + }, + { + title: localize('Currency'), + col_index: 'currency', + renderCellContent: ({ is_footer }) => + is_footer ? '' : , + }, + { + title: localize('Buy time'), + col_index: 'purchase_time', + renderCellContent: ({ cell_value, is_footer }) => { + if (is_footer) return ''; + return {cell_value} GMT; + }, + }, + { + title: localize('Buy price'), + col_index: 'buy_price', + renderCellContent: ({ cell_value, is_footer }) => { + if (is_footer) return ''; + + return ; + }, + }, + { + title: localize('Sell time'), + col_index: 'sell_time', + renderHeader: ({ title }) => {title}, + renderCellContent: ({ cell_value, is_footer }) => { + if (is_footer) return ''; + return {cell_value} GMT; + }, + }, + { + title: localize('Sell price'), + col_index: 'sell_price', + renderCellContent: ({ cell_value, is_footer }) => { + if (is_footer) return ''; + + return ; + }, + }, + { + title: localize('Profit / Loss'), + col_index: 'profit_loss', + renderCellContent: ({ cell_value }) => ( + + + + ), + }, +]; +export const getOpenPositionsColumnsTemplate = currency => [ + { + title: isMobile() ? '' : localize('Type'), + col_index: 'type', + renderCellContent: ({ cell_value, row_obj, is_footer }) => { + if (is_footer) return localize('Total'); + + return ; + }, + }, + { + title: localize('Ref. ID'), + col_index: 'reference', + }, + { + title: localize('Currency'), + col_index: 'currency', + renderCellContent: ({ row_obj }) => ( + + ), + }, + { + title: localize('Buy price'), + col_index: 'purchase', + renderCellContent: ({ cell_value }) => , + }, + { + title: localize('Payout limit'), + col_index: 'payout', + renderCellContent: ({ cell_value }) => + cell_value ? : -, + }, + { + title: localize('Indicative profit/loss'), + col_index: 'profit', + renderCellContent: ({ row_obj }) => { + if (!row_obj.profit_loss && (!row_obj.contract_info || !row_obj.contract_info.profit)) return; + const profit = row_obj.profit_loss || row_obj.contract_info.profit; + // eslint-disable-next-line consistent-return + return ( +
0, + })} + > + +
+ {profit > 0 ? : } +
+
+ ); + }, + }, + { + title: localize('Indicative price'), + col_index: 'indicative', + renderCellContent: ({ cell_value, row_obj, is_footer }) => ( + + ), + }, + { + title: localize('Remaining time'), + col_index: 'id', + renderCellContent: ({ row_obj }) => , + }, +]; + +export const getMultiplierOpenPositionsColumnsTemplate = ({ + currency, + onClickCancel, + onClickSell, + getPositionById, + server_time, +}) => [ + { + title: isMobile() ? '' : localize('Type'), + col_index: 'type', + renderCellContent: ({ cell_value, row_obj, is_footer }) => { + if (is_footer) return localize('Total'); + + return ( + + ); + }, + }, + { + title: localize('Multiplier'), + col_index: 'multiplier', + renderCellContent: ({ row_obj }) => + row_obj.contract_info && row_obj.contract_info.multiplier ? `x${row_obj.contract_info.multiplier}` : '', + }, + { + title: localize('Currency'), + col_index: 'currency', + renderCellContent: ({ row_obj }) => ( + + ), + }, + { + title: localize('Stake'), + col_index: 'buy_price', + renderCellContent: ({ row_obj }) => { + if (row_obj.contract_info) { + const { ask_price: cancellation_price = 0 } = row_obj.contract_info.cancellation || {}; + return ; + } + return ''; + }, + }, + { + title: localize('Deal cancel. fee'), + col_index: 'cancellation', + renderCellContent: ({ row_obj }) => { + if (!row_obj.contract_info || !row_obj.contract_info.underlying) return '-'; + + if (!shouldShowCancellation(row_obj.contract_info.underlying)) return localize('N/A'); + + if (row_obj.contract_info.cancellation) { + return ; + } + return '-'; + }, + }, + { + title: isMobile() ? ( + + ) : ( + + ), + col_index: 'purchase', + renderCellContent: ({ cell_value }) => , + }, + { + title: ]} />, + col_index: 'limit_order', + renderCellContent: ({ row_obj, is_footer }) => { + if (is_footer) { + return ''; + } + + const { take_profit, stop_loss } = row_obj.contract_info?.limit_order || {}; + return ( + +
+ {take_profit?.order_amount ? ( + + ) : ( + '-' + )} +
+
+ {stop_loss?.order_amount ? ( + + ) : ( + '-' + )} +
+
+ ); + }, + }, + { + title: localize('Current stake'), + col_index: 'bid_price', + renderCellContent: ({ row_obj, is_footer }) => { + if (is_footer) { + return ''; + } + + if (!row_obj.contract_info || !row_obj.contract_info.bid_price) return '-'; + + const total_profit = getTotalProfit(row_obj.contract_info); + return ( +
0, + })} + > + +
+ ); + }, + }, + { + title: isMobile() ? ( + + ) : ( + ]} /> + ), + col_index: 'profit', + renderCellContent: ({ row_obj }) => { + if (!row_obj.contract_info || !row_obj.contract_info.profit) return null; + const total_profit = getTotalProfit(row_obj.contract_info); + // eslint-disable-next-line consistent-return + return ( +
0, + })} + > + +
+ {total_profit > 0 ? : } +
+
+ ); + }, + }, + { + title: localize('Action'), + col_index: 'action', + renderCellContent: ({ row_obj, is_footer }) => { + if (is_footer) { + return
; + } + + const { contract_info } = row_obj; + const position = getPositionById(contract_info.contract_id); + const { is_sell_requested } = position || {}; + + return ( +
+ +
+ ); + }, + }, +]; +/* eslint-enable react/display-name, react/prop-types */ diff --git a/packages/reports/src/Constants/routes-config.js b/packages/reports/src/Constants/routes-config.js new file mode 100644 index 000000000000..0dc86e5778b6 --- /dev/null +++ b/packages/reports/src/Constants/routes-config.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { routes } from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import { OpenPositions, ProfitTable, Statement, Reports } from '../Containers'; + + +// Error Routes +const Page404 = React.lazy(() => import(/* webpackChunkName: "404" */ '../Modules/Page404')); + +// Order matters +const initRoutesConfig = () => { + return [ + { + path: routes.reports, + component: , + is_authenticated: true, + getTitle: () => localize('Reports'), + icon_component: 'IcReports', + routes: [ + { + path: routes.positions, + component: , + getTitle: () => localize('Open positions'), + icon_component: 'IcOpenPositions', + default: true, + }, + { + path: routes.profit, + component: , + getTitle: () => localize('Profit table'), + icon_component: 'IcProfitTable', + }, + { + path: routes.statement, + component: , + getTitle: () => localize('Statement'), + icon_component: 'IcStatement', + }, + ], + }, + ]; +}; + +let routesConfig; + +// For default page route if page/path is not found, must be kept at the end of routes_config array +const route_default = { component: Page404, getTitle: () => localize('Error 404') }; + +const getRoutesConfig = () => { + if (!routesConfig) { + routesConfig = initRoutesConfig(); + routesConfig.push(route_default); + } + return routesConfig; +}; + +export default getRoutesConfig; diff --git a/packages/reports/src/Containers/__tests__/open-positions.spec.js b/packages/reports/src/Containers/__tests__/open-positions.spec.js new file mode 100644 index 000000000000..cae4c669d486 --- /dev/null +++ b/packages/reports/src/Containers/__tests__/open-positions.spec.js @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import { configure, shallow } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { OpenPositionsTable } from '../open-positions'; + +configure({ adapter: new Adapter() }); + +describe('OpenPositions', () => { + jest.mock( + '../open-positions.jsx', + () => + jest.fn().mockReturnValue({ + OpenPositions: props =>
, + }), + jest.fn().mockReturnValue({ + OpenPositionsTable: props =>
, + }) + ); + + it('should render one component', async () => { + const OpenPositions = (await import('../../index')).default.OpenPositions; + const wrapper = shallow(); + expect(wrapper).to.have.length(1); + }); + + it('should render one component', () => { + const wrapper = shallow(); + expect(wrapper).to.have.length(1); + }); +}); diff --git a/packages/reports/src/Containers/index.js b/packages/reports/src/Containers/index.js new file mode 100644 index 000000000000..0523dc131e09 --- /dev/null +++ b/packages/reports/src/Containers/index.js @@ -0,0 +1,11 @@ +import OpenPositions from './open-positions.jsx'; +import ProfitTable from './profit-table.jsx'; +import Statement from './statement.jsx'; +import Reports from './reports.jsx'; + +export default { + OpenPositions, + ProfitTable, + Statement, + Reports, +}; diff --git a/packages/reports/src/Containers/open-positions.jsx b/packages/reports/src/Containers/open-positions.jsx new file mode 100644 index 000000000000..7368f5318571 --- /dev/null +++ b/packages/reports/src/Containers/open-positions.jsx @@ -0,0 +1,441 @@ +import { PropTypes as MobxPropTypes } from 'mobx-react'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { + DesktopWrapper, + MobileWrapper, + ProgressBar, + Tabs, + DataList, + DataTable, + ContractCard, + usePrevious, +} from '@deriv/components'; +import { urlFor, isMobile, isMultiplierContract, getTimePercentage, website_name, getTotalProfit } from '@deriv/shared'; +import { localize, Localize } from '@deriv/translations'; +import { ReportsTableRowLoader } from 'App/Components/Elements/ContentLoader'; +import { getContractPath } from 'App/Components/Routes/helpers'; +import { getContractDurationType } from 'Modules/Reports/Helpers/market-underlying'; +import EmptyTradeHistoryMessage from 'Modules/Reports/Components/empty-trade-history-message.jsx'; +import { + getOpenPositionsColumnsTemplate, + getMultiplierOpenPositionsColumnsTemplate, +} from 'Modules/Reports/Constants/data-table-constants'; +import PositionsCard from 'App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx'; +import PlaceholderComponent from 'Modules/Reports/Components/placeholder-component.jsx'; +import { getCardLabels } from 'Constants/contract'; +import { connect } from 'Stores/connect'; + +const EmptyPlaceholderWrapper = props => ( + + {props.is_empty ? ( + + ) : ( + props.children + )} + +); + +const MobileRowRenderer = ({ row, is_footer, columns_map, server_time, onClickCancel, onClickSell, measure }) => { + React.useEffect(() => { + if (!is_footer) { + measure(); + } + }, [row.contract_info?.underlying, measure, is_footer]); + + if (is_footer) { + return ( + <> +
+
+ + +
+
+ + +
+
+ + ); + } + + const { contract_info, contract_update, type, is_sell_requested } = row; + const { currency, status, date_expiry, date_start } = contract_info; + const duration_type = getContractDurationType(contract_info.longcode); + const progress_value = getTimePercentage(server_time, date_start, date_expiry) / 100; + + if (isMultiplierContract(type)) { + return ( + + ); + } + + return ( + <> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + ); +}; + +export const OpenPositionsTable = ({ + className, + columns, + component_icon, + currency, + active_positions, + is_loading, + getRowAction, + mobileRowRenderer, + preloaderCheck, + row_size, + totals, +}) => ( + + {is_loading ? ( + + ) : ( + currency && ( +
+ + + row_size} + content_loader={ReportsTableRowLoader} + > + + + + + + + + + + + +
+ ) + )} +
+); + +const getRowAction = row_obj => + row_obj.is_unsupported + ? { + component: ( + , + ]} + /> + ), + } + : getContractPath(row_obj.id); + +/* + * After refactoring transactionHandler for creating positions, + * purchase property in contract positions object is somehow NaN or undefined in the first few responses. + * So we set it to true in these cases to show a preloader for the data-table-row until the correct value is set. + */ +const isPurchaseReceived = item => isNaN(item.purchase) || !item.purchase; + +const getOpenPositionsTotals = (active_positions_filtered, is_multiplier_selected) => { + let totals; + + if (is_multiplier_selected) { + let ask_price = 0; + let profit = 0; + let buy_price = 0; + let bid_price = 0; + let purchase = 0; + + active_positions_filtered.forEach(portfolio_pos => { + buy_price += +portfolio_pos.contract_info.buy_price; + bid_price += +portfolio_pos.contract_info.bid_price; + purchase += +portfolio_pos.purchase; + if (portfolio_pos.contract_info) { + profit += getTotalProfit(portfolio_pos.contract_info); + + if (portfolio_pos.contract_info.cancellation) { + ask_price += portfolio_pos.contract_info.cancellation.ask_price || 0; + } + } + }); + totals = { + contract_info: { + profit, + buy_price, + bid_price, + }, + purchase, + }; + + if (ask_price > 0) { + totals.contract_info.cancellation = { + ask_price, + }; + } + } else { + let indicative = 0; + let purchase = 0; + let profit_loss = 0; + let payout = 0; + + active_positions_filtered?.forEach(portfolio_pos => { + indicative += +portfolio_pos.indicative; + purchase += +portfolio_pos.purchase; + profit_loss += portfolio_pos.profit_loss; + payout += portfolio_pos.payout; + }); + totals = { + indicative, + purchase, + profit_loss, + payout, + }; + } + return totals; +}; + +const OpenPositions = ({ + active_positions, + component_icon, + currency, + error, + getPositionById, + is_loading, + is_multiplier, + NotificationMessages, + onClickCancel, + onClickSell, + onMount, + server_time, +}) => { + const [active_index, setActiveIndex] = React.useState(is_multiplier ? 1 : 0); + // Tabs should be visible only when there is at least one active multiplier contract + const [has_multiplier_contract, setMultiplierContract] = React.useState(false); + const previous_active_positions = usePrevious(active_positions); + + React.useEffect(() => { + /* + * For mobile, we show portfolio stepper in header even for reports pages. + * `onMount` in portfolio store will be invoked from portfolio stepper component in `trade-header-extensions.jsx` + */ + if (!isMobile()) onMount(); + + checkForMultiplierContract(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + checkForMultiplierContract(previous_active_positions); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [previous_active_positions]); + + const checkForMultiplierContract = (prev_active_positions = []) => { + if (!has_multiplier_contract && active_positions !== prev_active_positions) { + setMultiplierContract(active_positions.some(p => isMultiplierContract(p.contract_info?.contract_type))); + } + }; + + const setActiveTabIndex = index => setActiveIndex(index); + + if (error) return

{error}

; + + const is_multiplier_selected = has_multiplier_contract && active_index === 1; + const active_positions_filtered = active_positions?.filter(p => { + if (p.contract_info) { + return is_multiplier_selected + ? isMultiplierContract(p.contract_info.contract_type) + : !isMultiplierContract(p.contract_info.contract_type); + } + return true; + }); + + const active_positions_filtered_totals = getOpenPositionsTotals(active_positions_filtered, is_multiplier_selected); + + const columns = is_multiplier_selected + ? getMultiplierOpenPositionsColumnsTemplate({ + currency, + onClickCancel, + onClickSell, + getPositionById, + server_time, + }) + : getOpenPositionsColumnsTemplate(currency); + + const columns_map = columns.reduce((map, item) => { + map[item.col_index] = item; + return map; + }, {}); + + const mobileRowRenderer = props => ( + + ); + + const shared_props = { + active_positions: active_positions_filtered, + component_icon, + currency, + is_loading, + mobileRowRenderer, + getRowAction, + preloaderCheck: isPurchaseReceived, + totals: active_positions_filtered_totals, + }; + + return ( + + + {has_multiplier_contract ? ( + +
+ +
+
+ +
+
+ ) : ( + + )} +
+ ); +}; + +OpenPositions.propTypes = { + active_positions: MobxPropTypes.arrayOrObservableArray, + component_icon: PropTypes.string, + currency: PropTypes.string, + error: PropTypes.string, + getPositionById: PropTypes.func, + is_loading: PropTypes.bool, + is_multiplier: PropTypes.bool, + NotificationMessages: PropTypes.func, + onClickCancel: PropTypes.func, + onClickSell: PropTypes.func, + onMount: PropTypes.func, + server_time: PropTypes.object, +}; + +export default connect(({ modules, client, common, ui }) => ({ + active_positions: modules.portfolio.active_positions, + currency: client.currency, + error: modules.portfolio.error, + getPositionById: modules.portfolio.getPositionById, + is_loading: modules.portfolio.is_loading, + is_multiplier: modules.trade.is_multiplier, + NotificationMessages: ui.notification_messages_ui, + onClickCancel: modules.portfolio.onClickCancel, + onClickSell: modules.portfolio.onClickSell, + onMount: modules.portfolio.onMount, + server_time: common.server_time, +}))(withRouter(OpenPositions)); diff --git a/packages/reports/src/Containers/profit-table.jsx b/packages/reports/src/Containers/profit-table.jsx new file mode 100644 index 000000000000..9469288002ca --- /dev/null +++ b/packages/reports/src/Containers/profit-table.jsx @@ -0,0 +1,224 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import { PropTypes as MobxPropTypes } from 'mobx-react'; +import React from 'react'; +import { withRouter } from 'react-router'; +import { DesktopWrapper, MobileWrapper, DataList, DataTable } from '@deriv/components'; +import { extractInfoFromShortcode, isForwardStarting, urlFor, website_name } from '@deriv/shared'; +import { localize, Localize } from '@deriv/translations'; +import { ReportsTableRowLoader } from 'App/Components/Elements/ContentLoader'; +import CompositeCalendar from 'App/Components/Form/CompositeCalendar'; +import { getContractPath } from 'App/Components/Routes/helpers'; +import { getSupportedContracts } from 'Constants'; +import { connect } from 'Stores/connect'; +import EmptyTradeHistoryMessage from '../Components/empty-trade-history-message.jsx'; +import PlaceholderComponent from '../Components/placeholder-component.jsx'; +import { ReportsMeta } from '../Components/reports-meta.jsx'; +import { getProfitTableColumnsTemplate } from '../Constants/data-table-constants'; + +const getRowAction = row_obj => + getSupportedContracts()[extractInfoFromShortcode(row_obj.shortcode).category.toUpperCase()] && + !isForwardStarting(row_obj.shortcode, row_obj.purchase_time_unix) + ? getContractPath(row_obj.contract_id) + : { + component: ( + , + ]} + /> + ), + }; + +const ProfitTable = ({ + component_icon, + currency, + data, + date_from, + date_to, + error, + filtered_date_range, + is_empty, + is_loading, + is_switching, + handleDateChange, + handleScroll, + has_selected_date, + onMount, + onUnmount, + totals, +}) => { + React.useEffect(() => { + onMount(); + return () => { + onUnmount(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (error) return

{error}

; + + const filter_component = ( + + ); + + const columns = getProfitTableColumnsTemplate(currency, data.length); + const columns_map = columns.reduce((map, item) => { + map[item.col_index] = item; + return map; + }, {}); + + const mobileRowRenderer = ({ row, is_footer }) => { + const duration_type = /^MULTUP|MULTDOWN/.test(row.shortcode) ? '' : row.duration_type; + const duration_classname = duration_type ? `duration-type__${duration_type.toLowerCase()}` : ''; + + if (is_footer) { + return ( +
+ + +
+ ); + } + + return ( + <> +
+ +
+
+ {localize(duration_type)} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + ); + }; + + return ( + + + {is_switching ? ( + + ) : ( + + {data.length === 0 || is_empty ? ( + + ) : ( +
+ + 63} + content_loader={ReportsTableRowLoader} + > + + + + + + + + +
+ )} +
+ )} +
+ ); +}; + +ProfitTable.propTypes = { + component_icon: PropTypes.string, + currency: PropTypes.string, + data: MobxPropTypes.arrayOrObservableArray, + date_from: PropTypes.number, + date_to: PropTypes.number, + error: PropTypes.string, + filtered_date_range: PropTypes.object, + is_empty: PropTypes.bool, + is_loading: PropTypes.bool, + is_switching: PropTypes.bool, + handleDateChange: PropTypes.func, + handleScroll: PropTypes.func, + has_selected_date: PropTypes.bool, + onMount: PropTypes.func, + onUnmount: PropTypes.func, + totals: PropTypes.object, +}; + +export default connect(({ modules, client }) => ({ + currency: client.currency, + data: modules.profit_table.data, + date_from: modules.profit_table.date_from, + date_to: modules.profit_table.date_to, + error: modules.profit_table.error, + filtered_date_range: modules.profit_table.filtered_date_range, + is_empty: modules.profit_table.is_empty, + is_loading: modules.profit_table.is_loading, + is_switching: client.is_switching, + handleDateChange: modules.profit_table.handleDateChange, + handleScroll: modules.profit_table.handleScroll, + has_selected_date: modules.profit_table.has_selected_date, + onMount: modules.profit_table.onMount, + onUnmount: modules.profit_table.onUnmount, + totals: modules.profit_table.totals, +}))(withRouter(ProfitTable)); diff --git a/packages/reports/src/Containers/reports.jsx b/packages/reports/src/Containers/reports.jsx new file mode 100644 index 000000000000..871bb6307a27 --- /dev/null +++ b/packages/reports/src/Containers/reports.jsx @@ -0,0 +1,131 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { + Div100vhContainer, + VerticalTab, + DesktopWrapper, + MobileWrapper, + FadeWrapper, + PageOverlay, + SelectNative, + Loading, +} from '@deriv/components'; +import { getSelectedRoute } from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import { connect } from 'Stores/connect'; +import 'Sass/app/modules/reports.scss'; + +const Reports = ({ + history, + is_logged_in, + is_logging_in, + is_visible, + location, + routeBackInApp, + routes, + setTabIndex, + setVisibilityRealityCheck, + tab_index, + toggleReports, +}) => { + React.useEffect(() => { + toggleReports(true); + + return () => { + setVisibilityRealityCheck(1); + toggleReports(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onClickClose = () => routeBackInApp(history); + + const handleRouteChange = e => history.push(e.target.value); + + const menu_options = () => { + const options = []; + + routes.forEach(route => { + options.push({ + default: route.default, + icon: route.icon_component, + label: route.getTitle(), + value: route.component, + path: route.path, + }); + }); + + return options; + }; + + const selected_route = getSelectedRoute({ routes, pathname: location.pathname }); + + if (!is_logged_in && is_logging_in) { + return ; + } + return ( + +
+ + + + + + + ({ + text: option.label, + value: option.path, + }))} + value={selected_route.path} + should_show_empty_option={false} + onChange={handleRouteChange} + /> + {selected_route && ( + + )} + + + +
+
+ ); +}; + +Reports.propTypes = { + history: PropTypes.object, + is_logged_in: PropTypes.bool, + is_logging_in: PropTypes.bool, + is_visible: PropTypes.bool, + location: PropTypes.object, + routeBackInApp: PropTypes.func, + routes: PropTypes.arrayOf(PropTypes.object), + setTabIndex: PropTypes.func, + setVisibilityRealityCheck: PropTypes.func, + tab_index: PropTypes.number, + toggleReports: PropTypes.func, +}; + +export default connect(({ client, common, ui }) => ({ + is_logged_in: client.is_logged_in, + is_logging_in: client.is_logging_in, + is_visible: ui.is_reports_visible, + routeBackInApp: common.routeBackInApp, + setVisibilityRealityCheck: client.setVisibilityRealityCheck, + setTabIndex: ui.setReportsTabIndex, + tab_index: ui.reports_route_tab_index, + toggleReports: ui.toggleReports, +}))(withRouter(Reports)); diff --git a/packages/reports/src/Containers/routes.jsx b/packages/reports/src/Containers/routes.jsx new file mode 100644 index 000000000000..5bc21076f20d --- /dev/null +++ b/packages/reports/src/Containers/routes.jsx @@ -0,0 +1,39 @@ +import { PropTypes as MobxPropTypes } from 'mobx-react'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { withRouter } from 'react-router'; +import BinaryRoutes from '../Components/Routes'; +import { connect } from '../Stores/connect'; +import ErrorComponent from '../Components/Errors'; + +const Routes = props => { + if (props.has_error) { + return ; + } + + return ( + + ); +}; + +Routes.propTypes = { + error: MobxPropTypes.objectOrObservableObject, + has_error: PropTypes.bool, + is_logged_in: PropTypes.bool, + is_virtual: PropTypes.bool, +}; + +// need to wrap withRouter around connect +// to prevent updates on from being blocked +export default withRouter( + connect(({ client, common }) => ({ + is_logged_in: client.is_logged_in, + is_logging_in: client.is_logging_in, + error: common.error, + has_error: common.has_error, + }))(Routes) +); diff --git a/packages/reports/src/Containers/statement.jsx b/packages/reports/src/Containers/statement.jsx new file mode 100644 index 000000000000..1d5b36935b9d --- /dev/null +++ b/packages/reports/src/Containers/statement.jsx @@ -0,0 +1,282 @@ +import { PropTypes as MobxPropTypes } from 'mobx-react'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { DesktopWrapper, MobileWrapper, DataList, DataTable, Text, Clipboard } from '@deriv/components'; +import { extractInfoFromShortcode, isForwardStarting, urlFor, website_name } from '@deriv/shared'; +import { localize, Localize } from '@deriv/translations'; +import { ReportsTableRowLoader } from 'App/Components/Elements/ContentLoader'; +import { getContractPath } from 'App/Components/Routes/helpers'; +import { getSupportedContracts } from 'Constants'; +import { connect } from 'Stores/connect'; +import { getStatementTableColumnsTemplate } from '../Constants/data-table-constants'; +import PlaceholderComponent from '../Components/placeholder-component.jsx'; +import AccountStatistics from '../Components/account-statistics.jsx'; +import FilterComponent from '../Components/filter-component.jsx'; +import { ReportsMeta } from '../Components/reports-meta.jsx'; +import EmptyTradeHistoryMessage from '../Components/empty-trade-history-message.jsx'; + +const DetailsComponent = ({ message = '', action_type = '' }) => { + const address_hash_match = /:\s([0-9a-zA-Z]+.{25,28})/gm.exec(message.split(/,\s/)[0]); + const address_hash = address_hash_match?.[1]; + const blockchain_hash_match = /:\s([0-9a-zA-Z]+.{25,34})/gm.exec(message.split(/,\s/)[1]); + const blockchain_hash = blockchain_hash_match?.[1]; + + let messages = [message]; + + if (address_hash || blockchain_hash) { + const lines = message.split(/,\s/); + messages = lines.map((text, index) => { + if (index !== lines.length - 1) { + return `${text}, `; + } + return text; + }); + } + + return ( + + {messages.map((text, index) => { + return ( +
+ {text} + {blockchain_hash && index === messages.length - 1 && ( + + )} + {address_hash && action_type === 'withdrawal' && index === messages.length - 1 && ( + + )} +
+ ); + })} +
+ ); +}; + +const getRowAction = row_obj => { + let action; + if (row_obj.id && ['buy', 'sell'].includes(row_obj.action_type)) { + action = + getSupportedContracts()[extractInfoFromShortcode(row_obj.shortcode).category.toUpperCase()] && + !isForwardStarting(row_obj.shortcode, row_obj.purchase_time || row_obj.transaction_time) + ? getContractPath(row_obj.id) + : { + component: ( + , + ]} + /> + ), + }; + } else if (row_obj.action_type === 'withdrawal') { + if (row_obj.withdrawal_details && row_obj.longcode) { + action = { + message: `${row_obj.withdrawal_details} ${row_obj.longcode}`, + }; + } else { + action = { + message: row_obj.desc, + }; + } + } else if (row_obj.desc && ['deposit', 'transfer', 'adjustment', 'hold', 'release'].includes(row_obj.action_type)) { + action = { + message: row_obj.desc, + }; + } + + if (action?.message) { + action.component = ; + } + + return action; +}; + +const Statement = ({ + account_statistics, + action_type, + component_icon, + currency, + data, + date_from, + date_to, + error, + filtered_date_range, + handleDateChange, + handleFilterChange, + handleScroll, + has_selected_date, + is_empty, + is_loading, + is_mx_mlt, + is_switching, + is_virtual, + onMount, + onUnmount, +}) => { + React.useEffect(() => { + onMount(); + return () => { + onUnmount(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (error) return

{error}

; + + const columns = getStatementTableColumnsTemplate(currency); + const columns_map = columns.reduce((map, item) => { + map[item.col_index] = item; + return map; + }, {}); + + const mobileRowRenderer = ({ row, passthrough }) => ( + +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ ); + + return ( + + + } + is_statement + optional_component={ + !is_switching && + is_mx_mlt && + } + /> + {is_switching ? ( + + ) : ( + + {data.length === 0 || is_empty ? ( + + ) : ( +
+ + getRowAction(row)} + onScroll={handleScroll} + passthrough={{ + isTopUp: item => is_virtual && item.action === 'Deposit', + }} + > + + + + + is_virtual && item.action === 'Deposit', + }} + > + + + +
+ )} +
+ )} +
+ ); +}; + +Statement.propTypes = { + action_type: PropTypes.string, + account_statistics: PropTypes.object, + component_icon: PropTypes.string, + currency: PropTypes.string, + data: MobxPropTypes.arrayOrObservableArray, + date_from: PropTypes.number, + date_to: PropTypes.number, + error: PropTypes.string, + filtered_date_range: PropTypes.object, + handleDateChange: PropTypes.func, + handleFilterChange: PropTypes.func, + handleScroll: PropTypes.func, + has_selected_date: PropTypes.bool, + is_empty: PropTypes.bool, + is_loading: PropTypes.bool, + is_mx_mlt: PropTypes.bool, + is_switching: PropTypes.bool, + is_virtual: PropTypes.bool, + onMount: PropTypes.func, + onUnmount: PropTypes.func, +}; + +export default connect(({ modules, client }) => ({ + action_type: modules.statement.action_type, + account_statistics: modules.statement.account_statistics, + currency: client.currency, + data: modules.statement.data, + date_from: modules.statement.date_from, + date_to: modules.statement.date_to, + error: modules.statement.error, + filtered_date_range: modules.statement.filtered_date_range, + handleDateChange: modules.statement.handleDateChange, + handleFilterChange: modules.statement.handleFilterChange, + handleScroll: modules.statement.handleScroll, + has_selected_date: modules.statement.has_selected_date, + is_empty: modules.statement.is_empty, + is_loading: modules.statement.is_loading, + is_mx_mlt: client.standpoint.iom || client.standpoint.malta, + is_switching: client.is_switching, + is_virtual: client.is_virtual, + onMount: modules.statement.onMount, + onUnmount: modules.statement.onUnmount, +}))(withRouter(Statement)); diff --git a/packages/reports/src/Helpers/market-underlying.js b/packages/reports/src/Helpers/market-underlying.js new file mode 100644 index 000000000000..82363678b752 --- /dev/null +++ b/packages/reports/src/Helpers/market-underlying.js @@ -0,0 +1,44 @@ +import { getMarketNamesMap, getContractConfig } from 'Constants'; +import { localize } from '@deriv/translations'; + +/** + * Fetch market information from shortcode + * @param shortcode: string + * @returns {{underlying: string, category: string}} + */ + +// TODO: Combine with extractInfoFromShortcode function in shared, both are currently used +export const getMarketInformation = shortcode => { + const market_info = { + category: '', + underlying: '', + }; + + const pattern = new RegExp( + '^([A-Z]+)_((1HZ[0-9-V]+)|((CRASH|BOOM)[0-9\\d]+[A-Z]?)|(OTC_[A-Z0-9]+)|R_[\\d]{2,3}|[A-Z]+)' + ); + const extracted = pattern.exec(shortcode); + if (extracted !== null) { + market_info.category = extracted[1].toLowerCase(); + market_info.underlying = extracted[2]; + } + + return market_info; +}; + +export const getMarketName = underlying => (underlying ? getMarketNamesMap()[underlying.toUpperCase()] : null); + +export const getTradeTypeName = category => (category ? getContractConfig()[category.toUpperCase()].name : null); + +export const getContractDurationType = (longcode, shortcode) => { + if (/^MULTUP|MULTDOWN/.test(shortcode)) return ''; + + const duration_pattern = new RegExp('ticks|tick|seconds|minutes|minute|hour|hours'); + const extracted = duration_pattern.exec(longcode); + if (extracted !== null) { + const duration_type = extracted[0]; + const duration_text = duration_type[0].toUpperCase() + duration_type.slice(1); + return duration_text.endsWith('s') ? duration_text : `${duration_text}s`; + } + return localize('Days'); +}; diff --git a/packages/reports/src/Helpers/profit-loss.js b/packages/reports/src/Helpers/profit-loss.js new file mode 100644 index 000000000000..095262006b56 --- /dev/null +++ b/packages/reports/src/Helpers/profit-loss.js @@ -0,0 +1 @@ +export const getProfitOrLoss = value => (+value.replace(/,/g, '') >= 0 ? 'profit' : 'loss'); diff --git a/packages/reports/src/Modules/Page404/Components/Page404.jsx b/packages/reports/src/Modules/Page404/Components/Page404.jsx new file mode 100644 index 000000000000..981431ce85c8 --- /dev/null +++ b/packages/reports/src/Modules/Page404/Components/Page404.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { PageError } from '@deriv/components'; +import { routes, getUrlBase } from '@deriv/shared'; + +import { localize } from '@deriv/translations'; + +const Page404 = () => ( + +); + +export default Page404; diff --git a/packages/reports/src/Modules/Page404/index.js b/packages/reports/src/Modules/Page404/index.js new file mode 100644 index 000000000000..ccb77a533cf4 --- /dev/null +++ b/packages/reports/src/Modules/Page404/index.js @@ -0,0 +1 @@ +export default from './Components/Page404.jsx'; diff --git a/packages/reports/src/Stores/Modules/CFD/Helpers/cfd-config.ts b/packages/reports/src/Stores/Modules/CFD/Helpers/cfd-config.ts new file mode 100644 index 000000000000..b037d6c92d05 --- /dev/null +++ b/packages/reports/src/Stores/Modules/CFD/Helpers/cfd-config.ts @@ -0,0 +1,108 @@ +import { localize } from '@deriv/translations'; + +export type TDxCompanies = ReturnType; +export type TMtCompanies = ReturnType; + +export const getDxCompanies = () => { + const synthetic_config = { + account_type: '', + leverage: 500, + short_title: localize('Synthetic'), + }; + const financial_config = { + account_type: 'financial', + leverage: 1000, + short_title: localize('Financial'), + }; + return { + demo: { + synthetic: { + dxtrade_account_type: synthetic_config.account_type, + leverage: synthetic_config.leverage, + title: localize('Demo Synthetic'), + short_title: synthetic_config.short_title, + }, + financial: { + dxtrade_account_type: financial_config.account_type, + leverage: financial_config.leverage, + title: localize('Demo Financial'), + short_title: financial_config.short_title, + }, + }, + real: { + synthetic: { + dxtrade_account_type: synthetic_config.account_type, + leverage: synthetic_config.leverage, + title: localize('Synthetic'), + short_title: synthetic_config.short_title, + }, + financial: { + dxtrade_account_type: financial_config.account_type, + leverage: financial_config.leverage, + title: localize('Financial'), + short_title: financial_config.short_title, + }, + }, + }; +}; + +export const getMtCompanies = (is_eu: boolean) => { + const synthetic_config = { + account_type: '', + leverage: 500, + short_title: localize('Synthetic'), + }; + const financial_config = { + account_type: 'financial', + leverage: 1000, + short_title: is_eu ? localize('CFDs') : localize('Financial'), + }; + const financial_stp_config = { + account_type: 'financial_stp', + leverage: 100, + short_title: localize('Financial STP'), + }; + + return { + demo: { + synthetic: { + mt5_account_type: synthetic_config.account_type, + leverage: synthetic_config.leverage, + title: localize('Demo Synthetic'), + short_title: synthetic_config.short_title, + }, + financial: { + mt5_account_type: financial_config.account_type, + leverage: financial_config.leverage, + title: is_eu ? localize('Demo CFDs') : localize('Demo Financial'), + short_title: financial_config.short_title, + }, + financial_stp: { + mt5_account_type: financial_stp_config.account_type, + leverage: financial_stp_config.leverage, + title: localize('Demo Financial STP'), + short_title: financial_stp_config.short_title, + }, + }, + real: { + synthetic: { + mt5_account_type: synthetic_config.account_type, + leverage: synthetic_config.leverage, + title: localize('Synthetic'), + short_title: synthetic_config.short_title, + }, + financial: { + mt5_account_type: financial_config.account_type, + leverage: financial_config.leverage, + title: is_eu ? localize('CFDs') : localize('Financial'), + short_title: financial_config.short_title, + }, + financial_stp: { + mt5_account_type: financial_stp_config.account_type, + leverage: financial_stp_config.leverage, + title: localize('Financial STP'), + short_title: financial_stp_config.short_title, + }, + }, + }; +}; diff --git a/packages/reports/src/Stores/Modules/CFD/cfd-store.js b/packages/reports/src/Stores/Modules/CFD/cfd-store.js new file mode 100644 index 000000000000..ff125ca64f61 --- /dev/null +++ b/packages/reports/src/Stores/Modules/CFD/cfd-store.js @@ -0,0 +1,514 @@ +import { action, computed, observable, runInAction } from 'mobx'; +import { getAccountListKey, getAccountTypeFields, CFD_PLATFORMS, WS } from '@deriv/shared'; +import BaseStore from 'Stores/base-store'; +import { getDxCompanies, getMtCompanies } from './Helpers/cfd-config'; + +export default class CFDStore extends BaseStore { + @observable is_compare_accounts_visible = false; + @observable account_type = { + category: undefined, + type: undefined, + }; + + @observable new_account_response = {}; + @observable map_type = {}; + @observable has_cfd_error = false; + @observable error_message = ''; + + @observable is_cfd_success_dialog_enabled = false; + @observable is_mt5_financial_stp_modal_open = false; + @observable is_cfd_password_modal_enabled = false; + @observable is_cfd_reset_password_modal_enabled = false; + + @observable is_cfd_pending_dialog_open = false; + + @observable current_account = undefined; // this is a tmp value, don't rely on it, unless you set it first. + + @observable error_type = undefined; + + constructor({ root_store }) { + super({ root_store }); + } + + @computed + get has_cfd_account() { + return this.current_list.length > 0; + } + + @computed + get account_title() { + return this.account_type.category + ? getMtCompanies(this.root_store.client.is_eu)[this.account_type.category][this.account_type.type].title + : ''; + } + + @computed + get current_list() { + const list = []; + + this.root_store.client.mt5_login_list.forEach(account => { + // e.g. mt5.real.financial_stp + list[getAccountListKey(account, CFD_PLATFORMS.MT5)] = { + ...account, + }; + }); + + this.root_store.client.dxtrade_accounts_list.forEach(account => { + // e.g. dxtrade.real.financial_stp + list[getAccountListKey(account, CFD_PLATFORMS.DXTRADE)] = { + ...account, + }; + }); + + return list; + } + + // eslint-disable-next-line class-methods-use-this + get mt5_companies() { + return getMtCompanies(this.root_store.client.is_eu); + } + + // eslint-disable-next-line class-methods-use-this + get dxtrade_companies() { + return getDxCompanies(); + } + + @action.bound + onMount() { + this.checkShouldOpenAccount(); + this.onRealAccountSignupEnd(this.realAccountSignupEndListener); + this.root_store.ui.is_cfd_page = true; + } + + @action.bound + onUnmount() { + this.disposeRealAccountSignupEnd(); + this.root_store.ui.is_cfd_page = false; + } + + // other platforms can redirect to here using account switcher's `Add` account button + // so in that case we should open the corresponding account opening modal on load/component update + @action.bound + checkShouldOpenAccount() { + const account_type = sessionStorage.getItem('open_cfd_account_type'); + if (account_type) { + const [category, type, set_password] = account_type.split('.'); + this.createCFDAccount({ category, type, set_password }); + sessionStorage.removeItem('open_cfd_account_type'); + } + } + + @action.bound + realAccountSignupEndListener() { + const post_signup = JSON.parse(sessionStorage.getItem('post_real_account_signup')); + if (post_signup && post_signup.category && post_signup.type) { + sessionStorage.removeItem('post_real_account_signup'); + this.enableCFDPasswordModal(); + } + return Promise.resolve(); + } + + @action.bound + resetFormErrors() { + this.error_message = ''; + this.error_type = undefined; + this.has_cfd_error = false; + } + + @action.bound + clearCFDError() { + this.resetFormErrors(); + this.is_cfd_password_modal_enabled = false; + } + + @action.bound + createCFDAccount({ category, type, set_password }) { + this.clearCFDError(); + this.setAccountType({ + category, + type, + }); + + if (category === 'real') { + this.realCFDSignup(set_password); + } else { + this.demoCFDSignup(); + } + } + + demoCFDSignup() { + this.enableCFDPasswordModal(); + } + + @action.bound + disableCFDPasswordModal() { + this.is_cfd_password_modal_enabled = false; + } + + @action.bound + enableCFDPasswordModal() { + this.is_cfd_password_modal_enabled = true; + } + + @action.bound + getName() { + const { first_name } = this.root_store.client.account_settings && this.root_store.client.account_settings; + const title = this.mt5_companies[this.account_type.category][this.account_type.type].title; + + // First name is not set when user has no real account + return first_name ? [first_name, title].join(' ') : title; + } + + @action.bound + openMT5Account(values) { + const name = this.getName(); + const leverage = this.mt5_companies[this.account_type.category][this.account_type.type].leverage; + const type_request = getAccountTypeFields(this.account_type); + + return WS.mt5NewAccount({ + mainPassword: values.password, + email: this.root_store.client.email_address, + leverage, + name, + ...(values.server ? { server: values.server } : {}), + ...type_request, + }); + } + + @action.bound + openCFDAccount(values) { + return WS.tradingPlatformNewAccount({ + password: values.password, + platform: values.platform, + account_type: this.account_type.category, + market_type: this.account_type.type, + }); + } + + @action.bound + beginRealSignupForMt5() { + sessionStorage.setItem('post_real_account_signup', JSON.stringify(this.account_type)); + this.root_store.ui.openRealAccountSignup(); + } + + realCFDSignup(set_password) { + switch (this.account_type.type) { + case 'financial': + this.enableCFDPasswordModal(); + break; + case 'financial_stp': + this.root_store.client.fetchResidenceList(); + this.root_store.client.fetchStatesList(); + this.root_store.client.fetchAccountSettings(); + if (set_password) this.enableCFDPasswordModal(); + else this.enableMt5FinancialStpModal(); + break; + case 'synthetic': + this.enableCFDPasswordModal(); + break; + default: + throw new Error('Cannot determine mt5 account signup.'); + } + } + + @action.bound + enableMt5FinancialStpModal() { + if (this.account_type.category === 'real' && this.account_type.type === 'financial_stp') { + this.is_mt5_financial_stp_modal_open = true; + } + } + + @action.bound + setAccountType(account_type) { + this.account_type = account_type; + } + + @action.bound + setCurrentAccount(data, meta) { + this.current_account = { + ...meta, + ...data, + }; + } + + @action.bound + setError(state, obj) { + this.has_cfd_error = state; + this.error_message = obj ? obj.message : ''; + this.error_type = obj?.code ?? undefined; + } + + @action.bound + setCFDNewAccount(cfd_new_account) { + this.new_account_response = cfd_new_account; + } + + @action.bound + setCFDSuccessDialog(value) { + this.is_cfd_success_dialog_enabled = !!value; + } + + @action.bound + storeProofOfAddress(file_uploader_ref, values, { setStatus }) { + return new Promise((resolve, reject) => { + setStatus({ msg: '' }); + this.setState({ is_btn_loading: true }); + + WS.setSettings(values).then(data => { + if (data.error) { + setStatus({ msg: data.error.message }); + reject(data); + } else { + this.root_store.fetchAccountSettings(); + // force request to update settings cache since settings have been updated + file_uploader_ref.current.upload().then(api_response => { + if (api_response.warning) { + setStatus({ msg: api_response.message }); + reject(api_response); + } else { + WS.authorized.storage.getAccountStatus().then(({ error, get_account_status }) => { + if (error) { + reject(error); + } + const { identity } = get_account_status.authentication; + const has_poi = !(identity && identity.status === 'none'); + resolve({ + identity, + has_poi, + }); + }); + } + }); + } + }); + }); + } + + @action.bound + async getAccountStatus(platform) { + const should_load_account_status = + (platform === CFD_PLATFORMS.MT5 && this.root_store.client.is_mt5_password_not_set) || + (platform === CFD_PLATFORMS.DXTRADE && this.root_store.client.is_dxtrade_password_not_set); + + if (should_load_account_status) { + await WS.getAccountStatus(); + } + } + + @action.bound + async creatMT5Password(values, actions) { + const response = await WS.tradingPlatformPasswordChange({ + new_password: values.password, + platform: CFD_PLATFORMS.MT5, + }); + if (response.error) { + this.setError(true, response.error); + actions.resetForm({}); + actions.setSubmitting(false); + actions.setStatus({ success: false }); + return true; + } + return false; + } + + @action.bound + async submitMt5Password(values, actions) { + if (this.root_store.client.is_mt5_password_not_set) { + const has_error = await this.creatMT5Password(values, actions); + if (has_error) return; + } + + this.resetFormErrors(); + const response = await this.openMT5Account(values); + if (!response.error) { + actions.setStatus({ success: true }); + actions.setSubmitting(false); + this.setError(false); + this.setCFDSuccessDialog(true); + await this.getAccountStatus(CFD_PLATFORMS.MT5); + + const mt5_login_list_response = await WS.authorized.mt5LoginList(); + this.root_store.client.responseMt5LoginList(mt5_login_list_response); + + WS.transferBetweenAccounts(); // get the list of updated accounts for transfer in cashier + this.root_store.client.responseMT5TradingServers(await WS.tradingServers(CFD_PLATFORMS.MT5)); + this.setCFDNewAccount(response.mt5_new_account); + } else { + await this.getAccountStatus(CFD_PLATFORMS.MT5); + this.setError(true, response.error); + actions.resetForm({}); + actions.setSubmitting(false); + actions.setStatus({ success: false }); + } + } + + @action.bound + async createCFDPassword(values, actions) { + const response = await WS.tradingPlatformPasswordChange({ + new_password: values.password, + platform: CFD_PLATFORMS.DXTRADE, + }); + if (response.error) { + this.setError(true, response.error); + actions.resetForm({}); + actions.setSubmitting(false); + actions.setStatus({ success: false }); + return true; + } + + return false; + } + + @action.bound + async submitCFDPassword(values, actions) { + if (this.root_store.client.is_dxtrade_password_not_set) { + const has_error = await this.createCFDPassword(values, actions); + if (has_error) return; + } + + const response = await this.openCFDAccount(values); + if (!response.error) { + actions.setStatus({ success: true }); + actions.setSubmitting(false); + this.setError(false); + this.setCFDSuccessDialog(true); + await this.getAccountStatus(CFD_PLATFORMS.DXTRADE); + + const trading_platform_accounts_list_response = await WS.tradingPlatformAccountsList(values.platform); + this.root_store.client.responseTradingPlatformAccountsList(trading_platform_accounts_list_response); + + WS.transferBetweenAccounts(); // get the list of updated accounts for transfer in cashier + this.setCFDNewAccount(response.trading_platform_new_account); + } else { + await this.getAccountStatus(CFD_PLATFORMS.DXTRADE); + this.setError(true, response.error); + actions.resetForm({}); + actions.setSubmitting(false); + actions.setStatus({ success: false }); + } + } + + @action.bound + toggleCompareAccountsModal() { + this.is_compare_accounts_visible = !this.is_compare_accounts_visible; + } + + @action.bound + disableMt5FinancialStpModal() { + this.is_mt5_financial_stp_modal_open = false; + } + + @action.bound + async topUpVirtual(platform) { + this.root_store.ui.setTopUpInProgress(true); + let response; + + switch (platform) { + case CFD_PLATFORMS.DXTRADE: { + response = await WS.authorized.send({ + trading_platform_deposit: 1, + platform: CFD_PLATFORMS.DXTRADE, + to_account: this.current_account.account_id, + }); + break; + } + case CFD_PLATFORMS.MT5: { + response = await WS.authorized.mt5Deposit({ + to_mt5: this.current_account.login, + }); + break; + } + default: { + response.error = 'Invalid platform'; + break; + } + } + + if (!response.error) { + let new_balance; + switch (platform) { + case CFD_PLATFORMS.DXTRADE: { + await WS.authorized + .tradingPlatformAccountsList(CFD_PLATFORMS.DXTRADE) + .then(this.root_store.client.responseTradingPlatformAccountsList); + new_balance = this.root_store.client.dxtrade_accounts_list.find( + item => item.account_id === this.current_account.account_id + )?.balance; + break; + } + case CFD_PLATFORMS.MT5: { + await WS.authorized.mt5LoginList().then(this.root_store.client.responseMt5LoginList); + + new_balance = this.root_store.client.mt5_login_list.find( + item => item.login === this.current_account.login + )?.balance; + break; + } + default: { + break; + } + } + runInAction(() => { + // Get new current account + this.root_store.ui.is_top_up_virtual_open = false; + this.current_account.balance = new_balance; + }); + setTimeout(() => { + runInAction(() => { + this.root_store.ui.is_top_up_virtual_success = true; + }); + }, 250); + } else { + // eslint-disable-next-line no-console + console.error(response); + } + this.root_store.ui.setTopUpInProgress(false); + } + + @action.bound + closeCFDPendingDialog() { + this.is_cfd_pending_dialog_open = false; + } + + @action.bound + openPendingDialog() { + setTimeout( + runInAction(() => { + this.is_cfd_pending_dialog_open = true; + }), + 300 + ); + } + + @action.bound + sendVerifyEmail() { + return WS.verifyEmail(this.root_store.client.email, 'trading_platform_investor_password_reset'); + } + + @action.bound + setCFDPasswordResetModal(val) { + this.is_cfd_reset_password_modal_enabled = !!val; + } + + static async changePassword({ login, old_password, new_password, password_type }) { + let response; + + if (password_type === 'investor') { + response = await WS.authorized.tradingPlatformInvestorPasswordChange({ + account_id: login, + old_password, + new_password, + platform: CFD_PLATFORMS.MT5, + }); + } else { + response = await WS.authorized.tradingPlatformPasswordChange({ + account_id: login, + old_password, + new_password, + platform: CFD_PLATFORMS.MT5, + }); + } + + return response?.error?.message; + } +} diff --git a/packages/reports/src/Stores/Modules/index.js b/packages/reports/src/Stores/Modules/index.js new file mode 100644 index 000000000000..616d37bb7796 --- /dev/null +++ b/packages/reports/src/Stores/Modules/index.js @@ -0,0 +1,7 @@ +import CFDStore from './CFD/cfd-store'; + +export default class ModulesStore { + constructor(root_store) { + this.cfd = new CFDStore({ root_store }); + } +} diff --git a/packages/reports/src/Stores/base-store.js b/packages/reports/src/Stores/base-store.js new file mode 100644 index 000000000000..3a803f0c2a40 --- /dev/null +++ b/packages/reports/src/Stores/base-store.js @@ -0,0 +1,537 @@ +import { action, intercept, observable, reaction, toJS, when } from 'mobx'; +import { isProduction, isEmptyObject } from '@deriv/shared'; + +import Validator from '../Utils/Validator'; + +/** + * BaseStore class is the base class for all defined stores in the application. It handles some stuff such as: + * 1. Creating snapshot object from the store. + * 2. Saving the store's snapshot in local/session storage and keeping them in sync. + */ +export default class BaseStore { + /** + * An enum object to define LOCAL_STORAGE and SESSION_STORAGE + */ + static STORAGES = Object.freeze({ + LOCAL_STORAGE: Symbol('LOCAL_STORAGE'), + SESSION_STORAGE: Symbol('SESSION_STORAGE'), + }); + + @observable + validation_errors = {}; + + @observable + validation_rules = {}; + + preSwitchAccountDisposer = null; + pre_switch_account_listener = null; + + switchAccountDisposer = null; + switch_account_listener = null; + + logoutDisposer = null; + logout_listener = null; + + clientInitDisposer = null; + client_init_listener = null; + + networkStatusChangeDisposer = null; + network_status_change_listener = null; + + themeChangeDisposer = null; + theme_change_listener = null; + + realAccountSignupEndedDisposer = null; + real_account_signup_ended_listener = null; + + @observable partial_fetch_time = 0; + + /** + * Constructor of the base class that gets properties' name of child which should be saved in storages + * + * @param {Object} options - An object that contains the following properties: + * @property {Object} root_store - An object that contains the root store of the app. + * @property {String[]} local_storage_properties - A list of properties' names that should be kept in localStorage. + * @property {String[]} session_storage_properties - A list of properties' names that should be kept in sessionStorage. + * @property {Object} validation_rules - An object that contains the validation rules for each property of the store. + * @property {String} store_name - Explicit store name for browser application storage (to bypass minification) + */ + constructor(options = {}) { + const { root_store, local_storage_properties, session_storage_properties, validation_rules, store_name } = + options; + + Object.defineProperty(this, 'root_store', { + enumerable: false, + writable: true, + }); + Object.defineProperty(this, 'local_storage_properties', { + enumerable: false, + writable: true, + }); + Object.defineProperty(this, 'session_storage_properties', { + enumerable: false, + writable: true, + }); + + const has_local_or_session_storage = + (local_storage_properties && local_storage_properties.length) || + (session_storage_properties && session_storage_properties.length); + + if (has_local_or_session_storage) { + if (!store_name) { + throw new Error('store_name is required for local/session storage'); + } + + Object.defineProperty(this, 'store_name', { + value: store_name, + enumerable: false, + writable: false, + }); + } + + this.root_store = root_store; + this.local_storage_properties = local_storage_properties || []; + this.session_storage_properties = session_storage_properties || []; + this.setValidationRules(validation_rules); + + this.setupReactionForLocalStorage(); + this.setupReactionForSessionStorage(); + this.retrieveFromStorage(); + } + + /** + * Returns an snapshot of the current store + * + * @param {String[]} properties - A list of properties' names that should be in the snapshot. + * + * @return {Object} Returns a cloned object of the store. + */ + getSnapshot(properties) { + let snapshot = toJS(this); + + if (!isEmptyObject(this.root_store)) { + snapshot.root_store = this.root_store; + } + + if (properties && properties.length) { + snapshot = properties.reduce((result, p) => Object.assign(result, { [p]: snapshot[p] }), {}); + } + + return snapshot; + } + + /** + * Sets up a reaction on properties which are mentioned in `local_storage_properties` + * and invokes `saveToStorage` when there are any changes on them. + * + */ + setupReactionForLocalStorage() { + if (this.local_storage_properties.length) { + reaction( + () => this.local_storage_properties.map(i => this[i]), + () => this.saveToStorage(this.local_storage_properties, BaseStore.STORAGES.LOCAL_STORAGE) + ); + } + } + + /** + * Sets up a reaction on properties which are mentioned in `session_storage_properties` + * and invokes `saveToStorage` when there are any changes on them. + * + */ + setupReactionForSessionStorage() { + if (this.session_storage_properties.length) { + reaction( + () => this.session_storage_properties.map(i => this[i]), + () => this.saveToStorage(this.session_storage_properties, BaseStore.STORAGES.SESSION_STORAGE) + ); + } + } + + /** + * Removes properties that are not passed from the snapshot of the store and saves it to the passed storage + * + * @param {String[]} properties - A list of the store's properties' names which should be saved in the storage. + * @param {Symbol} storage - A symbol object that defines the storage which the snapshot should be stored in it. + * + */ + saveToStorage(properties, storage) { + const snapshot = JSON.stringify(this.getSnapshot(properties), (key, value) => { + if (value !== null) return value; + return undefined; + }); + + if (storage === BaseStore.STORAGES.LOCAL_STORAGE) { + localStorage.setItem(this.store_name, snapshot); + } else if (storage === BaseStore.STORAGES.SESSION_STORAGE) { + sessionStorage.setItem(this.store_name, snapshot); + } + } + + /** + * Retrieves saved snapshot of the store and assigns to the current instance. + * + */ + @action + retrieveFromStorage() { + const local_storage_snapshot = JSON.parse(localStorage.getItem(this.store_name, {})); + const session_storage_snapshot = JSON.parse(sessionStorage.getItem(this.store_name, {})); + + const snapshot = { ...local_storage_snapshot, ...session_storage_snapshot }; + + Object.keys(snapshot).forEach(k => (this[k] = snapshot[k])); + } + + /** + * Sets validation error messages for an observable property of the store + * + * @param {String} propertyName - The observable property's name + * @param [{String}] messages - An array of strings that contains validation error messages for the particular property. + * + */ + @action + setValidationErrorMessages(propertyName, messages) { + const is_different = () => + !!this.validation_errors[propertyName] + .filter(x => !messages.includes(x)) + .concat(messages.filter(x => !this.validation_errors[propertyName].includes(x))).length; + if (!this.validation_errors[propertyName] || is_different()) { + this.validation_errors[propertyName] = messages; + } + } + + /** + * Sets validation rules + * + * @param {object} rules + * + */ + @action + setValidationRules(rules = {}) { + Object.keys(rules).forEach(key => { + this.addRule(key, rules[key]); + }); + } + + /** + * Adds rules to the particular property + * + * @param {String} property + * @param {String} rules + * + */ + @action + addRule(property, rules) { + this.validation_rules[property] = rules; + + intercept(this, property, change => { + this.validateProperty(property, change.newValue); + return change; + }); + } + + /** + * Validates a particular property of the store + * + * @param {String} property - The name of the property in the store + * @param {object} value - The value of the property, it can be undefined. + * + */ + @action + validateProperty(property, value) { + const trigger = this.validation_rules[property].trigger; + const inputs = { [property]: value !== undefined ? value : this[property] }; + const validation_rules = { [property]: this.validation_rules[property].rules || [] }; + + if (!!trigger && Object.hasOwnProperty.call(this, trigger)) { + inputs[trigger] = this[trigger]; + validation_rules[trigger] = this.validation_rules[trigger].rules || []; + } + + const validator = new Validator(inputs, validation_rules, this); + + validator.isPassed(); + + Object.keys(inputs).forEach(key => { + this.setValidationErrorMessages(key, validator.errors.get(key)); + }); + } + + /** + * Validates all properties which validation rule has been set for. + * + */ + @action + validateAllProperties() { + const validation_rules = Object.keys(this.validation_rules); + const validation_errors = Object.keys(this.validation_errors); + + validation_rules.forEach(p => { + this.validateProperty(p, this[p]); + }); + + // Remove keys that are present in error, but not in rules: + validation_errors.forEach(error => { + if (!validation_rules.includes(error)) { + delete this.validation_errors[error]; + } + }); + } + + @action.bound + onSwitchAccount(listener) { + if (listener) { + this.switch_account_listener = listener; + + this.switchAccountDisposer = when( + () => this.root_store.client.switch_broadcast, + () => { + try { + const result = this.switch_account_listener(); + if (result && result.then && typeof result.then === 'function') { + result.then(() => { + this.root_store.client.switchEndSignal(); + this.onSwitchAccount(this.switch_account_listener); + }); + } else { + throw new Error('Switching account listeners are required to return a promise.'); + } + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + } + } + + @action.bound + onPreSwitchAccount(listener) { + if (listener) { + this.pre_switch_account_listener = listener; + this.preSwitchAccountDisposer = when( + () => this.root_store.client.pre_switch_broadcast, + () => { + try { + const result = this.pre_switch_account_listener(); + if (result && result.then && typeof result.then === 'function') { + result.then(() => { + this.root_store.client.setPreSwitchAccount(false); + this.onPreSwitchAccount(this.pre_switch_account_listener); + }); + } else { + throw new Error('Pre-switch account listeners are required to return a promise.'); + } + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + } + } + + @action.bound + onLogout(listener) { + this.logoutDisposer = when( + () => this.root_store.client.has_logged_out, + async () => { + try { + const result = this.logout_listener(); + if (result && result.then && typeof result.then === 'function') { + result.then(() => { + this.root_store.client.setLogout(false); + this.onLogout(this.logout_listener); + }); + } else { + throw new Error('Logout listeners are required to return a promise.'); + } + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + this.logout_listener = listener; + } + + @action.bound + onClientInit(listener) { + this.clientInitDisposer = when( + () => this.root_store.client.initialized_broadcast, + async () => { + try { + const result = this.client_init_listener(); + if (result && result.then && typeof result.then === 'function') { + result.then(() => { + this.root_store.client.setInitialized(false); + this.onClientInit(this.client_init_listener); + }); + } else { + throw new Error('Client init listeners are required to return a promise.'); + } + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + this.client_init_listener = listener; + } + + @action.bound + onNetworkStatusChange(listener) { + this.networkStatusChangeDisposer = reaction( + () => this.root_store.common.is_network_online, + is_online => { + try { + this.network_status_change_listener(is_online); + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + + this.network_status_change_listener = listener; + } + + @action.bound + onThemeChange(listener) { + this.themeChangeDisposer = reaction( + () => this.root_store.ui.is_dark_mode_on, + is_dark_mode_on => { + try { + this.theme_change_listener(is_dark_mode_on); + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + + this.theme_change_listener = listener; + } + + @action.bound + onRealAccountSignupEnd(listener) { + this.realAccountSignupEndedDisposer = when( + () => this.root_store.ui.has_real_account_signup_ended, + () => { + try { + const result = this.real_account_signup_ended_listener(); + if (result && result.then && typeof result.then === 'function') { + result.then(() => { + this.root_store.ui.setRealAccountSignupEnd(false); + this.onRealAccountSignupEnd(this.real_account_signup_ended_listener); + }); + } else { + throw new Error('Real account signup listeners are required to return a promise.'); + } + } catch (error) { + // there is no listener currently active. so we can just ignore the error raised from treating + // a null object as a function. Although, in development mode, we throw a console error. + if (!isProduction()) { + console.error(error); // eslint-disable-line + } + } + } + ); + + this.real_account_signup_ended_listener = listener; + } + + @action.bound + disposePreSwitchAccount() { + if (typeof this.preSwitchAccountDisposer === 'function') { + this.preSwitchAccountDisposer(); + } + this.pre_switch_account_listener = null; + } + + @action.bound + disposeSwitchAccount() { + if (typeof this.switchAccountDisposer === 'function') { + this.switchAccountDisposer(); + } + this.switch_account_listener = null; + } + + @action.bound + disposeLogout() { + if (typeof this.logoutDisposer === 'function') { + this.logoutDisposer(); + } + this.logout_listener = null; + } + + @action.bound + disposeClientInit() { + if (typeof this.clientInitDisposer === 'function') { + this.clientInitDisposer(); + } + this.client_init_listener = null; + } + + @action.bound + disposeNetworkStatusChange() { + if (typeof this.networkStatusChangeDisposer === 'function') { + this.networkStatusChangeDisposer(); + } + this.network_status_change_listener = null; + } + + @action.bound + disposeThemeChange() { + if (typeof this.themeChangeDisposer === 'function') { + this.themeChangeDisposer(); + } + this.theme_change_listener = null; + } + + @action.bound + disposeRealAccountSignupEnd() { + if (typeof this.realAccountSignupEndedDisposer === 'function') { + this.realAccountSignupEndedDisposer(); + } + this.real_account_signup_ended_listener = null; + } + + @action.bound + onUnmount() { + this.disposePreSwitchAccount(); + this.disposeSwitchAccount(); + this.disposeLogout(); + this.disposeClientInit(); + this.disposeNetworkStatusChange(); + this.disposeThemeChange(); + this.disposeRealAccountSignupEnd(); + } + + @action.bound + assertHasValidCache(loginid, ...reactions) { + // account was changed when this was unmounted. + if (this.root_store.client.loginid !== loginid) { + reactions.forEach(act => act()); + this.partial_fetch_time = false; + } + } +} diff --git a/packages/reports/src/Stores/connect.js b/packages/reports/src/Stores/connect.js new file mode 100644 index 000000000000..4ef42c8d18b6 --- /dev/null +++ b/packages/reports/src/Stores/connect.js @@ -0,0 +1,31 @@ +import { useObserver } from 'mobx-react'; +import React from 'react'; + +const isClassComponent = Component => + !!(typeof Component === 'function' && Component.prototype && Component.prototype.isReactComponent); + +export const MobxContent = React.createContext(null); + +function injectStorePropsToComponent(propsToSelectFn, BaseComponent) { + const Component = own_props => { + const store = React.useContext(MobxContent); + + let ObservedComponent = BaseComponent; + + if (isClassComponent(BaseComponent)) { + const FunctionalWrapperComponent = props => ; + ObservedComponent = FunctionalWrapperComponent; + } + + return useObserver(() => ObservedComponent({ ...own_props, ...propsToSelectFn(store, own_props) })); + }; + + Component.displayName = BaseComponent.name; + return Component; +} + +export const MobxContentProvider = ({ store, children }) => { + return {children}; +}; + +export const connect = propsToSelectFn => Component => injectStorePropsToComponent(propsToSelectFn, Component); diff --git a/packages/reports/src/Stores/index.js b/packages/reports/src/Stores/index.js new file mode 100644 index 000000000000..fef448cd42cb --- /dev/null +++ b/packages/reports/src/Stores/index.js @@ -0,0 +1,14 @@ +import ModulesStore from './Modules'; + +export default class RootStore { + constructor(core_store) { + this.client = core_store.client; + this.common = core_store.common; + this.modules = new ModulesStore(this, core_store); + this.ui = core_store.ui; + this.gtm = core_store.gtm; + this.rudderstack = core_store.rudderstack; + this.pushwoosh = core_store.pushwoosh; + this.notifications = core_store.notifications; + } +} diff --git a/packages/reports/src/_common/__tests__/utility.js b/packages/reports/src/_common/__tests__/utility.js new file mode 100644 index 000000000000..f138acbc03e1 --- /dev/null +++ b/packages/reports/src/_common/__tests__/utility.js @@ -0,0 +1,16 @@ +const expect = require('chai').expect; +const Utility = require('../utility'); + +describe('Utility', () => { + describe('.template()', () => { + it('works as expected', () => { + expect(Utility.template('abc [_1] abc', ['2'])).to.eq('abc 2 abc'); + expect(Utility.template('[_1] [_2]', ['1', '2'])).to.eq('1 2'); + expect(Utility.template('[_1] [_1]', ['1'])).to.eq('1 1'); + }); + + it('does not replace twice', () => { + expect(Utility.template('[_1] [_2]', ['[_2]', 'abc'])).to.eq('[_2] abc'); + }); + }); +}); diff --git a/packages/reports/src/_common/base/server_time.js b/packages/reports/src/_common/base/server_time.js new file mode 100644 index 000000000000..b639380b27a5 --- /dev/null +++ b/packages/reports/src/_common/base/server_time.js @@ -0,0 +1,25 @@ +const PromiseClass = require('../utility').PromiseClass; + +const ServerTime = (() => { + let clock_started = false; + const pending = new PromiseClass(); + let common_store; + + const init = store => { + if (!clock_started) { + common_store = store; + pending.resolve(common_store.server_time); + clock_started = true; + } + }; + + const get = () => (clock_started && common_store.server_time ? common_store.server_time.clone() : undefined); + + return { + init, + get, + timePromise: () => (clock_started ? Promise.resolve(common_store.server_time) : pending.promise), + }; +})(); + +module.exports = ServerTime; diff --git a/packages/reports/src/_common/utility.js b/packages/reports/src/_common/utility.js new file mode 100644 index 000000000000..5c20dc5c33e0 --- /dev/null +++ b/packages/reports/src/_common/utility.js @@ -0,0 +1,52 @@ +const template = (string, content) => { + let to_replace = content; + if (content && !Array.isArray(content)) { + to_replace = [content]; + } + return string.replace(/\[_(\d+)]/g, (s, index) => to_replace[+index - 1]); +}; + +/** + * Creates a DOM element and adds any attributes to it. + * + * @param {String} tag_name: the tag to create, e.g. 'div', 'a', etc + * @param {Object} attributes: all the attributes to assign, e.g. { id: '...', class: '...', html: '...', ... } + * @return the created DOM element + */ +const createElement = (tag_name, attributes = {}) => { + const el = document.createElement(tag_name); + Object.keys(attributes).forEach(attr => { + const value = attributes[attr]; + if (attr === 'text') { + el.textContent = value; + } else if (attr === 'html') { + el.html(value); + } else { + el.setAttribute(attr, value); + } + }); + return el; +}; + +let static_hash; +const getStaticHash = () => { + static_hash = + static_hash || (document.querySelector('script[src*="main"]').getAttribute('src') || '').split('.')[1]; + return static_hash; +}; + +class PromiseClass { + constructor() { + this.promise = new Promise((resolve, reject) => { + this.reject = reject; + this.resolve = resolve; + }); + } +} + +module.exports = { + template, + createElement, + getStaticHash, + PromiseClass, +}; diff --git a/packages/reports/src/app.jsx b/packages/reports/src/app.jsx new file mode 100644 index 000000000000..1c4be2c3999c --- /dev/null +++ b/packages/reports/src/app.jsx @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Routes from './Containers/routes.jsx'; +import { MobxContentProvider } from './Stores/connect'; +import initStore from './init-store'; // eslint-disable-line import/extensions + +const App = ({ passthrough }) => { + const [root_store] = React.useState(initStore(passthrough.root_store, passthrough.WS)); + + return ( + + + + + + ); +}; + +App.propTypes = { + passthrough: PropTypes.shape({ + root_store: PropTypes.object, + WS: PropTypes.object, + }), +}; + +export default App; diff --git a/packages/reports/src/index.jsx b/packages/reports/src/index.jsx new file mode 100644 index 000000000000..c5c2db91b385 --- /dev/null +++ b/packages/reports/src/index.jsx @@ -0,0 +1,14 @@ +import 'promise-polyfill'; + +import 'event-source-polyfill'; + +import React from 'react'; +import { makeLazyLoader } from '@deriv/shared'; +import { Loading } from '@deriv/components'; + +const App = makeLazyLoader( + () => import(/* webpackChunkName: "reports-app", webpackPreload: true */ './app.jsx'), + () => +)(); + +export default App; diff --git a/packages/reports/src/init-store.js b/packages/reports/src/init-store.js new file mode 100644 index 000000000000..71149f32bcd8 --- /dev/null +++ b/packages/reports/src/init-store.js @@ -0,0 +1,20 @@ +import { configure } from 'mobx'; +import RootStore from './Stores'; +import { setWebsocket } from '@deriv/shared'; +import ServerTime from '_common/base/server_time'; + +configure({ enforceActions: 'observed' }); + +let root_store; + +const initStore = (core_store, websocket) => { + if (root_store) return root_store; + + ServerTime.init(core_store.common); + setWebsocket(websocket); + root_store = new RootStore(core_store); + + return root_store; +}; + +export default initStore; diff --git a/packages/reports/src/sass/app.scss b/packages/reports/src/sass/app.scss new file mode 100644 index 000000000000..841cbe4696ac --- /dev/null +++ b/packages/reports/src/sass/app.scss @@ -0,0 +1,38 @@ +// Layout +@import 'app/_common/layout/trader-layouts'; + +// Drawers +@import 'app/_common/drawer/contract-drawer'; +@import 'app/_common/drawer/positions-drawer'; +@import 'app/_common/drawer/positions-modal-card'; + +// Form +@import 'app/_common/form/time-picker'; + +// Components +@import 'app/_common/components/amount'; +@import 'app/_common/components/allow-equals'; +//@import 'app/_common/components/calendar'; // TODO: [move-to-components] Calendar component should be moved +@import 'app/_common/components/card-list'; +@import 'app/_common/components/contract-audit'; +@import 'app/_common/components/contract-replay'; +@import 'app/_common/components/contract-type-dialog'; +@import 'app/_common/components/contract-type-widget'; +@import 'app/_common/components/contract-type-list'; +@import 'app/_common/components/contract-type-info'; +@import 'app/_common/components/contract-type-no-result'; +//@import 'app/_common/components/date-picker'; // TODO: [move-to-components] Datepicker component should be moved +@import 'app/_common/components/market-is-closed-overlay'; +@import 'app/_common/components/market-symbol-icon'; +@import 'app/_common/components/number-selector'; +@import 'app/_common/components/popconfirm'; +@import 'app/_common/components/purchase-button'; +@import 'app/_common/components/range-slider'; +@import 'app/_common/components/toggle-button'; + +// Modules +@import 'app/modules/contract'; +@import 'app/modules/contract/bottom-widgets'; +@import 'app/modules/portfolio'; +@import 'app/modules/smart-chart'; +@import 'app/modules/trading'; diff --git a/packages/reports/src/sass/app/_common/components/allow-equals.scss b/packages/reports/src/sass/app/_common/components/allow-equals.scss new file mode 100644 index 000000000000..ea3d2b81384c --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/allow-equals.scss @@ -0,0 +1,19 @@ +/** @define allow-equals */ +.allow-equals { + display: flex; + align-items: center; + position: relative; + margin-top: 0.6em; + + &__label { + @include typeface(--paragraph-left-normal-black, none); + @include toEm(padding, 0 8px, 1.2em); + color: var(--text-general); + cursor: pointer; + } + @include mobile { + &__subtitle { + color: var(--text-less-prominent); + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/amount.scss b/packages/reports/src/sass/app/_common/components/amount.scss new file mode 100644 index 000000000000..ab4916398f02 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/amount.scss @@ -0,0 +1,8 @@ +.amount { + &--profit { + color: var(--text-profit-success); + } + &--loss { + color: var(--text-loss-danger); + } +} diff --git a/packages/reports/src/sass/app/_common/components/card-list.scss b/packages/reports/src/sass/app/_common/components/card-list.scss new file mode 100644 index 000000000000..9d4017837c29 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/card-list.scss @@ -0,0 +1,20 @@ +/** @define card-list */ +.card-list { + overflow: auto; // fixes margin collapse + + & &__card { + // keep & for higher specificity + display: block; + text-decoration: none; + max-width: 450px; + margin: 0.6em auto; + border-radius: 4px; + background-color: var(--general-main-2); + border: 1px solid var(--general-main-2); + color: var(--text-prominent); + + &-link { + cursor: pointer; + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/composite-calendar.scss b/packages/reports/src/sass/app/_common/components/composite-calendar.scss new file mode 100644 index 000000000000..e05ee710e3a6 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/composite-calendar.scss @@ -0,0 +1,183 @@ +/* @define .composite-calendar; weak; */ +.composite-calendar { + display: grid; + grid-template-columns: 128px minmax(min-content, 280px) minmax(min-content, 280px); + position: absolute; + top: 36px; + right: 0; + z-index: 99; + border-radius: $BORDER_RADIUS; + background-color: var(--general-main-2); + box-shadow: 0 2px 16px 8px var(--shadow-menu); + + .composite-wrapper { + position: absolute; + width: 100%; + height: 100%; + background-color: var(--general-main-1); + z-index: 98; + } + &__input-fields { + display: flex; + border-radius: $BORDER_RADIUS; + width: 100%; + + &--fill { + width: 100%; + + & > .dc-input-field { + width: 100%; + } + } + & > .dc-input-field { + margin: 0; + width: 100%; + + @include desktop { + max-width: 17.6rem; + } + + @include mobile { + .inline-icon { + top: 1.2rem; + } + } + + @include colorIcon(var(--text-prominent)); + + & .input { + height: 3.2rem; + background-color: var(--fill-normal); + border: 1px solid var(--border-normal); + appearance: none; + + @include mobile { + height: 4rem; + text-align: left; + padding-left: 3rem; + } + + &:hover { + border-color: var(--border-hover); + } + &:focus, + &:active { + border-color: var(--border-active); + } + &::placeholder { + color: var(--text-general); + } + } + } + & > .dc-input-field:not(:first-child) { + margin-left: 8px; + } + } + & > .first-month, + & > .second-month { + .dc-calendar__body { + border-bottom: none; + } + } + &__prepopulated-list { + padding-top: 50px; + @include typeface(--paragraph-center-normal-black); + color: var(--text-prominent); + background: var(--state-normal); + + &--is-active { + color: var(--text-prominent); + background-color: var(--state-active); + font-weight: bold; + } + & li { + cursor: pointer; + padding: 6px 6px 6px 16px; + height: 32px; + display: flex; + align-items: center; + + &:hover:not(.composite-calendar__prepopulated-list--is-active) { + background: var(--state-hover); + } + } + } +} + +/* @define composite-calendar-modal; weak; */ +.composite-calendar-modal { + @include mobile { + &__actions { + display: flex; + padding: 16px; + border-top: 2px solid var(--border-disabled); + + > * { + flex: 1; + margin: 8px; + } + &-today { + width: 100%; + } + } + &__radio-group { + padding: 16px 16px 24px; + display: grid; + grid-template-columns: 1fr 1fr; + border-bottom: 2px solid var(--border-disabled); + } + &__radio { + display: flex; + align-items: center; + padding: 7px 8px; + border: 1px solid; + border-color: var(--border-normal); + border-radius: 4px; + margin: 8px; + font-size: 1.4rem; + + &-input { + display: none; + } + &-circle { + border: 2px solid var(--text-general); + border-radius: 50%; + box-shadow: 0 0 1px 0 var(--shadow-menu); + width: 16px; + height: 16px; + transition: all 0.3s ease-in-out; + margin-right: 8px; + align-self: center; + + &--selected { + border-width: 4px; + border-color: var(--brand-red-coral); + background: $color-white; + } + } + &--selected { + border-color: var(--brand-secondary); + font-weight: bold; + } + } + &__custom { + padding: 16px; + + &-radio { + display: inline-flex; + } + &-date-range { + margin: 8px; + display: flex; + flex-direction: column; + + &-start-date { + margin: 16px 0px; + } + &-end-date { + margin-bottom: 8px; + } + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-audit.scss b/packages/reports/src/sass/app/_common/components/contract-audit.scss new file mode 100644 index 000000000000..c2499691fbb3 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-audit.scss @@ -0,0 +1,101 @@ +/** @define contract-audit; */ +$HEIGHT: calc(100vh - 34em); + +.contract-audit { + &__wrapper { + border-radius: $BORDER_RADIUS; + opacity: 1; + flex: 1; + overflow: hidden; + background-color: var(--general-main-1); + color: var(--text-prominent); + display: flex; + margin-bottom: 0.8rem; + + /* postcss-bem-linter: ignore */ + .dc-tabs { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + /* postcss-bem-linter: ignore */ + &__content { + display: flex; + height: calc(100% - 4rem); + /* postcss-bem-linter: ignore */ + .dc-themed-scrollbars { + flex: 1; + } + } + } + /* postcss-bem-linter: ignore */ + .dc-themed-scrollbars { + width: 100%; + } + } + &__grid { + display: flex; + padding: 0.8rem 0; + + &:not(:first-child) { + border-top: 1px solid var(--general-section-2); + } + } + &__icon { + display: flex; + align-items: flex-start; + margin-right: 8px; + } + &__item { + display: flex; + flex-direction: column; + margin: 0 4px; + } + &__label { + white-space: nowrap; + max-width: 90px; + } + &__value-wrapper { + display: flex; + flex-direction: column; + } + &__value, + &__value2 { + padding-top: 2px; + } + &__timestamp { + align-self: center; + margin-left: auto; + + &-value { + display: block; + } + } + &__empty { + display: flex; + height: 100%; + width: 100%; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 16px; + + &-header { + font-size: 1.4rem; + font-weight: bold; + text-align: center; + color: var(--text-less-prominent); + padding: 8px; + } + } + &__tabs { + &-content { + padding: 0.8rem 1.6rem; + flex: 1; + + @include mobile { + overflow-y: auto; + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-replay.scss b/packages/reports/src/sass/app/_common/components/contract-replay.scss new file mode 100644 index 000000000000..1680947557a9 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-replay.scss @@ -0,0 +1,24 @@ +.dc-vertical-tab { + &__action-bar { + width: 100%; + display: flex; + flex-flow: row-reverse nowrap; + justify-content: space-between; + align-items: center; + padding: 16px; + + &--icon { + @include colorIcon(var(--text-general)); + } + &-wrapper { + cursor: pointer; + padding: 4px 4px 2px; + border-radius: $BORDER_RADIUS; + margin-left: auto; + + &:hover { + background: var(--general-hover); + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-type-dialog.scss b/packages/reports/src/sass/app/_common/components/contract-type-dialog.scss new file mode 100644 index 000000000000..2215f46d3289 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-type-dialog.scss @@ -0,0 +1,101 @@ +/** @define contract-type-dialog; */ +.contract-type-dialog { + right: 0.4em; + width: 100%; + pointer-events: none; + position: absolute; + transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.3s; + user-select: none; + opacity: 0; + height: 0; + z-index: 999; + + &__wrapper { + position: absolute; + width: 290px; + right: calc(100% + 10px); + top: -66px; + display: grid; + grid-auto-columns: 1fr; + z-index: 2; + border-radius: 4px; + background: var(--general-main-2); + box-shadow: 0 2px 8px 0 var(--shadow-menu); + min-width: 560px; + height: 680px; + max-height: calc(100vh - 100px); + overflow: hidden; + + /* postcss-bem-linter: ignore */ + .dc-vertical-tab { + max-height: 680px; + } + /* postcss-bem-linter: ignore */ + .dc-vertical-tab__action-bar { + flex-flow: row; + border-bottom: 2px solid var(--general-section-1); + height: 56px; + width: 320px; + + /* postcss-bem-linter: ignore */ + .dc-input { + margin-bottom: 0; + + /* postcss-bem-linter: ignore */ + &__field { + padding: 0.8rem 1.2rem 0.8rem 3.6rem; + height: 3.2rem; + + /* postcss-bem-linter: ignore */ + &::-moz-placeholder { + line-height: 16px; + } + } + /* postcss-bem-linter: ignore */ + &__leading-icon { + top: 0.8rem; + } + /* postcss-bem-linter: ignore */ + &__trailing-icon { + cursor: pointer; + } + } + } + /* postcss-bem-linter: ignore */ + .dc-vertical-tab__content { + padding: 0 !important; + + /* postcss-bem-linter: ignore */ + &-container { + max-height: calc(100% - 56px); + height: calc(100% - 56px); + } + } + /* postcss-bem-linter: ignore */ + .dc-vertical-tab__tab { + min-width: 240px; + + /* postcss-bem-linter: ignore */ + .dc-vertical-tab__header-title { + height: 56px; + } + } + /* postcss-bem-linter: ignore */ + .contract-type-info__wrapper { + height: calc(100% - 2.4rem - 18px); + } + } + &--enter, + &--exit { + transform: translate3d(20%, 0, 0); + pointer-events: none; + opacity: 0; + height: 0; + } + &--enterDone { + transform: translate3d(0, 0, 0); + max-height: 100%; + opacity: 1; + pointer-events: auto; + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-type-info.scss b/packages/reports/src/sass/app/_common/components/contract-type-info.scss new file mode 100644 index 000000000000..ed853e69740a --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-type-info.scss @@ -0,0 +1,110 @@ +/** @define contract-type-info; */ +.contract-type-info { + overflow: hidden; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + + & .dc-carousel { + width: 100%; + } + &__card { + min-height: calc(100vh - 210px); + max-height: calc(100vh - 268px); + display: flex; + flex-direction: column; + padding: 0.4rem 1rem; + @media screen and (min-height: 768px) and (max-height: 820px) { + min-height: calc(100vh - 288px); + } + @media screen and (min-height: 821px) { + min-height: 535px; + } + } + &__content { + overflow: hidden; + + /* postcss-bem-linter: ignore */ + h2 { + @include typeface(--paragraph-left-bold-black, none); + margin-bottom: 0.8rem; + color: var(--text-prominent); + } + /* postcss-bem-linter: ignore */ + p, + ul li { + @include typeface(--paragraph-left-normal-black, none); + margin-bottom: 1.6rem; + color: var(--text-general); + } + /* postcss-bem-linter: ignore */ + ul { + margin-left: 1.6rem; + list-style: disc; + + /* postcss-bem-linter: ignore */ + li { + margin-bottom: 0.8rem; + } + } + } + &__gif { + width: 100%; + height: 148px; + border-radius: 4px; + } + &__action-bar { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } + &__title { + margin: auto; + cursor: default; + } + &__nav { + display: flex; + align-items: center; + width: 320px; + padding: 0 2.4rem; + overflow: hidden; + margin-bottom: 2.4rem; + + &-list { + display: inline-block; + text-align: center; + margin: 0 auto; + position: relative; + } + &-item { + width: 0.8rem; + height: 0.8rem; + border-radius: 50%; + display: inline-block; + margin: 0 0.8rem; + cursor: pointer; + background-color: var(--text-disabled); + + &--active { + position: absolute; + top: 0; + left: 0; + cursor: initial; + transition: transform 0.3s linear; + background-color: var(--brand-red-coral); + } + } + } + &__icon { + cursor: pointer; + } + &__scrollbars { + margin-top: 2.4rem; + margin-bottom: auto; + } + &__button { + margin-top: 2.4rem; + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-type-list.scss b/packages/reports/src/sass/app/_common/components/contract-type-list.scss new file mode 100644 index 000000000000..d31350ae6c0b --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-type-list.scss @@ -0,0 +1,94 @@ +/** @define contract-type-list; */ +.contract-type-list { + padding: 1.6rem; + + &__label { + @include typeface(--paragraph-left-bold-black); + color: var(--text-prominent); + margin-bottom: 0.8rem; + margin-left: 1.6rem; + } + &:last-child { + border-bottom: none; + } + &__contracts-wrapper { + margin-bottom: 1.4rem; + } + @include mobile { + border-bottom: 4px solid var(--border-disabled); + } +} + +/** @define contract-type-item */ +.contract-type-item { + width: 100%; + cursor: pointer; + display: inline-flex; + flex-direction: row; + align-items: center; + padding: 0.8rem 1.6rem; + box-sizing: border-box; + transition: transform 0.3s opacity 0.3s; + color: var(--text-general); + border-radius: 4px; + + &:hover { + background-color: var(--state-hover); + + .contract-type-item__icon { + display: block; + } + } + &__title { + padding-left: 1.6rem; + } + &__icon { + display: none; + margin-left: auto; + + /* postcss-bem-linter: ignore */ + .dc-icon { + pointer-events: none; + } + &:hover { + /* postcss-bem-linter: ignore */ + .dc-icon { + pointer-events: none; + } + } + } + &--selected { + background: var(--state-active); + color: var(--text-prominent); + + &:hover { + background: var(--state-active); + } + .contract-type-item__title { + font-weight: bold; + color: var(--text-prominent); + } + .contract-type-item__icon { + /* postcss-bem-linter: ignore */ + .dc-icon { + pointer-events: none; + } + &:hover { + /* postcss-bem-linter: ignore */ + .dc-icon { + pointer-events: none; + } + } + } + } + &__icon-wrapper { + display: inline-flex; + align-items: center; + justify-content: center; + + /* postcss-bem-linter: ignore */ + .category-wrapper + .category-wrapper { + margin-left: 0.4rem; + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-type-no-result.scss b/packages/reports/src/sass/app/_common/components/contract-type-no-result.scss new file mode 100644 index 000000000000..111e4fa1d1b6 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-type-no-result.scss @@ -0,0 +1,24 @@ +/** @define no-results-found; weak */ +.no-results-found { + display: flex; + flex-direction: column; + padding: 2.4rem 0.8rem; + justify-content: center; + align-content: center; + text-align: center; + + &__title { + font-size: var(--text-size-xs); + font-weight: normal; + font-style: normal; + line-height: 1.43; + letter-spacing: normal; + text-align: center; + margin-bottom: 0.8rem; + color: var(--text-general); + } + &__subtitle { + letter-spacing: normal; + margin: 0; + } +} diff --git a/packages/reports/src/sass/app/_common/components/contract-type-widget.scss b/packages/reports/src/sass/app/_common/components/contract-type-widget.scss new file mode 100644 index 000000000000..71fcea8eed8c --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/contract-type-widget.scss @@ -0,0 +1,107 @@ +/** @define contract-type-widget; weak */ +.contract-type-widget { + position: relative; + z-index: 2; + border-radius: 4px; + transition: transform 0.3s, opacity 0.3s; + color: var(--text-prominent); + background: var(--fill-normal); + border: 1px solid var(--fill-normal); + + &__display { + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-start; + flex-direction: row; + padding: 1.6rem; + + /* postcss-bem-linter: ignore */ + span[name='contract_type'] { + @include typeface(--paragraph-left-normal-black); + color: var(--text-prominent); + font-weight: bold; + vertical-align: middle; + padding-left: 0.4em; + } + &:hover, + &:focus { + border-color: var(--border-hover); + outline: none; + } + &--clicked { + border-color: var(--border-active); + + .contract-type-widget__select-arrow { + transform: rotate(-90deg); + } + } + } + &__select-arrow { + display: inline-block; + font-style: normal; + vertical-align: baseline; + text-align: center; + text-transform: none; + text-rendering: optimizeLegibility; + position: absolute; + pointer-events: none; + top: 37%; + left: 0.6em; + line-height: 1; + transition: transform 0.25s ease; + transform: rotate(90deg); + transform-origin: 45% 45; + color: var(--text-prominent); + @extend %inline-icon; + } + &__icon-wrapper { + display: inline-flex; + align-items: center; + justify-content: center; + padding-left: 1.2em; + + /* postcss-bem-linter: ignore */ + .category-wrapper { + margin: 0 0.2em; + background-color: var(--general-section-1); + + /* postcss-bem-linter: ignore */ + .category-type { + /* postcss-bem-linter: ignore */ + .transparent { + fill: var(--general-section-1); + } + /* postcss-bem-linter: ignore */ + .color1-fill { + fill: var(--brand-red-coral); + } + /* postcss-bem-linter: ignore */ + .color2-fill { + fill: var(--brand-secondary); + } + } + } + } + &:hover { + border-color: var(--border-hover); + } + &:active, + &:focus, + &--show { + outline: none; + border-radius: 4px; + border-color: var(--border-active); + } + @include mobile { + margin-bottom: 0.8rem; + height: 4rem; + + &--multiplier { + margin-bottom: 0.6rem; + } + .contract-type-widget__display { + padding: 0.8rem; + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/market-is-closed-overlay.scss b/packages/reports/src/sass/app/_common/components/market-is-closed-overlay.scss new file mode 100644 index 000000000000..64a68ba5ea8d --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/market-is-closed-overlay.scss @@ -0,0 +1,54 @@ +/** @define .market-is-closed-overlay */ +.market-is-closed-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0; + background-color: var(--overlay-inside-dialog); + z-index: 1000; + + &--loading { + display: none; + } + &__main-heading { + margin-bottom: 2rem; + } + &__open-date, + &__main-message { + margin-bottom: 2rem; + } + &__open-at, + &__come-back { + &--main-page { + margin-bottom: 1rem; + } + } + &__separator { + margin-top: 1rem; + border: 2px solid var(--general-section-1); + width: 19.2rem; + margin-bottom: 2rem; + } + &__button { + height: 40px; + margin-top: 1rem; + } + + @include mobile { + position: unset; + top: unset; + left: unset; + overflow-y: scroll; + padding-top: 16rem; + + &__button { + min-height: 4rem; + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss b/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss new file mode 100644 index 000000000000..c1b39d42b87b --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss @@ -0,0 +1,38 @@ +/** @define .market-symbol-icon; weak */ +.market-symbol-icon { + display: flex; + justify-content: flex-start; + width: 100%; + + .color1-fill { + fill: var(--brand-red-coral); + } + .color2-fill { + fill: var(--brand-secondary); + } + &-name, + &-category { + display: flex; + justify-content: flex-start; + align-items: center; + @include typeface(--paragraph-left-bold-active); + color: var(--text-prominent); + } + &-name { + width: 3.2rem; + margin-right: 0.8rem; + } + &-category { + svg { + width: 2.4rem; + height: 2.4rem; + } + } + &__multiplier { + color: var(--text-less-prominent); + font-size: 1rem; + display: flex; + align-items: flex-end; + margin: 0 0 0.4rem 0.4rem; + } +} diff --git a/packages/reports/src/sass/app/_common/components/message-box.scss b/packages/reports/src/sass/app/_common/components/message-box.scss new file mode 100644 index 000000000000..fc66f8215952 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/message-box.scss @@ -0,0 +1,103 @@ +/** @define message-box */ +.message-box { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 2; + border-radius: $BORDER_RADIUS; + display: flex; + align-items: center; + background-color: var(--general-main-2); + color: var(--text-prominent); + + &__close-btn { + position: absolute; + cursor: pointer; + right: 2px; + top: 2px; + + &-ic { + width: 24px; + height: 24px; + } + } + &__result { + padding: 16px; + line-height: 1.5; + font-size: 0.8em; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + max-height: 187px; + height: 100%; + width: 100%; + color: var(--text-prominent); + + &-header { + margin-bottom: 0.5rem; + font-size: 12px; + } + &-label { + margin-right: 4px; + font-size: 10px; + color: var(--text-prominent); + } + &-currency { + position: relative; + display: inline-flex; + } + } + &__login { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + color: var(--text-prominent); + + &-btn { + margin: 0 auto; + + &-span { + font-size: 0.8em; + } + } + &-info { + font-weight: 300; + font-size: 1.2em; + } + &-prompt { + line-height: 100%; + margin-bottom: 0; + font-size: 0.8em; + font-weight: 300; + } + &-link { + text-decoration: none; + color: var(--text-prominent); + + &-info { + padding: 5px 10px 10px; + font-weight: 500; + color: var(--status-info); + } + &:hover, + &:focus, + &:active { + text-decoration: underline; + color: var(--text-prominent); + } + } + } + &__info { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1.5rem; + text-align: center; + } +} diff --git a/packages/reports/src/sass/app/_common/components/number-selector.scss b/packages/reports/src/sass/app/_common/components/number-selector.scss new file mode 100644 index 000000000000..82a220fbf76f --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/number-selector.scss @@ -0,0 +1,56 @@ +/** @define number-selector */ +.number-selector { + margin-top: 1.2em; + text-align: center; + + &__row:first-child { + margin-bottom: 0.8em; + } + &__selection { + width: calc(20% - 4px); + height: 24px; + border-radius: $BORDER_RADIUS; + display: inline-flex; + justify-content: center; + align-items: center; + @include typeface(--paragraph-left-normal-black); + background-color: var(--state-normal); + color: var(--text-general); + + &:not(:first-child) { + margin-left: 5px; + } + &--selected { + @include typeface(--paragraph-left-bold-active); + background-color: var(--state-active); + color: var(--text-prominent); + } + &:hover:not(&--selected) { + cursor: pointer; + background-color: var(--state-hover); + } + &:last-child { + margin-right: 0; + } + } + @include mobile { + margin-top: 0; + padding: 0.8rem; + border-radius: $BORDER_RADIUS; + background-color: var(--fill-normal); + + &__selection { + width: calc(20% - 7px); + height: 32px; + font-size: 1.6rem; + background-color: var(--general-section-1); + + &--selected { + background-color: var(--state-active); + } + &:not(:first-child) { + margin-left: 0.8rem; + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/popconfirm.scss b/packages/reports/src/sass/app/_common/components/popconfirm.scss new file mode 100644 index 000000000000..fd43c5da1218 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/popconfirm.scss @@ -0,0 +1,201 @@ +/** @define popconfirm */ +//.popconfirm { +// position: absolute; +// left: 50%; +// bottom: 4em; +// box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.14); +// width: 348px; +// min-height: 9em; +// font-weight: 500; +// border-radius: 4px; +// transform: scale(0, 0) translate(-50%, 0); +// opacity: 0; +// transform-origin: left bottom; +// transition: all 0.2s; +// z-index: 3; +// @include themify($themes) { +// background: themed('container_secondary_color'); +// } +// +// &--open { +// transform: scale(1, 1) translate(-50%, 0); +// opacity: 1; +// } +// &__button { +// margin: 0 0.8em; +// box-shadow: none; +// text-decoration: none; +// display: flex; +// align-items: center; +// justify-content: center; +// transition: background 0.2s, color 0.2s; +// @include themify($themes) { +// background: themed('container_secondary_color'); +// } +// +// &-wrapper { +// display: flex; +// justify-content: flex-end; +// } +// &-text { +// @include themify($themes) { +// color: themed('text_primary_color'); +// } +// font-size: 1.2em; +// font-weight: bold; +// } +// &:hover { +// @include themify($themes) { +// background: themed('tab_hover_color'); +// +// .popconfirm__button-text { +// color: themed('text_color'); +// } +// } +// } +// } +// &__title { +// margin: 0.8em; +// padding: 0.8em; +// display: flex; +// flex-direction: row; +// align-items: center; +// +// } +// &__icon_exclamation { +// margin-right: 0.8em; +// } +// &__header { +// font-size: 1.2em; +// font-weight: 300; +// text-transform: none; +// padding: 0; +// margin: 0; +// line-height: 100%; +// @include themify($themes) { +// color: themed('text_color'); +// } +// } +// &:after { +// // arrow pointing down +// position: absolute; +// content: ''; +// bottom: -0.3em; +// left: 50%; +// margin-left: -0.5em; +// border-width: 5px; +// border-style: solid; +// transform: rotate(45deg); +// box-shadow: rgba(0, 0, 0, 0.05) 2px 2px 2px; +// @include themify($themes) { +// border-color: themed('container_secondary_color'); +// } +// } +// // Alignments +// &--bottom { +// top: 4em; +// bottom: unset; +// transform-origin: left top; +// +// &:after { +// top: -0.3em; +// bottom: unset; +// transform: rotate(-135deg); +// } +// } +// &--right { +// bottom: 50%; +// left: unset; +// right: -2em; +// transform-origin: right bottom; +// transform: scale(0, 0) translate(100%, 50%); +// +// &--open { +// transform: scale(1, 1) translate(100%, 50%); +// } +// &:after { +// left: 0; +// bottom: 50%; +// transform: rotate(135deg); +// } +// } +// &--left { +// bottom: 50%; +// left: -2em; +// transform-origin: left bottom; +// transform: scale(0, 0) translate(-100%, 50%); +// +// &--open { +// transform: scale(1, 1) translate(-100%, 50%); +// } +// &:after { +// left: unset; +// right: -0.3em; +// bottom: 45%; +// transform: rotate(-45deg); +// } +// } +// &--top-right { +// transform-origin: right bottom; +// transform: scale(0, 0) translate(0, 0); +// left: unset; +// right: 0; +// +// &--open { +// transform: scale(1, 1) translate(0, 0); +// } +// &:after { +// left: unset; +// right: 5%; +// } +// } +// &--top-left { +// transform-origin: left bottom; +// transform: scale(0, 0) translate(0, 0); +// left: 0; +// +// &--open { +// transform: scale(1, 1) translate(0, 0); +// } +// &:after { +// left: 8%; +// } +// } +// &--bottom-right { +// top: 4em; +// bottom: unset; +// transform-origin: right top; +// transform: scale(0, 0) translate(0, 0); +// left: unset; +// right: 0; +// +// &--open { +// transform: scale(1, 1) translate(0, 0); +// } +// &:after { +// top: -0.3em; +// bottom: unset; +// left: unset; +// right: 5%; +// transform: rotate(-135deg); +// +// } +// } +// &--bottom-left { +// top: 4em; +// bottom: unset; +// transform-origin: left top; +// transform: scale(0, 0) translate(0, 0); +// left: 0; +// +// &--open { +// transform: scale(1, 1) translate(0, 0); +// } +// &:after { +// top: -0.3em; +// left: 8%; +// bottom: unset; +// transform: rotate(-135deg); +// } +// } +//} diff --git a/packages/reports/src/sass/app/_common/components/positions-toggle.scss b/packages/reports/src/sass/app/_common/components/positions-toggle.scss new file mode 100644 index 000000000000..e82b9ef9c3c9 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/positions-toggle.scss @@ -0,0 +1,115 @@ +/** @define positions-toggle; weak */ +.positions-toggle { + position: relative; + text-decoration: none; + height: 100%; + display: inline-flex; + align-items: center; + padding: 0 0.8rem; + + &__icon { + margin-bottom: 0.5em; + height: 1.6rem; + width: 1.6rem; + } + &:before { + content: ''; + width: 0.2rem; + height: 0.2rem; + border-radius: 50%; + position: absolute; + bottom: 0.8rem; + right: 1.5rem; + display: block; + transition: transform 0.25s ease; + transform: translate3d(0, 0, 0); + background-color: var(--text-prominent); + } + &--active { + background: none !important; + + &:before { + transform: translate3d(0, 50px, 0); + } + .positions-toggle__icon { + margin-bottom: 0; + } + @include desktop { + &:after { + transform: translate3d(0, 4px, 0); + } + } + } + &--has-count:after { + content: attr(data-count); + width: 14px; + height: 14px; + font-size: 0.9rem; + border-radius: 50%; + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 2px; + right: 0; + background-color: var(--brand-red-coral); + color: $color-white; + } + @include mobile { + padding: 0 1.2rem; + height: inherit; + + &--has-count:after { + top: -4px; + right: 4px; + } + .positions-toggle__icon { + margin: 0; + } + &:before { + display: none; + } + } +} + +/** @define positions-modal; weak */ +.positions-modal { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + + &__body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + } + &__header { + display: flex; + justify-content: space-between; + padding: 1.6rem; + height: 50px; + } + &__title { + display: flex; + align-items: center; + font-size: 1rem; + color: var(--text-general); + line-height: 1.5; + + &-icon { + width: 1.2rem; + height: 1.2rem; + margin-right: 0.4rem; + } + } + &__footer { + height: 72px; + width: 100%; + + &-btn { + margin: 1.6rem 0.8rem; + width: calc(100% - 1.6rem); + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/purchase-button.scss b/packages/reports/src/sass/app/_common/components/purchase-button.scss new file mode 100644 index 000000000000..177b2ae04363 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/purchase-button.scss @@ -0,0 +1,327 @@ +/** @define btn-purchase */ +.btn-purchase { + position: relative; + background-color: var(--purchase-section-1); + overflow: hidden; + height: 56px; + width: 100%; + margin: 5px 0 0; + padding: 0; + text-align: left; + transition: transform 0.25s ease; + display: flex; + z-index: 1; + touch-action: manipulation; + cursor: pointer; + white-space: nowrap; + border: 0; + border-radius: $BORDER_RADIUS; + outline: 0; + font-size: 1rem; + + @include mobile { + flex-direction: column; + justify-content: space-between; + align-items: center; + margin: initial; + height: 70px; + + &__top, + &__bottom { + height: 50%; + flex: 1 1 auto; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + } + &__top { + width: 50%; + } + &__bottom { + width: 40vw; // TODO remove this when find a solution for fill-width inside flex. + } + &:hover { + transform: none !important; + } + &--disabled, + &[disabled] { + opacity: 0.32; + } + &--swoosh { + transform: none !important; + } + &__error { + position: absolute; + padding: 0.8rem; + width: 100%; + height: 100%; + color: var(--text-colored-background); + background-color: var(--status-danger); + justify-content: center; + align-items: center; + line-height: 1.4; + z-index: 2; + border-radius: $BORDER_RADIUS; + max-height: 90px; + overflow: hidden; + font-weight: bold; + } + } + + &__icon_wrapper { + @extend %inline-icon.white; + /* postcss-bem-linter: ignore */ + .color1-fill { + fill: var(--text-colored-background); + } + /* postcss-bem-linter: ignore */ + .color2-fill { + fill: var(--text-colored-background); + } + @include mobile { + margin-right: 0.5rem; + } + } + &__info { + color: var(--text-colored-background); + text-align: left; + position: relative; + display: flex; + flex-grow: 1; + align-items: center; + padding-left: 10px; + height: 56px; + } + &__info--left { + background-color: var(--purchase-main-1); + width: 40%; + transition: transform linear 0.25s; + z-index: 2; + + .btn-purchase__text_wrapper { + margin-left: 24px; + position: absolute; + } + } + &__info--right { + width: 45%; + padding-right: 1rem; + background-color: transparent; + display: flex; + justify-content: flex-end; + + .btn-purchase__text { + opacity: 1; + transition: 0.3s; + } + } + &__effect-detail { + position: absolute; + background-color: var(--purchase-main-1); + transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1); + width: 116px; + height: 100%; + z-index: 1; + } + &__effect-detail--arrow { + content: ''; + width: 55px; + transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1); + transform: rotate(45deg); + height: 100%; + background: var(--purchase-main-1); + left: 80px; + position: absolute; + top: 0; + bottom: 0; + display: inline-block; + } + &[disabled]:hover { + cursor: initial; + transform: none; + } + &:hover:not(&--disabled):not([disabled]) { + transform: translate3d(0, -4px, 0); + } + &__type-wrapper { + display: flex; + align-items: center; + transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1); + transform: none; + } + &__shadow-wrapper { + position: relative; + + .btn-purchase__box-shadow { + position: absolute; + width: 100%; + bottom: 14px; + height: 50%; + z-index: 0; + opacity: 0.9; + box-shadow: 0 16px 14px 0 var(--shadow-menu); + transition: opacity 0.25s linear; + pointer-events: none; + } + &--disabled { + pointer-events: none; + + &:hover { + transform: none; + } + } + } + &:active:not(&--disabled):not([disabled]), + &:focus:not(&--disabled):not([disabled]) { + transform: translate3d(0, -4px, 0); + } + &--swoosh { + transform: translate3d(0, -4px, 0); + + .btn-purchase__type-wrapper { + transform: translate3d(55px, 0, 0); + } + .btn-purchase__effect-detail { + transform: scale3d(3, 1, 1); + + &--arrow { + transform: translate3d(120px, 0, 0) rotate(45deg); + } + } + } + &--1 { + background-color: var(--purchase-section-1); + + .btn-purchase__info--left { + background-color: var(--purchase-main-1); + } + .btn-purchase__effect-detail { + background: var(--purchase-main-1); + + &--arrow { + background: var(--purchase-main-1); + } + } + + @include mobile { + background: var(--purchase-section-1); + background: linear-gradient( + 0deg, + var(--purchase-section-1) 0%, + var(--purchase-section-1) 50%, + var(--purchase-main-1) 50%, + var(--purchase-main-1) 100% + ); + } + } + &--2 { + background-color: var(--purchase-section-2); + + .btn-purchase__info--left { + background-color: var(--purchase-main-2); + } + .btn-purchase__effect-detail { + background: var(--purchase-main-2); + + &--arrow { + background: var(--purchase-main-2); + } + } + @include mobile { + background: var(--purchase-section-2); + background: linear-gradient( + 0deg, + var(--purchase-section-2) 0%, + var(--purchase-section-2) 50%, + var(--purchase-main-2) 50%, + var(--purchase-main-2) 100% + ); + } + } + &--disabled, + &[disabled] { + cursor: default; + + &:after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.32); + z-index: 4; + pointer-events: none; + } + } + &--animated--slide { + .btn-purchase__icon_wrapper, + .btn-purchase__text_wrapper { + @extend .loader; + } + .btn-purchase__icon, + .btn-purchase__text { + @extend .loader--loading; + } + .btn-purchase__icon_wrapper { + width: 16px; + height: 16px; + } + .btn-purchase__text_wrapper { + width: 56px; + height: 8px; + + .btn-purchase__text { + display: block; + } + } + } + &--animated--fade &__info--right &__text { + opacity: 0; + } + &__shadow-wrapper:hover:after { + opacity: 0; + } + &--multiplier { + .btn-purchase__info--left { + width: 35%; + flex: none; + } + .btn-purchase__effect-detail--arrow { + left: 40px; + } + &.btn-purchase--swoosh { + .btn-purchase { + &__effect-detail { + transform: scale3d(5, 1, 1); + + &--arrow { + transform: translate3d(160px, 0, 0) rotate(45deg); + } + } + &__type-wrapper { + transform: translate3d(70px, 0, 0); + } + } + } + .btn-purchase__effect-detail { + width: 72px; + } + .btn-purchase__top { + @include mobile { + justify-content: center; + } + } + .trade-container__price-info { + @include mobile { + justify-content: center; + } + } + @include mobile { + &-deal-cancel { + height: 64px; + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/components/range-slider.scss b/packages/reports/src/sass/app/_common/components/range-slider.scss new file mode 100644 index 000000000000..6c1cff37e173 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/range-slider.scss @@ -0,0 +1,183 @@ +// Range Slider +/** @define range-slider */ +.range-slider { + margin-top: 8px; + margin-bottom: 8px; + padding: 0 4px; + box-sizing: border-box; + + &__label { + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin-top: 16px; + margin-bottom: 4px; + } + /* overriding firefox shadow pseudo element */ + &__track[type='range']::-moz-focus-outer { + border: 0; + } + // TODO: remove once we have proper styling for input slider validation error + &__error { + .range-slider { + &__track[type='range'] { + // Range Handle Thumb - Chrome / Webkit based browsers + &::-webkit-slider-thumb { + background: var(--text-loss-danger); + } + // Range Handle Thumb - Firefox / Gecko based browsers + &::-moz-range-thumb { + background: var(--text-loss-danger); + } + } + &__line { + background: var(--text-loss-danger) !important; + } + &__ticks-step--marked { + background: var(--text-loss-danger) !important; + border-color: var(--text-loss-danger) !important; + } + } + } + &__track[type='range'] { + position: relative; + // removal of !important is pending refactor in main trading sass file that overrides rules for input els + appearance: none !important; + width: 100% !important; + height: 2px; // !important had to be removed to fix disappearing thumb on MS Edge + border-radius: 5px; + outline: none; + padding: 0; + margin: 0; + cursor: pointer; + background: var(--state-normal); + border: none; + + // Handles Track - MS Edge and IE + @supports (-ms-ime-align: auto) { + height: 14px; + position: relative; + top: -6px; + } + // Range Handle Thumb - Chrome / Webkit based browsers + &::-webkit-slider-thumb { + @include thumbStyle(); + background: var(--state-active); + } + // Range Handle Thumb - Firefox / Gecko based browsers + &::-moz-range-thumb { + @include thumbStyle(); + background: var(--state-active); + } + // Range Handle Thumb - Microsoft Edge + &::-ms-thumb { + @include thumbStyle(); + background: var(--state-active); + } + &:hover, + &:focus, + &:active { + border: 0; + outline: none; + } + &:active, + &:focus { + box-shadow: none !important; + } + &::-ms-tooltip { + display: none; + } + &[type='range']::-ms-track { + /*example */ + width: 100%; + height: 2px; + border-width: 6px 0; + background: var(--state-active); + border-color: var(--general-section-1); + color: var(--state-active); + } + &[type='range']::-ms-fill-upper { + height: 2px; + background: var(--state-active); + } + &[type='range']::-ms-fill-lower { + height: 2px; + background: var(--state-active); + } + } + &__ticks { + width: 100%; + position: absolute; + display: flex; + justify-content: space-between; + left: 0; + top: -3px; + height: 0; + + &-step { + height: 8px; + width: 8px; + border-radius: 50%; + transition: box-shadow 0.2s; + cursor: pointer; + border: 4px solid var(--state-normal); + background: var(--state-normal); + + &--active, + &--marked { + background: var(--state-active); + border-color: var(--state-active); + } + &--marked-hover { + background: var(--state-hover); + border-color: var(--state-hover); + } + &--marked:hover:not(&--active) { + border-color: var(--state-active) !important; + } + &--active { + visibility: hidden; + pointer-events: none; + + &:after { + content: ''; + width: 1em; + height: 1em; + position: absolute; + display: block; + top: 0; + border-radius: 50%; + background: var(--state-active); + } + } + &:hover:not(&--active) { + background: var(--state-hover); + border-color: var(--state-hover); + box-shadow: 0 0 0 5px var(--shadow-menu); + } + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + } + } + &__line { + position: absolute; + top: 0; + left: 0; + height: 2px; + pointer-events: none; + background: var(--state-active); + + &--fill { + background: var(--state-hover); + } + } + &__caption { + text-align: center; + padding-top: 8px; + } +} diff --git a/packages/reports/src/sass/app/_common/components/toggle-button.scss b/packages/reports/src/sass/app/_common/components/toggle-button.scss new file mode 100644 index 000000000000..85dbaf9b6f40 --- /dev/null +++ b/packages/reports/src/sass/app/_common/components/toggle-button.scss @@ -0,0 +1,28 @@ +/** @define toggle-button */ +.toggle-button { + border-radius: 4px; + background-color: var(--state-normal); + color: var(--text-general); + height: 3.2em; + margin: 0; + margin-top: 0.8em; + + &:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + &:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + &--selected { + background-color: var(--state-active); + color: var(--text-prominent); + } +} + +/** @define toggle-button-group */ +.toggle-button-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(4.4em, 1fr)); +} diff --git a/packages/reports/src/sass/app/_common/drawer/contract-drawer.scss b/packages/reports/src/sass/app/_common/drawer/contract-drawer.scss new file mode 100644 index 000000000000..2d93785b3cef --- /dev/null +++ b/packages/reports/src/sass/app/_common/drawer/contract-drawer.scss @@ -0,0 +1,131 @@ +$header-height: 4em; + +.contract-drawer { + width: $POSITIONS_DRAWER_WIDTH; + // removing 2px below for borders + height: calc(100vh - #{$HEADER_HEIGHT} - #{$FOOTER_HEIGHT} - 2px - (#{$POSITIONS_DRAWER_MARGIN} * 4)); + z-index: 2; + padding: 0 0.8rem; + box-sizing: border-box; + will-change: transform, opacity; + transition: opacity 0.3s ease, transform 0.3s ease; + border-radius: $BORDER_RADIUS; + border: 1px solid var(--general-section-1); + background: var(--general-section-1); + color: var(--text-prominent); + + svg { + @extend %inline-icon; + } + h2 { + margin-left: 8px; + text-transform: none; + color: var(--text-prominent); + } + .currency-badge { + margin-bottom: 5px; + } + &--contract-mode { + &:before { + background: var(--general-section-1); + content: ''; + position: absolute; + top: 38px; + left: -3px; + width: calc(100% + 6px); + height: calc(100% - 32px); + filter: blur(3px); + } + } + &__heading { + display: flex; + justify-content: flex-start; + align-items: center; + padding: 0.5em; + margin-left: -8px; + @include typeface(--title-left-bold-black); + color: var(--text-general); + + &-btn { + padding: 4px 8px 0; + margin-bottom: -4px; + cursor: pointer; + border-radius: $BORDER_RADIUS; + background: var(--general-section-1); + + &:hover { + background: var(--general-hover); + } + } + } + &__icon { + margin-right: 16px; + } + &__body { + display: flex; + flex-direction: column; + + @include desktop { + height: 100%; + } + } +} + +/** @define .contract-card; weak */ +.contract-card { + &__market-closed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 0; + background-image: var(--contract-gradient-danger); + z-index: 1000; + + &__title { + margin-bottom: 1rem; + } + &--loading { + display: none; + } + &--disabled { + cursor: not-allowed; + } + &--hidden { + opacity: 0; + } + } + &__footer { + margin-bottom: 0.5rem; + padding: 0 0.4rem; + + &-wrapper { + @include typeface(--small-center-normal-black); + color: var(--text-prominent); + padding: 0.8rem 0.8rem 0; + display: grid; + grid-template-columns: 1fr 1fr; + } + } + &__sell-button { + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.25s ease, opacity 0.25s linear; + border-top: 1px solid var(--general-section-1); + + .dc-btn--sell, + .dc-btn--cancel { + height: 2.4rem; + + @include mobile { + height: 3.2rem; + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/drawer/positions-drawer.scss b/packages/reports/src/sass/app/_common/drawer/positions-drawer.scss new file mode 100644 index 000000000000..eedc16f387e0 --- /dev/null +++ b/packages/reports/src/sass/app/_common/drawer/positions-drawer.scss @@ -0,0 +1,124 @@ +$header-height: 3.6em; + +// Trade page animation performance fix #perfmatters +.trade-container + .positions-drawer { + transition: opacity 0.4s ease; +} + +.positions-drawer { + $MARGIN_TOP: #{$POSITIONS_DRAWER_MARGIN * 2}; + $MARGIN_BOTTOM: #{$POSITIONS_DRAWER_MARGIN * 2}; + + width: $POSITIONS_DRAWER_WIDTH; + height: calc(100vh - #{$HEADER_HEIGHT} - #{$FOOTER_HEIGHT} - #{$MARGIN_TOP} - #{$MARGIN_BOTTOM}); + margin-top: #{$MARGIN_TOP}; + position: fixed; + z-index: 2; + top: #{$HEADER_HEIGHT}; + left: 4px; + box-sizing: border-box; + opacity: 0; + transform: translateX(-100%); + will-change: transform, opacity; + transition: opacity 0.3s ease, transform 0.3s ease; + border-radius: $BORDER_RADIUS; + border: 1px solid var(--general-section-1); + background-color: var(--general-section-1); + color: var(--text-prominent); + + &__bg { + position: absolute; + z-index: 2; + top: 0; + left: 0; + width: 260px; + height: 100%; + background: var(--general-main-1); + // box-shadow: 10px 0 5px -2px var(--general-main-1); + transition: opacity 0.25s linear; + opacity: 0; + pointer-events: none; + + &--open { + opacity: 1; + } + } + &--open { + transform: translateX(#{$POSITIONS_DRAWER_MARGIN}); + opacity: 1; + } + &__header { + height: $header-height; + display: flex; + flex-direction: row; + align-items: center; + padding: 0 1em; + + &:after { + content: ''; + position: absolute; + height: 8px; + width: calc(100% - 18px); + left: 9px; + top: 39px; + z-index: 1; + box-shadow: 0 8px 2px -2px var(--general-section-1) inset; + } + } + &__body { + height: calc(100% - #{$header-height * 2.7}); + padding: 0.8em 0 0; + box-sizing: border-box; + overflow: hidden; + align-self: center; + color: var(--text-general); + + & .dc-themed-scrollbars { + height: 100%; + } + & .dc-contract-card { + &__stop-loss { + & .dc-contract-card-item { + &__header { + padding-top: 0.8rem; + } + } + } + } + } + &__footer { + position: relative; + height: 6em; + display: flex; + justify-content: center; + align-items: center; + + &:before { + content: ''; + position: absolute; + height: 8px; + width: calc(100% - 18px); + left: 9px; + top: -6px; + box-shadow: 0 8px 2px -2px var(--general-section-1) inset; + } + .dc-btn { + width: 100%; + margin: 8px; + height: 40px; + } + } + &__icon-main { + margin-right: 0.8em; + } + &__icon-close { + display: inline-block; + margin-left: auto; + cursor: pointer; + svg { + @extend %inline-icon; + height: 1.6em; + width: 1.6em; + } + } +} diff --git a/packages/reports/src/sass/app/_common/drawer/positions-modal-card.scss b/packages/reports/src/sass/app/_common/drawer/positions-modal-card.scss new file mode 100644 index 000000000000..9c750117da33 --- /dev/null +++ b/packages/reports/src/sass/app/_common/drawer/positions-modal-card.scss @@ -0,0 +1,259 @@ +/** @define positions-modal-card; weak */ + +.positions-modal-card { + $positions-modal-card: &; + + box-sizing: border-box; + display: flex; + flex-direction: column; + text-decoration: none; + position: relative; + padding: 0.8rem; + color: var(--text-general); + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; + + &__result { + &--enter, + &--exit { + opacity: 0; + } + &--enter-done { + opacity: 1; + } + } + &__caption { + font-size: 1.6em; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + transition: opacity 0.25s ease; + cursor: pointer; + width: 100%; + + &--won { + color: var(--text-profit-success); + } + &--lost { + color: var(--text-loss-danger); + } + &-wrapper { + cursor: pointer; + width: 100%; + grid-row: 1 / -1; + align-self: center; + justify-self: center; + text-decoration: none; + } + } + &__icon { + margin-left: 4px; + } + &__wrapper { + margin: 0.8rem; + border-radius: $BORDER_RADIUS; + transition: transform 0.25s ease, opacity 0.25s linear; + position: relative; + background: var(--general-main-1); + border: 1px solid var(--border-disabled); + } + &__grid { + display: grid; + + &-header { + border-bottom: 1px solid var(--general-section-1); + grid-template-columns: 1fr 1fr 1fr; + padding-bottom: 1rem; + margin-bottom: 0.8rem; + } + &-body { + grid-template-columns: 2fr 1fr; + grid-template-rows: auto auto; + display: grid; + } + &-underlying-trade { + grid-template-columns: 1fr 1fr 1fr; + } + &-profit-payout { + grid-template-columns: 1fr 1fr; + display: grid; + grid-row: 1 / 2; + grid-column: 1 / 2; + padding: 8px 0; + border-radius: $BORDER_RADIUS; + } + &-price-payout { + display: grid; + grid-row: 2 / 3; + grid-column: 1 / 2; + grid-template-columns: 1fr 1fr; + } + } + &__progress { + justify-self: end; + align-self: center; + grid-row: 1 / 3; + grid-column: 2 / 3; + } + &__sell-button { + justify-self: end; + align-self: center; + + .dc-btn--sell { + height: 3.2rem !important; + } + } + &__purchase, + &__payout { + &-label { + text-transform: none; + white-space: nowrap; + max-width: 90px; + } + &-value { + display: flex; + justify-content: flex-start; + align-items: center; + padding: 4px 0 8px; + } + &-price { + display: flex; + flex-direction: column; + } + } + &__type { + font-size: 1.2rem; + display: flex; + justify-content: flex-start; + font-weight: bold; + + /* postcss-bem-linter: ignore */ + .category-type { + .color1-fill { + fill: var(--brand-red-coral); + } + .color2-fill { + fill: var(--brand-secondary); + } + } + } + &__symbol { + margin-left: 0.5rem; + /* iPhone SE screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 480px) { + font-size: 0.9rem; + } + } + &__indicative { + font-size: 1.2rem; + font-weight: bold; + display: flex; + justify-content: flex-start; + align-items: center; + color: var(--text-general); + + &-label { + font-size: 0.8rem; + font-weight: normal; + margin-bottom: 4px; + white-space: nowrap; + max-width: 90px; + color: var(--text-general); + } + &--movement { + margin-left: 2px; + width: 16px; + height: 16px; + + &-complete, + &-complete:after { + display: none; + } + &:after { + content: ''; + width: 16px; + } + } + } + &__remaining-time { + font-size: 1.2em; + color: var(--text-general); + } + &__timer { + margin: 0.2rem auto; + } + &__profit-loss { + font-size: 1.2rem; + text-align: center; + display: flex; + justify-content: flex-start; + align-items: center; + font-weight: bold; + + &-label { + font-size: 0.8rem; + margin-bottom: 4px; + font-weight: normal; + white-space: nowrap; + max-width: 90px; + color: var(--text-general); + /* iPhone SE screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 480px) { + font-size: 0.8rem; + } + } + &--is-crypto { + margin-left: -6px; + } + &--negative { + color: var(--text-loss-danger); + + &:before { + content: '-'; + color: inherit; + } + } + &--positive { + color: var(--text-profit-success); + + &:before { + content: '+'; + color: inherit; + } + } + } + &__underlying-name { + display: flex; + justify-content: flex-start; + align-items: center; + font-weight: bold; + font-size: 1.2em; + } + &--multiplier { + .dc-contract-card { + &__grid-items { + grid-template-columns: 1fr 1fr 1fr; + padding: 1.2rem 0rem; + } + &__deal-cancel-fee, + &__buy-price, + &__stop-loss { + order: 1; + } + } + .dc-contract-card__sell-button { + border-top: 0rem; + } + } +} + +/** @define dc-remaining-time; weak */ +.dc-remaining-time { + display: inline; + + @include mobile { + font-size: 0.9rem; + } +} diff --git a/packages/reports/src/sass/app/_common/form/time-picker.scss b/packages/reports/src/sass/app/_common/form/time-picker.scss new file mode 100644 index 000000000000..240329b5c2b6 --- /dev/null +++ b/packages/reports/src/sass/app/_common/form/time-picker.scss @@ -0,0 +1,93 @@ +/** @define time-picker */ +.time-picker { + position: relative; + margin-top: 0.8em; + + &--padding { + padding: 1.6em; + } + &__icon { + @extend %inline-icon; + position: absolute; + bottom: 0; + top: 0.8em; + left: 5%; + z-index: 1; + margin: unset; + /* postcss-bem-linter: ignore */ + g { + stroke: var(--text-general); + } + } + &__dialog { + box-shadow: 0 2px 4px 0 var(--shadow-menu); + border-radius: 4px; + padding-top: 10px; + position: absolute; + top: 0; + transform-origin: right; + transform: scale(1, 0) translate3d(0, 0, 0); + transition: transform 0.25s ease, opacity 0.25s linear; + width: 231px; + z-index: 2; + background-color: var(--general-main-2); + + &--enter, + &--exit { + opacity: 0; + transform: scale(1, 1) translate3d(-225px, 0, 0); + } + &--enter-done { + opacity: 1; + transform: translate3d(-245px, 0, 0); + } + } + &__selector { + height: inherit; + + &--hours { + border-right: 1px solid var(--general-section-1); + border-radius: 0 0 0 4px; + width: 150px; + } + &--minutes { + width: 80px; + border-radius: 0 5px 5px 0; + } + &--hours, + &--minutes { + display: inline-block; + height: 228px; + overflow: hidden; + text-align: center; + border-color: var(--general-section-1); + color: var(--text-prominent); + } + &-list-title { + @include typeface(--small-center-bold-black); + color: var(--text-prominent); + } + &-list-item { + @include typeface(--small-left-normal-black); + cursor: pointer; + border-radius: 4px; + display: inline-block; + margin: 3px; + padding: 7px; + color: var(--text-general); + + &:hover:not(&--disabled):not(&--selected) { + background: var(--state-hover); + color: var(--text-general); + } + &--selected { + background: var(--state-active); + color: var(--text-prominent) !important; + } + &--disabled { + color: var(--text-disabled); + cursor: default; + } + } + } +} diff --git a/packages/reports/src/sass/app/_common/layout/trader-layouts.scss b/packages/reports/src/sass/app/_common/layout/trader-layouts.scss new file mode 100644 index 000000000000..4e86770dac1c --- /dev/null +++ b/packages/reports/src/sass/app/_common/layout/trader-layouts.scss @@ -0,0 +1,797 @@ +/** @define app-contents; weak */ +.app-contents { + &--show-positions-drawer:not(&--is-mobile) { + .trade-container { + .chart-container { + width: 100%; + + .sc-navigation-widget, + .cq-top-ui-widgets, + .sc-toolbar-widget, + .stx-panel-control { + transform: translate3d(248px, 0, 0); + } + .cq-chart-controls { + transform: translate3d(130px, 0, 0) !important; + } + .cq-bottom-ui-widgets { + .digits__container { + transform: translate3d(130px, 0, 0) !important; + } + } + .cq-chart-control-left { + .cq-chart-controls { + transform: translate3d(248px, 0, 0) !important; + } + .cq-bottom-ui-widgets { + .digits__container { + transform: translate3d(170px, 40px, 0) !important; + } + } + } + &__loader { + .barspinner { + transform: translate3d(130px, 0, 0) !important; + } + } + } + } + } + &--is-mobile { + .top-widgets-portal { + position: absolute; + top: 0px; + width: 100%; + display: flex; + align-items: center; + flex-wrap: wrap; + z-index: 1; + + .recent-trade-info { + min-width: 8rem; + line-height: 2.4rem; + margin-left: 0.8rem; + } + } + .cq-chart-title { + > .cq-menu-btn { + padding: 0.4rem; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; + } + } + & .contract-details-wrapper + .smartcharts-undefined { + & .cq-symbols-display { + display: none; + } + } + } +} + +$FLOATING_HEADER_HEIGHT: 41px; +/** @define trade-container; weak */ +.trade-container { + position: relative; + padding: 0.8em 1.2em; + display: flex; + min-height: calc(100vh - 84px); + overflow: hidden; + + &__replay { + width: 100%; + display: flex; + flex-direction: row; + height: calc(100vh - 108px - #{$FLOATING_HEADER_HEIGHT + 12px}); + padding-bottom: 2.4rem; + + .contract-drawer { + /* prettier-ignore */ + height: calc(100% + 2.4rem); + border-bottom-right-radius: 0; + border-top-right-radius: 0; + z-index: 1; + overflow: hidden; + min-width: 240px; + + &-wrapper { + z-index: 4; + } + + .dc-contract-card { + margin: 0.8rem 0; + &__sell-button { + &--exit { + display: none; + } + } + } + + @include mobile { + z-index: 4; + height: auto; + border-bottom-right-radius: $BORDER_RADIUS; + border-top-right-radius: $BORDER_RADIUS; + width: calc(100% - 1.6rem); + margin-left: 0.8rem; + transition: none; + + &__mobile-wrapper { + position: relative; + } + &--with-collapsible-btn { + overflow: visible; + position: relative; + height: 100%; + display: flex; + flex-direction: column; + + & .dc-contract-card { + margin-top: 0; + } + } + &__transition { + &-enter, + &-exit { + transition: transform 0.25s linear; + } + } + & .dc-contract-card { + &__grid-underlying-trade { + grid-template-columns: 2fr 1fr !important; + } + &__underlying-name { + max-width: none; + } + } + &--is-multiplier { + & .dc-contract-card { + &__body-wrapper { + flex-direction: column; + padding-top: 0.4rem; + } + &-items-wrapper { + grid-template-columns: 1fr 1fr 1fr; + grid-gap: 0.4rem; + padding-bottom: 0.4rem; + border-bottom: 1px solid var(--general-section-1); + min-height: 80px; + + @media (max-width: 320px) and (max-height: 480px) { + min-height: unset; + } + } + &-item { + &__total-profit-loss { + flex-direction: row; + margin: 0 auto; + + & .contract-card-item__header { + font-size: 1.2rem !important; + + @media (max-width: 320px) and (max-height: 480px) { + font-size: 1rem !important; + } + } + } + &__header, + &__body { + font-size: 1.2rem; + } + &__body--loss { + padding-left: 0.4rem; + } + &:nth-child(1) { + order: 0; + } + &:nth-child(3), + &:nth-child(5) { + order: 2; + } + &:nth-child(6) { + order: 6; + } + @media only screen and (max-width: 320px) { + &__header { + font-size: 1rem; + } + } + @media (max-width: 320px) and (max-height: 480px) { + &__body { + font-size: 1rem; + } + } + } + @media (max-width: 320px) and (max-height: 480px) { + &__symbol, + &__type { + font-size: 1rem; + } + &__sell-button { + padding-top: 0.4rem; + + & .dc-btn { + height: 2.6rem; + + &__text { + font-size: 1rem; + } + } + } + &__footer { + margin-bottom: 0; + } + } + + &__sell-button { + &--has-cancel-btn { + display: grid; + grid-template-columns: 1fr 1fr; + } + @media (max-width: 320px) and (max-height: 480px) { + & .dc-btn--cancel { + & .dc-btn__text { + align-items: center; + } + & .dc-remaining-time { + font-size: 1rem; + padding-top: 0; + } + } + } + } + } + &.contract-drawer--with-collapsible-btn { + & .dc-contract-card { + &__indicative--movement { + margin-top: 2px; + } + } + } + & .dc-contract-card__grid-underlying-trade, + & .dc-contract-card__footer-wrapper { + grid-template-columns: 1fr 1fr 0.7fr; + + @media only screen and (max-height: 480px) { + grid-template-columns: 1fr 1fr; + } + } + @media only screen and (max-height: 480px) { + & .dc-contract-card__body-wrapper { + padding-top: 0.2rem; + } + } + + &-sold { + &.contract-drawer--with-collapsible-btn { + & .dc-contract-card-item { + &__total-profit-loss { + flex-direction: column; + } + &__body--profit { + font-size: 1.6rem; + } + } + } + } + } + } + } + .replay-chart__container { + width: 100%; + position: relative; + margin-left: 24px; + + .smartcharts { + left: 0; + border-radius: $BORDER_RADIUS; + + .ciq-chart { + .cq-top-ui-widgets, + & .info-box { + transition: transform 0.25s ease; + + .cq-symbols-display { + z-index: 1; + + &.ciq-disabled { + display: none; + } + } + .info-box-container { + transform: none; + opacity: 1; + left: 1px; + + .chart-close-btn { + display: none; + } + } + } + .sc-toolbar-widget, + .stx-panel-control, + .sc-navigation-widget { + transition: transform 0.25s ease; + } + .ciq-asset-information { + top: 75px; + } + .stx_jump_today.home > svg { + top: 10px; + left: 8px; + padding: 0; + position: absolute; + } + .cq-bottom-ui-widgets { + bottom: 30px !important; + + .digits { + margin-right: 0; + + &__container { + transition: transform 0.25s ease; + } + } + } + } + + /* postcss-bem-linter: ignore */ + &-mobile { + /* TODO: Remove this override once the issue is fixed in smartcharts */ + .stx-holder.stx-panel-chart { + z-index: 14; + + .cq-inchart-holder { + z-index: 107; + position: relative; + } + } + + .cq-context { + height: 100%; + } + } + } + + &-swipeable-wrapper { + .dc-swipeable__item { + margin-left: 0.8rem; + width: calc(100vw - 1.6rem); + } + } + + @include mobile { + height: 100%; + width: calc(100% - 1.6rem); + margin-left: 0.8rem; + } + } + @include mobile { + display: flex; + flex-direction: column-reverse; + height: 100%; + padding-bottom: 0; + position: relative; + + #dt_contract_drawer_audit { + flex: 1; + overflow: auto; + } + + & .contract-audit-card { + height: calc(100% - 1rem); + &__container { + height: 100%; + } + } + } + } + @include mobile { + flex-direction: column; + min-height: calc(100vh - 48px); + padding: 0; + } +} + +/** @define mobile-wrapper; weak */ +.mobile-wrapper { + padding: 0 0.8rem; + display: flex; + flex-direction: column; + height: 212px; + position: relative; + + &__content-loader { + position: absolute; + height: 100%; + width: 100%; + left: 0; + bottom: -0.8rem; + + svg { + height: 100%; + width: 100%; + } + } +} + +/** @define chart-container; weak */ +.chart-container { + width: 100%; + position: relative; + + &__wrapper { + position: fixed; + top: calc(#{$HEADER_HEIGHT} + 2px); + // charts width is 100% - sidebar width - sidebar margin - .trade-container padding + width: calc(100% - 240px - 1.6rem - 2.4rem); + height: calc(100vh - #{$HEADER_HEIGHT} - #{$FOOTER_HEIGHT}); + } + &__loader { + position: absolute; + height: calc(100% - 68px); + width: calc(100% - 12px); + top: 54px; + left: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: $BORDER_RADIUS; + background-color: var(--general-main-1); + + .initial-loader { + pointer-events: none; + } + .barspinner { + display: flex; + justify-content: center; + align-items: center; + margin: 0; + width: 100%; + height: 18px; + } + & + .smartcharts { + visibility: hidden; + pointer-events: none; + + .chart-marker-line__wrapper, + .cq-chart-controls, + .cq-symbols-display, + .cq-bottom-ui-widgets, + .cq-inchart-subholder { + display: none; + } + } + } + // smartchart library style fixes + .smartcharts-mobile { + .sc-categorical-display { + height: calc(100% - 8px) !important; + } + .ciq-chart { + padding: 0 0.8rem; + } + } + .cq-context { + top: 0; + left: 0; + z-index: 0; + + div.ciq-chart { + height: 100%; + box-shadow: none; + + div.cq-last-digits { + bottom: 15px; + left: calc(45% - 150px); + } + .info-box div.cq-chart-controls { + box-shadow: none; + } + // TODO: enable asset information + // div.ciq-asset-information { + // z-index: 0; + // top: 0; + // left: 0; + // } + div.stx_jump_today.home > svg { + top: 10px; + left: 8px; + padding: 0; + position: absolute; + } + div.stx-marker { + z-index: 2; + + &:not(.chart-marker-line) { + animation: fadeIn 0.2s; + } + } + } + div.cq-chart-control-left { + .cq-top-ui-widgets { + width: calc(100% - 9em); + } + } + } + div.debug-text { + display: none; + } + .cq-chart-control-left { + .cq-chart-controls, + .sc-toolbar-widget, + .stx-panel-control, + .sc-navigation-widget { + transform: translate3d(0, 0, 0); + transition: transform 0.25s ease; + } + .cq-top-ui-widgets { + left: 9em; + + .info-box { + transform: translate3d(0, 0, 0); + } + } + } + .ciq-chart { + .cq-top-ui-widgets, + & .info-box { + transition: transform 0.25s ease; + + .cq-symbols-display { + z-index: 1; + + &.ciq-disabled { + display: none; + } + @include mobile { + top: 0.8rem; + left: 0.8rem; + min-width: 170px; + max-width: 260px; + width: auto; + + .cq-menu-btn { + padding: 0.2rem; + } + .cq-symbol-select-btn { + padding: 0.3rem 0.9rem; + + .cq-symbol-dropdown { + transform: scale(1); + margin-left: auto; + } + .cq-symbol { + font-size: 1.2rem; + } + .cq-chart-price { + display: none; + } + } + } + } + } + .cq-chart-controls { + transition: max-width 0.25s ease, transform 0.25s ease; + } + .sc-navigation-widget, + .stx-panel-control { + transition: transform 0.25s ease; + } + .sc-toolbar-widget { + transition: transform 0.25s ease; + + @include mobile { + background: transparent; + border-width: 0; + bottom: 2.8rem; + + /* postcss-bem-linter: ignore */ + .sc-chart-mode, + .sc-studies { + background: var(--general-section-1); + padding: 0.4rem 0.2rem; + width: 4rem; + height: 4rem; + display: flex; + border-radius: 50%; + justify-content: center; + align-items: center; + margin: 0.8rem; + opacity: 0.75; + + &__menu { + &__timeperiod { + top: 0.8rem; + left: 0.8rem; + } + & > .ic-icon { + top: 0.6rem; + } + } + } + } + } + &--screenshot { + .sc-toolbar-widget, + .stx-panel-control, + .sc-navigation-widget, + .cq-top-ui-widgets { + transform: translate3d(0, 0, 0) !important; + } + } + } + .chartContainer { + background: transparent; + min-height: 100%; + } +} + +/** @define sidebar; weak; */ +.sidebar { + &__container { + position: relative; + margin: 0.8rem 0 0.8rem 1.6rem; + width: $SIDEBAR_WIDTH; + z-index: 5; + } + &__items { + opacity: 1; + transform: none; + position: relative; + min-height: 460px; + width: $SIDEBAR_WIDTH; + + &:after { + transition: opacity 0.25s cubic-bezier(0.25, 0.1, 0.1, 0.25); + opacity: 0; + position: absolute; + pointer-events: none; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; + content: ''; + background-color: var(--overlay-outside-dialog); + } + &--market-closed { + & .dc-tooltip--with-label { + display: none; + } + & .dc-tooltip--with-label:before, + .dc-tooltip--with-label:after { + display: none; + } + } + } +} + +// TODO: improve handling of rendering markets dropdown via portals from smartcharts library +/** @define smartcharts-portal; weak */ +.smartcharts-portal { + @include mobile { + &--open { + .smartcharts { + z-index: 9999; + } + } + } +} + +/** @define contract; weak */ +.contract { + // TODO: Remove below if redundant + &-update { + /* postcss-bem-linter: ignore */ + &__wrapper { + display: flex; + flex-direction: column; + + /* postcss-bem-linter: ignore */ + & .dc-tooltip:before, + & .dc-tooltip:after { + display: none; + } + /* postcss-bem-linter: ignore */ + & .dc-contract-card-dialog__button { + display: flex; + align-items: flex-end; + } + } + } + &--enter { + transform: translate3d(calc(100% + 1.6em), 0, 0); + opacity: 0; + } + &--exit { + transform: translate3d(calc(100% + 1.6em), 0, 0); + opacity: 0; + pointer-events: none; + } +} + +/** @define smartcharts; weak */ +/* postcss-bem-linter: ignore */ +.smartcharts { + &-dark, + &-light { + @include mobile { + /* postcss-bem-linter: ignore */ + .cq-menu-dropdown-enter-done { + margin-top: 0; + + /* postcss-bem-linter: ignore */ + .icon-close-menu { + opacity: 1; + pointer-events: auto; + top: 8px; + } + } + .cq-dialog-portal { + /* postcss-bem-linter: ignore */ + .cq-dialog { + max-width: calc(100% - 36px); + } + } + /** @define ciq-chart-type; weak */ + .sc-chart-type { + &__item { + /* postcss-bem-linter: ignore */ + .sc-tooltip, + .dc-tooltip { + display: none; + } + } + } + /** @define ciq-chart-mode; weak */ + .sc-chart-mode { + /* postcss-bem-linter: ignore */ + &__section__item { + /* postcss-bem-linter: ignore */ + .sc-interval { + display: grid; + padding: 1.6rem; + grid-template-columns: 1fr; + + /* postcss-bem-linter: ignore */ + &__content { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + padding-top: 16px; + } + /* postcss-bem-linter: ignore */ + &__item { + width: 100% !important; + margin: 0; + + /* postcss-bem-linter: ignore */ + .sc-tooltip, + .dc-tooltip { + display: none; + } + } + /* postcss-bem-linter: ignore */ + &__info { + margin-top: 0.4rem; + padding-left: 0.2rem; + } + } + } + } + /** @define cq-top-ui-widgets; weak */ + .cq-top-ui-widgets { + z-index: 15 !important; + } + } + } + /* TODO: Remove this override once the issue is fixed in smartcharts */ + /* postcss-bem-linter: ignore */ + @at-root body.theme--light & .chart-line.horizontal .title-wrapper { + background-image: linear-gradient( + rgba(255, 255, 255, 0.001) 30%, + var(--general-main-1) 50%, + rgba(255, 255, 255, 0.001) 75% + ); + } +} diff --git a/packages/reports/src/sass/app/_common/mobile-widget.scss b/packages/reports/src/sass/app/_common/mobile-widget.scss new file mode 100644 index 000000000000..b9896a97cd8a --- /dev/null +++ b/packages/reports/src/sass/app/_common/mobile-widget.scss @@ -0,0 +1,159 @@ +/** @define mobile-widget */ +.mobile-widget { + border-radius: 4px; + padding: 1rem 0.8rem; + height: 4rem; + background-color: var(--general-main-1); + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 0 0.8rem; + flex: 1; + + &__amount { + @include typeface(--paragraph-center-bold-black); + color: var(--text-prominent); + } + &__duration { + @include typeface(--paragraph-center-normal-black); + color: var(--text-prominent); + } + &__type { + @include typeface(--paragraph-center-normal-black); + color: var(--text-less-prominent); + } + &__item { + color: var(--text-prominent); + line-height: 1.4rem; + + &-value { + font-weight: bold; + font-size: 1.2rem; + } + } + &__multiplier { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0; + + &-amount { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.4rem 0.8rem 0; + + .mobile-widget__item-label { + margin-right: 0.4rem; + } + } + &-expiration { + min-width: 7.2rem; + display: flex; + flex-direction: column; + align-items: center; + } + &-options { + flex: none; + flex-direction: column; + padding: 0.6rem 0.8rem; + margin-bottom: 0.6rem; + margin-left: 0.8rem; + justify-content: center; + min-width: 8.8rem; + + .mobile-widget__item-label { + color: var(--text-general); + } + } + &-risk-management { + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + margin-bottom: 0.6rem; + color: var(--text-general); + padding: 0.4rem 0.8rem; + + .mobile-widget__item { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + } + .mobile-widget__item-label { + color: var(--text-general); + } + .mobile-widget__item-label, + .mobile-widget__item-value { + font-size: 1.4rem; + line-height: 1.8rem; + } + } + &-trade-info { + display: flex; + justify-content: space-evenly; + background: var(--general-main-1); + flex: 2; + margin-left: 0.8rem; + + &--no-stop-out { + justify-content: flex-end; + margin-right: 1.6rem; + } + &-tooltip-text { + display: flex; + flex-direction: row; + justify-content: center; + + span:before { + content: ': '; + } + &:first-child { + margin-right: 0.8rem; + } + @media only screen and (max-width: 340px) { + font-size: 0.8rem; + } + } + } + } + &__wrapper { + display: flex; + + .mobile-widget:last-child:not(:only-child) { + margin-left: 0.8rem; + flex: unset; + } + } +} + +/** @define fieldset-minimized */ +.fieldset-minimized { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5em; + white-space: pre-line; + + &__amount { + grid-area: c; + } + &__barrier1 { + grid-area: b; + } + &__barrier2 { + grid-area: d; + } + &__currency:before { + margin-right: 0.1em; + position: static; + display: inline; + font-size: 1em; + } + &__basis { + font-weight: bold; + color: var(--text-prominent); + } +} diff --git a/packages/reports/src/sass/app/modules/contract.scss b/packages/reports/src/sass/app/modules/contract.scss new file mode 100644 index 000000000000..c67dfeb6a995 --- /dev/null +++ b/packages/reports/src/sass/app/modules/contract.scss @@ -0,0 +1,165 @@ +$CONTRACT_INFO_BOX_PADDING: 1.6em; + +.info-box-container { + position: absolute; + z-index: 3; + top: 1em; + right: 1em; + display: flex; + justify-content: space-between; + width: 100%; + + &-button:hover { + cursor: pointer; + } + &-icon { + width: 32px; + height: 32px; + } + .info-box { + position: relative; + border-radius: $BORDER_RADIUS; + padding: $CONTRACT_INFO_BOX_PADDING; + background: var(--general-section-1); + font-weight: 300; + + &-longcode { + display: flex; + + &-icon { + @extend %inline-icon; + margin-right: 1.6rem; + padding: 0.4rem; + + @include mobile { + margin-right: 0.8rem; + } + } + &-text { + max-width: 360px; + + @include mobile { + max-width: 175px; + line-height: 1.4; + letter-spacing: normal; + font-size: 1rem; + /* iPhone SE screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 480px) { + font-size: 0.8rem; + } + } + } + } + .expired { + display: flex; + align-items: center; + + svg { + width: 2.4em; + height: 2.4em; + margin-right: 1em; + + .color1-fill { + fill: var(--status-success); + } + } + .pl-value { + color: var(--text-profit-success); + font-weight: bold; + font-size: 1.6em; + line-height: 1.5em; + + .percentage { + display: inline-block; + margin-left: 0.7em; + } + } + &.lost { + .pl-value { + color: var(--text-loss-danger); + } + svg .color1-fill { + fill: var(--status-danger); + } + } + .sell-info { + margin-right: 2em; + text-align: center; + line-height: 1.2em; + } + } + .general { + display: flex; + align-items: center; + line-height: 1.4em; + + .values { + margin-left: 1em; + margin-right: 2em; + text-align: right; + font-weight: bold; + + .profit { + color: var(--text-profit-success); + } + .loss { + color: var(--text-loss-danger); + } + } + .sell { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + margin: -$CONTRACT_INFO_BOX_PADDING; + margin-left: $CONTRACT_INFO_BOX_PADDING; + padding: $CONTRACT_INFO_BOX_PADDING; + + .dc-tooltip { + position: absolute; + bottom: 0.5em; + right: 0.5em; + line-height: 0; + + &:before { + width: 350px; + white-space: normal; + } + } + } + } + @include mobile { + padding: 0.8rem; + margin-left: 0.8rem; + } + } + .message { + margin-top: 0.5em; + border: 1px solid var(--brand-red-coral); + border-radius: $BORDER_RADIUS; + padding: 1em; + background-color: transparentize($color-black, 0.84); + display: flex; + align-items: center; + + .message-icon { + margin-right: 1em; + } + .message-text { + flex-grow: 1; + } + .message-close { + cursor: pointer; + } + } + .chart-close-btn { + position: absolute; + cursor: pointer; + z-index: 11; + right: 0; + top: 0; + } + @include mobile { + left: 0; + } +} diff --git a/packages/reports/src/sass/app/modules/contract/bottom-widgets.scss b/packages/reports/src/sass/app/modules/contract/bottom-widgets.scss new file mode 100644 index 000000000000..9dd994c8370a --- /dev/null +++ b/packages/reports/src/sass/app/modules/contract/bottom-widgets.scss @@ -0,0 +1,10 @@ +/** @define bottom-widgets */ +.bottom-widgets { + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: flex-end; + bottom: 2em; +} diff --git a/packages/reports/src/sass/app/modules/contract/digits.scss b/packages/reports/src/sass/app/modules/contract/digits.scss new file mode 100644 index 000000000000..76dc2ae097d4 --- /dev/null +++ b/packages/reports/src/sass/app/modules/contract/digits.scss @@ -0,0 +1,464 @@ +/** @define cq-bottom-ui-widgets; weak */ +.cq-bottom-ui-widgets { + z-index: 4; + overflow: visible; + height: 0; + top: unset !important; + bottom: 80px; + + .bottom-widgets { + left: -3.5em; + } +} + +/** @define digits; weak */ +.digits { + $self: &; + display: flex; + align-items: center; + position: relative; + margin: 0 2.5em 0 1em; + + &__container { + position: relative; + + /* Screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 520px) { + transform: scale(0.85); + transform-origin: bottom; + padding: 0 0.8rem !important; + } + @media only screen and (max-height: 480px) { + transform: scale(0.75); + } + } + &__tooltip-container { + position: absolute; + z-index: 2; + top: -10px; + right: 10px; + } + &__digit { + pointer-events: none; + margin: 0 0.6em; + text-align: center; + position: relative; + transition: transform 0.25s linear; + + &--latest { + z-index: 1; + transform: scale(1.2); + + .digits__digit-spot { + font-size: 0.9em; + } + } + &--win { + z-index: 1; + transform: scale(1.25); + + .digits__pie-container:before { + box-shadow: 0px 1px 18px var(--text-profit-success); + } + } + &--loss { + z-index: 1; + transform: scale(1.25); + + .digits__pie-container:before { + top: -2px; + left: -2px; + box-shadow: 0px 1px 18px var(--text-loss-danger); + border: 3px solid var(--text-loss-danger); + + @include mobile { + top: -1px; + left: -3px; + } + } + } + &--is-selected { + .digits__digit-display { + &-value, + &-percentage { + color: $color-white; + } + } + .progress__bg { + fill: $color-blue-2; + } + } + &--is-selectable { + pointer-events: auto; + + &:focus, + &:active, + &:hover { + .digits__digit-display { + &-value, + &-percentage { + color: $color-white; + } + } + .progress__bg { + fill: $color-blue-2; + } + } + } + &-display-value { + transition: transform 0.25s linear; + position: absolute; + transform: scale(0.9); + top: 4px; + color: var(--text-prominent); + + &--no-stats { + transform: scale(1) translateY(5px); + } + + @include mobile { + top: 10px; + transform: none; + font-size: 1.4rem; + line-height: 1.43; + + &--no-stats { + top: 15px; + } + } + } + &-display-percentage { + top: 20px; + position: absolute; + font-size: 0.65em; + font-weight: 400; + color: var(--text-general); + + @include mobile { + top: 25px; + transform: none; + font-size: 1rem; + line-height: 1.4; + } + } + &-value { + @include typeface(--paragraph-center-bold-black); + width: 40px; + height: 40px; + background-color: var(--general-main-1); + color: var(--text-prominent); + margin-bottom: 0.5em; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + transition: background-color 0.2s ease-out, transform 0.1s ease-out; + + &--selected { + background-color: var(--general-main-2); + } + &--win { + background-color: var(--text-profit-success); + color: var(--text-prominent); + + .digits__digit-display-value, + .digits__digit-display-percentage { + color: $color-white; + } + } + &--blink { + animation-duration: 500ms; + animation-iteration-count: 4; + animation-timing-function: step-end; + animation-name: blinking; + } + &--loss { + border: none !important; + background-color: var(--text-loss-danger); + color: var(--text-prominent); + + .digits__digit-display-value, + .digits__digit-display-percentage { + color: $color-white; + } + } + @include mobile { + height: 48px; + width: 48px; + margin: 0; + } + } + &-spot { + position: absolute; + top: -25px; + left: -50%; + right: -50%; + width: auto; + white-space: nowrap; + + &-value { + transform: scale(0.95); + display: inline-block; + } + &-last { + @include typeface(--paragraph-center-bold-black); + padding: 0 4px; + margin-left: 1px; + border-radius: 50%; + border: 1px solid $color-blue; + color: var(--text-prominent); + background: var(--general-main-2); + + &--selected-win { + color: var(--text-profit-success); + } + &--win { + color: var(--text-colored-background); + border-color: var(--text-profit-success); + background: var(--text-profit-success); + } + &--loss { + color: var(--text-colored-background); + border-color: var(--text-loss-danger); + background: var(--text-loss-danger); + } + } + @include mobile { + display: flex; + justify-content: center; + top: 1.6rem; + left: 0; + right: 0; + margin-top: 4.8rem; + pointer-events: none; + + &-value, + &-last { + font-size: 2rem; + } + &-last { + padding: 0 8px; + } + &:not(&--is-trading) { + position: relative; + top: 0.8rem; + } + /* iPhone 8 screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 580px) { + &--is-trading { + &-value, + &-last { + font-size: 1.4rem; + } + &-last { + padding: 0 7px; + } + } + } + @media only screen and (max-height: 580px) { + position: relative; + + &:not(&--is-trading) { + top: 1.4rem; + } + } + @media only screen and (max-height: 520px) { + position: relative; + top: 0; + margin-top: 1.6rem; + + &--is-trading { + margin-top: auto; + } + } + } + } + } + &__pie-container { + position: absolute; + top: -1px; + left: -1px; + + &:before { + position: absolute; + width: 40px; + height: 40px; + border-radius: 50%; + top: 1px; + left: 1px; + content: ''; + + @include mobile { + top: -4px; + left: -2px; + width: 48px; + height: 48px; + } + } + &--selected:before { + top: -2px; + left: -2px; + width: 42px; + height: 42px; + border: 2px solid var(--text-profit-success); + + @include mobile { + top: 0; + left: -2px; + width: 48px; + height: 48px; + } + } + } + &__pie-progress { + transform: rotate(-90deg); + width: 42px; + height: 42px; + + /* postcss-bem-linter: ignore */ + .progress__bg { + stroke: var(--general-disabled); + } + /* postcss-bem-linter: ignore */ + .progress__value { + stroke: var(--text-less-prominent); + + /* postcss-bem-linter: ignore */ + &--is-max { + stroke: var(--text-profit-success); + } + /* postcss-bem-linter: ignore */ + &--is-min { + stroke: var(--text-loss-danger); + } + } + @include mobile { + width: 48px; + height: 48px; + margin-top: 0.2rem; + } + } + &__pointer { + position: absolute; + bottom: -12px; + padding: 0 12px; + transition: transform 0.25s ease; + + @include mobile { + left: -2px; + } + } + &__particles { + position: absolute; + padding: 0 20px; + top: 8px; + transform: rotate(45deg); + opacity: 0; + + &-particle { + background: var(--brand-secondary); + opacity: 0.7; + border-radius: 50%; + display: block; + width: 5px; + height: 5px; + position: absolute; + transition: transform 0.5s ease, opacity 0.5s linear 0.5s; + } + &--explode { + opacity: 1; + + .digits__particles-particle { + opacity: 0; + + &:nth-child(1) { + transform: translate(45px, 45px); + } + &:nth-child(2) { + transform: translate(45px, 0px); + } + &:nth-child(3) { + transform: translate(0px, 45px); + } + &:nth-child(4) { + transform: translate(-45px, 45px); + } + &:nth-child(5) { + transform: translate(-45px, -45px); + } + &:nth-child(6) { + transform: translate(-45px, 0px); + } + &:nth-child(7) { + transform: translate(0px, -45px); + } + &:nth-child(8) { + transform: translate(45px, -45px); + } + } + } + } + &__icon { + &-color { + fill: var(--brand-orange); + } + &--win .digits__icon-color { + fill: var(--text-profit-success); + } + &--loss .digits__icon-color { + fill: var(--text-loss-danger); + } + } + @include mobile { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 2.4rem 1.6rem; + align-items: center; + margin: 1.6rem 0; + + &--trade { + grid-gap: 2rem 1.6rem; + margin: auto 0; + transform: scale(1.1); + transform-origin: bottom; + + @media only screen and (max-height: 580px) { + transform: unset; + transform-origin: unset; + } + @media only screen and (max-height: 480px) { + margin: auto 0 4.8rem; + } + } + &__container { + width: 100%; + margin: 0; + padding: 0.8rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + &__tooltip-text { + font-size: 1.2rem; + color: var(--text-general); + line-height: 18px; + text-align: center; + } + &__digit { + margin: auto; + } + } + @include mobile { + @at-root .popup-root #{$self}__toast-info { + top: 10.5rem; + } + } +} + +@keyframes blinking { + 50% { + background-color: var(--general-main-1); + color: var(--text-general); + } +} diff --git a/packages/reports/src/sass/app/modules/mt5/cfd-dashboard.scss b/packages/reports/src/sass/app/modules/mt5/cfd-dashboard.scss new file mode 100644 index 000000000000..c4289961409a --- /dev/null +++ b/packages/reports/src/sass/app/modules/mt5/cfd-dashboard.scss @@ -0,0 +1,1539 @@ +/* +* [Work In Progress] +* This file is a WIP and will be broken down into different files for different components before the module is enabled +* Class names might change, properties and values will definitely change +* */ + +/* stylelint-disable */ +/** @define cfd-dashboard; */ +.cfd-dashboard { + padding: 4.8em 0; + max-width: fit-content; + margin: 0 auto; + + &__dxtrade-download { + background: $color-black-7; + border-radius: $BORDER_RADIUS; + text-decoration: none; + display: grid; + grid-template-columns: 32px 1fr; + grid-gap: 0.8rem; + width: 12.7rem; + align-items: center; + height: 4rem; + border: 1px solid #a6a6a6; + + &-text { + display: flex; + flex-direction: column; + padding: 0.4rem 0rem 0.4rem 0.2rem; + } + &--icon { + margin-left: 0.6rem; + } + } + &--hint { + @include typeface(--xsmall-left-normal-black, none); + } + &__container { + position: relative; + overflow-y: auto; + + .notification-messages { + position: fixed; + right: 16px; + top: 64px; + } + } + &__info { + background: var(--fill-disabled); + margin-top: 5rem; + font-size: 1.4rem; + color: var(--text-general); + display: grid; + grid-template-columns: 0.85fr 1fr 1fr; + height: 56px; + border-radius: $BORDER_RADIUS; + + &-description, + &-copy { + display: flex; + align-items: center; + } + &-description { + justify-content: flex-start; + padding-left: 3.2rem; + } + &-copy { + justify-content: center; + font-size: 1.6rem; + font-weight: bold; + color: var(--text-prominent); + } + &-display { + padding-right: 0.8rem; + } + &-label { + padding-right: 0.4rem; + } + @include mobile { + display: flex; + height: 100%; + padding: 1.6rem; + flex-direction: column; + align-items: flex-start; + + &-copy { + margin: 0 0 0.8rem; + font-size: 1.4rem; + } + &-description { + margin: 0.8rem 0 1.6rem; + } + } + } + &__welcome-message { + margin-bottom: 2.4rem; + + &--heading { + @include typeface(--title-center-bold-black, none); + color: var(--text-general); + } + } + &__missing-real { + align-items: center; + background-color: var(--general-section-1); + display: flex; + justify-content: space-between; + margin: 2.4em 0 0; + max-width: 960px; + padding: 1.6rem; + + &--heading { + color: var(--text-prominent); + line-height: 1.5; + margin-right: 1rem; + + @include desktop { + white-space: nowrap; + } + @include typeface(--title-left-bold-black, none); + @include mobile { + font-size: 1.2rem; + } + } + &--button { + height: 4rem; + } + @include mobile { + flex-direction: column; + padding: 0.8rem; + width: calc(100vw - 3.2rem); + + &--heading { + margin-bottom: 0.8rem; + margin-right: 0; + } + &--button { + width: 27rem; + } + } + } + &__download-center { + border-top: 2px solid var(--general-section-1); + padding-top: 2rem; + padding-bottom: 5.8rem; + + &-options { + display: flex; + justify-content: center; + + &--desktop { + display: block; + align-items: baseline; + flex-flow: column nowrap; + + &-links { + display: flex; + flex-direction: column; + } + &-row { + display: flex; + align-items: baseline; + justify-content: center; + margin-bottom: 3.1rem; + + > svg:first-child { + margin-bottom: 0.4rem; + margin-right: 0.8rem; + } + a > svg { + margin-top: 0.8rem; + margin-right: 0.8rem; + } + } + &-download { + display: grid; + grid-auto-flow: column; + grid-gap: 0.8rem; + justify-content: center; + + a { + margin-top: 0.4rem; + } + } + } + &--mobile { + align-items: center; + display: flex; + flex-flow: column nowrap; + margin-left: 4.7rem; + + &-devices { + margin-bottom: 1.6em; + svg:first-child { + margin-right: 0.8em; + } + } + &-link:not(:first-child) { + margin-left: 1.6rem; + } + @include mobile { + margin-left: unset; + margin-right: unset; + } + } + } + &--heading { + @include typeface(--title-center-bold-black, none); + color: var(--text-general); + line-height: 1.5; + margin-left: 2.1rem; + margin-bottom: 4.2rem; + @include desktop { + margin-left: 0; + text-align: center; + } + } + &--hint { + margin-top: 1.6rem; + } + @include mobile { + margin-bottom: 1.6rem; + } + } + &__maintenance { + @include typeface(--small-center-normal-black, none); + display: flex; + justify-content: center; + margin: 2.4rem 0; + + &-icon { + margin-right: 0.8rem; + margin-top: 0.2rem; + + @include mobile { + margin-top: -0.2rem; + } + } + &-text { + text-align: left; + line-height: 1.67; + } + } + &__real-accounts-wrapper { + display: flex; + flex-direction: column; + } + .notification-messages { + top: calc(-4.8rem + 16px); + } + @include mobile { + margin-top: 2.4rem; + padding: 0 1.6rem; + } +} + +.cfd-attribute-describer { + .counter { + color: var(--brand-red-coral); + vertical-align: top; + } + @include mobile { + font-size: 1.2rem; + } +} + +.cfd-compare-accounts { + display: flex; + flex-flow: column nowrap; + flex-grow: 1; + background: inherit; + height: 100%; + position: relative; + + &__table { + @include mobile { + overflow-x: auto; + } + } + + &__table-row { + grid-template-columns: var(--cfd-compare-accounts-template-columns); + box-shadow: inset 0 -1px 0 0 var(--general-section-1); + } + &__table-row-eu { + grid-template-columns: 1.5fr 2fr 2fr; + box-shadow: inset 0 -1px 0 0 var(--general-section-1); + } + &__star { + color: var(--brand-red-coral); + } + &__bullet { + flex: none; + + &--circle { + background-color: var(--text-general); + border-radius: 100%; + margin-right: 1.2rem; + margin-left: 0.2rem; + margin-top: 0.8rem; + width: 0.4rem; + height: 0.4rem; + } + &--star { + margin-right: 1rem; + } + &-wrapper { + display: flex; + margin-bottom: 0.8rem; + line-height: 1.5; + } + } + &__footnote { + &-title { + padding-bottom: 0.8rem; + } + } + + .dc-table { + padding: 0 30px; + + &__header { + border-bottom: none; + height: 50px; + position: sticky; + top: 0; + background: var(--general-main-2); + z-index: 10; + } + &__cell, + &__head { + padding: 0.8rem; + + &--fixed { + padding-left: 1.6rem; + + @include mobile { + background-color: var(--general-section-2); + } + } + } + &__cell { + border-bottom: none; + align-items: normal; + padding: 1rem 0.8rem; + } + &--scrollbar { + display: block; // Safari needs this to work properly! + } + @include desktop { + &__header { + background-color: var(--general-main-2); + } + } + } + &-modal { + &__wrapper { + display: flex; + justify-content: center; + margin-top: 2.4rem; + } + } + @include mobile { + background: var(--general-main-1); + height: 100%; + font-size: 1.2rem; + + .dc-table { + padding: 0; + height: inherit; + + &__cell { + font-size: 1.2rem; + } + &__row { + padding: 0; + } + &__head { + font-size: 1.2rem; + border-bottom: 2px solid var(--border-disabled); + background-color: var(--general-main-1); + margin-top: -1px; + } + &__header, + &__row { + width: 150vw; + grid-template-columns: 1fr 1fr 1fr 1fr; + } + } + } +} + +.cfd-compare-account--hint { + color: var(--text-less-prominent); + font-size: 12px; + line-height: 1.5em; + width: 100%; + padding: 1.6rem 2.4rem 0.8rem 2.4rem; + + @include mobile { + padding: 1.6rem; + border-top: 2px solid var(--border-disabled); + } +} + +.cfd-dashboard__accounts-error { + background-color: var(--status-warning-transparent); + margin: 1.2rem 0; + + &-message { + padding: 0.8rem 2.4rem; + } +} + +.cfd-real-accounts-display, +.cfd-demo-accounts-display { + display: grid; + grid-auto-flow: column; + grid-gap: 2.4rem; + justify-content: center; + padding-top: 2.4em; + height: 100%; + + @include mobile { + display: flex; + gap: 0; + overflow-x: hidden; + flex-direction: column; + height: unset; + } +} + +.cfd-demo-accounts-display .cfd-account-card__wrapper { + @include desktop { + height: auto; + } +} + +.cfd-real-accounts-display { + transition: margin-bottom 0.3s ease-in-out; + margin-bottom: var(--cfd-real-acc-margin-bottom); + + .dc-carousel__card, + .dc-carousel__wrapper { + padding: 0; + } + + &--has-trade-servers { + .cfd-account-card__wrapper { + @include desktop { + grid-template-rows: 1fr 4rem; + } + } + } +} + +.cfd-account-card { + border: solid 1px var(--border-normal); + border-radius: 4px; + display: flex; + flex-flow: column nowrap; + min-height: 37rem; + width: 30.4em; + position: relative; + @include desktop { + height: 100%; + } + + &__wrapper { + display: grid; + grid-template-rows: 1fr; + height: 100%; + justify-content: center; + + @include mobile { + grid-template-rows: 1fr; + height: fit-content; + + &:not(:last-child) { + margin-bottom: 2.4rem; + } + } + + @include desktop { + height: 100%; + } + } + + &__logged-out { + min-height: 29rem; + } + + &__add-server { + font-size: 1.4rem; + cursor: pointer; + display: grid; + grid-auto-flow: column; + grid-gap: 0.4rem; + justify-content: center; + align-items: center; + width: 100%; + height: 4rem; + color: var(--text-prominent); + + @include mobile { + height: 4rem; + align-content: start; + } + + &--disabled { + cursor: not-allowed; + color: var(--text-disabled); + + .cfd-account-card__add-server--icon { + background-color: var(--border-disabled); + color: var(--text-disabled); + } + } + &-exit, + &-enter-done { + transition: all 0.3s ease-in-out; + opacity: 1; + } + &-enter, + &-exit-done { + transition: all 0.3s ease-in-out; + opacity: 0; + } + &--icon { + color: var(--status-colored-background); + background-color: var(--brand-red-coral); + border-radius: 100%; + width: 2.4rem; + height: 2.4rem; + font-size: 1.8rem; + display: block; + text-align: center; + padding: 0.2rem; + } + } + + &__server { + font-size: 1.4rem; + text-align: left; + width: 27.2rem; + &--value { + font-weight: bold; + } + } + + &:hover { + box-shadow: 0 2px 8px 0 var(--shadow-menu); + } + &--heading { + @include typeface(--title-left-bold-black, none); + color: var(--text-general); + line-height: 1.5; + } + &--paragraph { + @include typeface(--paragraph-left-normal-black, none); + color: var(--text-general); + line-height: 1.45; + } + &--balance { + @include typeface(--title-left-bold-black); + color: var(--text-profit-success); + } + &__banner { + position: absolute; + transform: rotate(45deg); + border-bottom: 2.3rem solid var(--brand-red-coral); + border-left: 2.3rem solid transparent; + border-right: 2.3rem solid transparent; + height: 0; + width: 13.7rem; + left: 19.4rem; + top: 2.8rem; + color: var(--text-colored-background); + font-weight: bold; + line-height: 24px; + white-space: nowrap; + text-align: center; + + &--demo { + border-bottom-color: var(--brand-secondary); + } + &--server { + // TODO: Add a CSS variable + border-bottom-color: $color-blue-2; + } + @include mobile { + left: calc(100vw - 14.2rem); + } + } + &__type { + display: flex; + padding: 1.5rem 1.6rem 1.6rem; // -1 for border + + &--description { + display: flex; + margin-left: 1.6rem; + max-width: 19.2rem; + flex-direction: column; + + .cfd-account-card--paragraph { + width: 18.3rem; + padding-top: 0.8rem; + } + } + &--has-banner { + .cfd-account-card--paragraph { + width: 15rem; + } + } + } + &__display { + display: flex; + flex-wrap: wrap; + width: min-content; + justify-content: center; + height: fit-content; + + &-text { + @include typeface(--title-center-bold-black); + color: var(--brand-red-coral); + line-height: 1.5; + margin-top: 0.4rem; + + &--top-right { + position: relative; + left: 22.2rem; + bottom: 7.2rem; + margin: 0; + } + } + &--fixed-height { + height: 64px; // don't leave space for the demo text that is moved to the top right + } + } + &__cta { + display: flex; + flex-direction: column; + margin-top: auto; + + &-wrapper { + align-items: center; + display: flex; + flex-direction: column; + position: relative; + } + } + &__specs { + padding: 1.6rem; + width: 100%; + + &-table { + width: 100%; + + &-attribute .cfd-account-card--paragraph { + color: var(--text-general); + margin-right: 2.4rem; + } + &-data { + .cfd-account-card--paragraph { + @include typeface(--paragraph-left-bold-black, none); + line-height: 1.45; + } + } + } + } + &__login-specs { + margin: 0.8rem 0; + width: 100%; + padding: 0 1.8rem; + + &-table { + width: 100%; + table-layout: fixed; + border-collapse: separate; + border-spacing: 0 0.8rem; + + &-attribute { + width: 35%; + vertical-align: middle; + .cfd-account-card--paragraph { + color: var(--text-general); + } + } + &-data { + width: 65%; + vertical-align: middle; + .cfd-account-card--paragraph { + @include typeface(--paragraph-left-normal-black, none); + line-height: 1.45; + } + } + &-row { + &--account-id { + height: 3.6rem; + } + } + } + } + &__spec-box { + display: grid; + grid-template-columns: 1fr 2.4rem; + border: 1px solid var(--border-normal); + border-radius: $BORDER_RADIUS; + } + &__spec-text { + padding: 0.4rem; + font-family: Courier, monospace; + overflow: hidden; + } + &__spec-copy { + display: grid; + justify-content: center; + align-content: center; + border-left: 1px solid var(--border-normal); + } + &__password { + &-box { + display: grid; + grid-template-columns: 1fr 2.4rem; + border: 1px solid var(--border-normal); + border-radius: $BORDER_RADIUS; + } + &-text { + padding: 0.4rem; + overflow: hidden; + } + &-action { + display: grid; + justify-content: center; + align-content: center; + border-left: 1px solid var(--border-normal); + border-radius: 0; + padding: 0 0 0 0.5rem; + overflow: hidden; + } + &-tooltip { + width: 20rem; + } + } + &__account-selection { + align-items: center; + display: flex; + height: 4rem; + justify-content: center; + margin: 0 1.6rem 1.5rem; // -1 for border + padding: 0; + text-decoration: none; + width: calc(100% - 3.2rem); + @include typeface(--paragraph-center-bold-black, none); + + &--primary { + background-color: var(--brand-red-coral); + color: var(--text-colored-background); + } + &--secondary { + background-color: transparent; + border: 2px solid var(--button-secondary-default); + color: var(--text-prominent); + + &:hover { + background-color: var(--button-secondary-hover); + } + } + &--disabled { + background-color: var(--brand-red-coral); + cursor: initial; + opacity: 0.32; + } + } + &__login { + @include typeface(--title-center-normal-black); + align-items: center; + background-color: var(--general-section-1); + border-radius: 4px; + color: var(--text-general); + display: flex; + padding: 0.8rem; + justify-content: center; + margin: 1rem 0 1.6rem; + width: 272px; + + strong { + font-weight: bold; + } + span, + strong { + margin-right: 0.8rem; + } + } + &__manage { + display: flex; + width: calc(100% - 3.2rem); + + .dc-btn { + width: 100%; + margin-bottom: 1.6rem; + height: 4rem; + + &:not(:last-child) { + margin-right: 1.6rem; + } + } + } + &__action-wrapper { + margin: 0 auto 1.3rem; + padding-top: 1rem; + width: 75%; + text-align: center; + + &__link { + font-size: var(--text-size-xxs); + + &--disabled { + pointer-events: none; + } + } + } + &__divider { + width: calc(100% - 3.2rem); + margin-left: 1.6rem; + border-top: 1px solid var(--general-section-1); + margin-top: 1rem; + } + @include mobile { + width: calc(100vw - 3.2rem); + height: fit-content; + + &:not(:last-child) { + margin-bottom: 2.4rem; + margin-right: 0; + } + } +} + +.cfd-password-manager { + margin: 5.6rem auto 3.2rem; + width: 100%; + height: 100%; + + @media only screen and (max-height: 645px) { + margin-top: 1.5rem; + overflow: auto; + } + + &--paragraph { + margin-bottom: 3.2rem; + } + &--error-message { + margin-bottom: 1em; + } + &__investor-wrapper { + margin: 3.2rem 0.5rem 0; + + & .dc-input__label { + top: 0.9rem; + } + @include mobile { + padding-bottom: 12rem; + } + } + .dc-password-meter__container, + .dc-password-input { + width: 30rem; + margin: auto; + } + &__investor-wrapper { + .cfd-password-manager--paragraph:first-child { + margin-bottom: 0.8rem; + } + } + &__investor-form { + width: 100%; + max-width: 300px; + margin: 2.4em auto 0; + + @include mobile { + max-width: unset; + } + } + &__new-password { + margin-bottom: 8.6rem; + } + &__actions { + align-items: center; + display: flex; + flex-flow: column nowrap; + } + &--button { + margin-top: 1.6rem; + + @include mobile { + width: 100%; + margin-top: 0.8rem; + max-width: 30rem; + white-space: normal; + } + } + &__success { + text-align: center; + margin-top: 3.2rem; + + &-header { + @include typeface(--paragraph-center-bold-black); + margin: 1.6rem 0 0.8rem; + } + &-btn { + margin-top: 3.2rem; + width: 6.4rem; + + @include mobile { + width: 100%; + max-width: 30rem; + } + } + } + .multi-step__header { + margin: 0 0 2.4rem 0; + } + .multi-step__component { + text-align: center; + } + @include desktop { + max-width: calc(450px + 1rem); // needs extra padding for the scrollbar, otherwise it will cover the words + } + @include mobile { + padding: 0 1.6rem; + margin-top: 2.4rem; + + &__scroll-wrapper { + overflow-y: auto; + overflow-x: hidden; + } + /* iPhone SE screen width fixes due to UI space restrictions */ + @media only screen and (max-width: 320px) { + padding: 0; + } + } +} + +.cfd-financial-stp-modal { + display: grid; + grid-template-rows: 13rem minmax(10rem, 1fr); + height: 100%; + position: relative; + width: 100%; + + .dc-modal-footer { + position: fixed; + padding: unset; + bottom: 1.6rem; + right: 1.6rem; + } + + &__form { + height: 100%; + display: flex; + flex-direction: column; + + .details-form { + flex: 1; + } + } + @include desktop { + overflow: hidden; + + & .details-form__description { + width: 100%; + } + } + @include mobile { + overflow: hidden; + grid-template-rows: 7rem minmax(10rem, 1fr); + &__heading { + padding: unset; + } + &__header { + &-steps { + padding-bottom: 0.8rem; + width: 100%; + &-title { + color: var(--brand-red-coral); + font-size: 1.4rem; + font-weight: bold; + line-height: 20px; + padding: 1.6rem 2.4rem 0.8rem; + } + &-subtitle { + color: var(--text-prominent); + line-height: 18px; + font-size: 1.2rem; + font-weight: bold; + padding: 0 2.4rem; + } + } + } + .dc-form-submit-button { + background-color: var(--color-white); + } + } + .dc-autocomplete { + margin-bottom: 2.4rem; + } + .account-management { + &__message-icon { + display: flex; + justify-content: center; + align-items: center; + } + &__message-content { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: auto; + padding: 0 3rem; + padding-bottom: 7rem; + @include mobile { + overflow-y: auto; + } + } + &__message { + @include typeface(--title-center-bold-black); + color: var(--text-prominent); + margin-bottom: 1rem; + } + &__text { + @include typeface(--paragraph-center-normal-black); + color: var(--text-prominent); + } + &__continue { + margin-top: 3rem; + } + &__list { + &-message { + display: flex; + } + &-icon { + margin-right: 1.2rem; + } + } + } +} + +.dc-modal { + &__container { + min-width: initial; + &_cfd-password-modal { + &__description { + @include typeface(--xsmall-left-normal-black); + line-height: 1.5; + max-width: calc(min(100vw, 349px)); + margin: -2.7rem 0 2.4rem; + color: var(--text-less-prominent); + } + &__account-title { + margin-bottom: 1.6rem; + } + } + &_cfd-dashboard__compare-accounts { + width: 904px; + } + &_cfd-password-manager__modal { + height: 100% !important; + min-height: calc(100vh - 22rem); + width: 904px; + max-height: calc(100vh - 48px - 36px) !important; + } + } +} + +.account-poa { + &__upload { + &-remove-btn { + position: absolute; + width: 16px; + height: 16px; + top: 8px; + right: 8px; + cursor: pointer; + transition: transform 0.25s linear; + + &:hover { + transform: scale(1.25, 1.25); + } + &--error { + circle { + fill: var(--status-danger); + } + } + &-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + } +} + +.cfd-proof-of-address { + height: 100%; + & .details-form { + display: grid; + grid-template-rows: minmax(10rem, 1fr) 7.2rem; + height: 100%; + } + &__file-upload { + padding-top: 0.8rem; + position: relative; + + .account-poa { + &__upload { + &-section { + margin-top: 0; + display: flex; + } + &-file { + width: 100%; + flex: 1; + height: 24rem; + position: relative; + margin: 0; + + .dc-file-dropzone { + border: 1px dashed var(--border-normal); + max-width: 40rem; + + &__message-subtitle { + font-size: unset; + font-weight: unset; + } + } + @include mobile { + flex: unset; + } + } + &-list { + display: unset; + + .account-poa__upload-box { + display: flex; + flex-direction: unset; + flex: unset; + align-items: center; + justify-content: flex-start; + margin: 0 1.6rem 0.8rem 0; + border: none; + border-radius: 0; + padding: 0; + text-align: unset; + } + } + &-icon { + width: 2.5rem; + margin-bottom: 0; + } + &-item { + min-width: 23.8rem; + width: 100%; + margin-left: 1rem; + font-size: var(--text-size-xxs); + line-height: 1.5; + color: var(--text-prominent); + padding: 0; + } + } + } + } + &__field-area { + max-width: 556px; + margin: 0 auto; + + & .dc-dropdown__display-placeholder-text, + & .dc-input__label, + & .dc-dropdown__display { + background: var(--general-main-2); + } + @include mobile { + overflow: auto; + } + } + &__inline-fields { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-gap: 1rem; + + .dc-dropdown-container { + margin-top: 0; + } + } + @include mobile { + .dc-input__label { + background: var(--general-section-2); + } + &__field-area { + padding: 0 1.6rem 3.2rem; + max-height: calc(100% - 0.8rem); + } + &__inline-fields { + display: flex; + flex-direction: column; + + .dc-select-native { + margin-bottom: 3.2rem; + } + } + &__file-upload { + .dc-field--error { + top: 50%; + left: 0; + right: unset; + } + .account-poa__upload-section { + flex-direction: column; + } + } + } +} + +.cfd-proof-of-identity { + height: 100%; + overflow: auto; + + &__fields { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + + @include mobile { + display: unset; + position: relative; + } + + .proof-of-identity { + &:is(span) { + width: unset; + height: unset; + } + + &__footer { + width: 45%; + display: inline-flex; + justify-content: flex-end; + height: unset; + position: unset; + bottom: unset; + left: unset; + padding: unset; + z-index: unset; + border-radius: unset; + border-top: unset; + background-color: unset; + align-items: unset; + flex-direction: unset; + + @include mobile { + width: 95%; + margin-top: 8px; + } + + &-alert { + margin-right: unset; + } + } + + &__container { + display: flex; + flex-direction: column; + align-items: center; + width: 45%; + justify-content: unset; + + @include mobile { + width: 100%; + padding: 0 1.2rem; + overflow-y: unset; + justify-content: unset; + } + + .icon { + width: 12.8rem; + height: 12.8rem; + margin: 1.6rem 0 2.4rem 0; + } + .dc-input__bottom-label { + margin: unset; + } + .btm-spacer { + margin-bottom: 1.6rem; + } + .top-spacer { + margin-top: 1.6rem; + } + + .proof-of-identity__footer { + // for cases when __footer is a child of proof-of-identity__container: + width: 100%; + margin-bottom: 8.6em; + + span.dc-text.dc-btn__text { + display: inline-flex; + align-items: center; + } + .back-btn { + margin-right: unset; + + &-icon { + margin-right: 0.8rem; + } + } + } + } + + &__header { + margin: 0 0 1.6rem; + } + &__country-text { + text-align: center; + margin-bottom: 1.6rem; + } + &__inner-container { + display: unset; + flex-direction: unset; + justify-content: unset; + align-items: unset; + width: 100%; + } + &__image { + width: 100%; + max-width: unset; + border-radius: unset; + object-fit: unset; + + &-container { + width: unset; + height: unset; + padding: unset; + border-radius: unset; + background-color: unset; + } + } + &__fieldset { + width: 100%; + + @include mobile { + margin: 0 0 1.8rem; + } + + &-container { + display: unset; + flex-direction: unset; + justify-content: unset; + align-items: unset; + } + &-input { + width: unset; + } + } + &__sample-container { + margin-top: 2.4rem; + margin-left: unset; + width: unset; + } + + &__submit-button { + margin-left: 0.8rem; + @include mobile { + margin-right: unset; + } + } + } + .dc-themed-scrollbars { + height: 100%; + } + } + & .details-form { + display: grid; + grid-template-rows: minmax(10rem, 1fr) 8.2rem; + height: 100%; + position: relative; + + @include mobile { + max-height: calc(100% - 1rem); + } + } + @include mobile { + overflow: hidden; + } +} + +.cfd-change-password { + &__description { + margin-bottom: 2.4rem; + } + + &__icon { + margin-left: 2.3rem; + margin-bottom: 1.4rem; + flex: none; + } + + &-confirmation { + &__description { + margin-top: 0.8rem; + margin-bottom: 2.4rem; + } + } +} + +.cfd-trading-password { + margin-top: 3.2rem; +} + +.dc-modal__container_cfd-financial-stp-signup-modal { + max-height: calc(100vh - 102px) !important; + @include desktop { + display: grid; + grid-template-rows: 6rem minmax(20rem, 1fr); + } +} +.dc-modal__container_cfd-password-modal { + .dc-form-submit-button { + background-color: unset; + padding-bottom: 1.2rem; + + @include mobile { + border-top: none; + bottom: 0; + } + .dc-btn:first-child { + margin-left: 0; + } + } + .dc-input { + &__label { + top: 0.9rem; + } + &__hint { + top: 4.8rem; + } + } +} +.dc-modal__container_cfd-pending-dialog, +.dc-modal__container_cfd-success-dialog { + .dc-modal-header__title--cfd-pending-dialog { + text-align: center; + margin: auto; + padding: 2.4rem; + width: fit-content; + } + .dc-modal-body { + text-align: center; + } + .dc-modal-footer { + justify-content: center; + padding: 0.8rem 2rem 2.4rem; + + .dc-btn { + min-width: 90px; + + &:only-child { + margin: 0; + } + &:first-child { + margin-left: 0; + } + } + @include mobile { + padding: 0.8rem 1.6rem 1.6rem; + display: flex; + grid-gap: 0.6rem; + + & .dc-btn { + &:only-child { + max-width: 14rem; + width: auto; + min-width: 9rem; + } + } + } + } +} +.dc-modal__container_cfd-success-dialog { + .success-change__icon-area { + width: 128px; + margin: 0 auto; + position: relative; + + &--large .bottom-right-overlay { + position: absolute; + left: 76px; + top: 76px; + + @include mobile { + left: 78px; + top: 58px; + } + } + &--xlarge .bottom-right-overlay { + position: absolute; + bottom: 0.8rem; + right: -2.8rem; + height: 5.2rem; + width: 5.2rem; + } + } + .dc-modal-body { + .dc-modal-header__title { + text-align: center; + margin: auto; + padding: 1.6rem 0 0.8rem; + width: fit-content; + } + @include mobile { + font-size: 1.4rem; + } + } + .cfd-account__platform { + font-style: italic; + white-space: nowrap; + } +} +.cfd-personal-details-form-error { + @include typeface(--paragraph-left-normal-red); + @include desktop { + white-space: nowrap; + display: flex; + align-items: flex-end; + } +} + +/* stylelint-enable */ diff --git a/packages/reports/src/sass/app/modules/mt5/cfd.scss b/packages/reports/src/sass/app/modules/mt5/cfd.scss new file mode 100644 index 000000000000..23b3e5ef71da --- /dev/null +++ b/packages/reports/src/sass/app/modules/mt5/cfd.scss @@ -0,0 +1,394 @@ +.dc-modal__container_cfd-password-modal, +.dc-mobile-dialog__cfd-password-modal { + display: flex; + flex-direction: column; + justify-content: flex-start; + max-width: 424px; + + form { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + padding: 0 2.4rem 1.2rem; + + @include mobile { + padding: 0; + } + } + &__description { + @include typeface(--small-center-normal-black); + color: var(--text-prominent); + text-transform: none; + margin-top: 1rem; + top: 2rem; + position: relative; + padding: 1.3rem; + } + &__hint { + max-width: 40rem; + } + &__body { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + } + .dc-input { + &__label { + top: 1.2rem; + @include mobile { + top: 0.9rem; + background-color: var(--fill-normal); + } + } + } + .dc-password-meter { + &__bg { + background: var(--general-section-2); + @include mobile { + background: var(--fill-disabled); + } + } + } + .status-dialog { + margin-top: 3em; + } + .cfd-password-reset { + @include mobile { + flex: 1; + } + } + & .dc-password-meter__container { + flex-grow: 1; + margin: auto; + + @include mobile { + width: calc(100vw - 4.8rem); + max-width: 30rem; + } + } + & .mt5-password-field { + margin-bottom: 1em; + width: 80%; + } + .input-element { + width: 100%; + } + @include mobile { + .cfd-password-modal__content { + overflow-y: auto; + overflow-x: hidden; + height: 100%; + max-height: calc(100% - 6.4rem); + padding: 1rem; + } + } + @include tablet-up { + .cfd-password-modal__content { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 2rem; + + &--password-reset { + padding: 0 2.5rem; + } + } + } +} +.dc-mobile-dialog__mt5-email-sent { + padding-bottom: 1rem; +} + +.cfd-password-modal { + &__warning { + padding: 2.5rem; + max-width: 50rem; + align-self: center; + &-text { + border-radius: 0.8rem; + padding: 0.8rem; + background-color: var(--status-warning); + } + } + &__message { + max-width: 32rem; + margin: auto; + line-height: 1.43; + } + &__radio { + &.dc-radio-group { + display: grid; + grid-gap: 2rem; + margin-top: 0; + padding-bottom: 2.4rem; + } + &.dc-radio-group__item { + display: flex; + flex-direction: row; + } + } + &__create-password { + padding: 0 1.2rem; + + &-content { + display: flex; + flex-direction: column; + width: 100%; + align-items: center; + margin-top: -2.5rem; + + .dc-icon { + margin-left: 1.1rem; + flex: none; + } + } + &-title { + margin-top: 2.4rem; + margin-bottom: 0.8rem; + } + &-description { + max-width: 30rem; + margin-bottom: 1.6rem; + } + } + &__change-password-confirmation { + display: flex; + align-items: center; + flex-direction: column; + max-width: 30rem; + + @include desktop { + margin-top: -2.5rem; + } + + &-wrapper { + display: flex; + justify-content: center; + width: 100%; + } + .dc-form-submit-button { + margin-bottom: 2.2rem; + } + } +} + +.dc-modal__container_top-up-virtual { + width: 384px; + min-height: 367px; + display: flex; + flex-direction: column; + + &__body { + display: flex; + flex-direction: column; + padding: 4.8rem 4.2rem; + flex-grow: 1; + justify-content: space-around; + align-items: center; + } + &__description { + text-transform: none; + } + &--h4 { + @include typeface(--small-center-bold-black); + text-transform: none; + color: var(--text-prominent); + } + &--balance { + @include typeface(--title-center-bold-black); + line-height: 1.5; + color: var(--text-profit-success); + } +} + +.cfd-success-topup { + &__heading { + @include typeface(--paragraph-center-bold-black); + margin: 1.6rem 1.8rem; + } + &__description { + p { + @include typeface(--small-center-normal-black); + color: var(--text-general); + text-transform: none; + } + } +} + +.cfd-dashboard { + &__container { + & .dc-popover__target { + display: flex; + } + } +} + +.cfd-verification-email-sent { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + &__title { + @include typeface(--title-center-bold-black); + margin: 0.8rem 0; + + &--sub { + margin-bottom: 0.8rem; + margin-top: 3rem; + } + } + &__resend-button { + display: block; + @include typeface(--paragraph-center-bold-black); + color: var(--brand-red-coral); + text-decoration: none; + margin: 3.2rem 0 0; + } + & .countdown { + margin: 0 0.4rem; + } +} + +.cfd-reset-password { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + @include typeface(--paragraph-left-normal-black); + color: var(--text-prominent); + + &__container { + display: grid; + grid-template-rows: 12rem 7rem; + height: 100%; + width: 100%; + justify-items: center; + flex-grow: 1; + + @include mobile { + min-height: 24rem; + width: calc(100vw - 3.2rem); + } + } + &__heading { + @include typeface(--paragraph-center-bold-black); + margin: 1.6rem 0 0; + } + &__hint { + @include typeface(--small-left-normal-black); + color: var(--text-less-prominent); + margin-left: 1.2rem; + } + &__password-field { + margin-bottom: 0.5rem; + + & .dc-input__label { + top: 0.9rem; + } + } + &__password-area { + @include desktop { + width: calc(min(33rem, 100vw)); + padding: 2.4rem; + } + @include mobile { + width: 100%; + max-width: calc(100% - 2.4rem); + } + } + &__error { + display: flex; + max-width: 38.4rem; + min-height: 28.4rem; + flex-direction: column; + align-items: center; + padding-top: 2rem; + margin: 0 auto; + } + &__description { + @include typeface(--paragraph-left-normal-black); + color: var(--text-prominent); + margin-bottom: 2.4rem; + + &--is-centered { + margin-bottom: 2.4rem; + max-width: 70%; + } + } + &__confirm-button { + margin-top: 2.4rem; + } + &__success { + width: 38.4rem; + height: 28.4rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; + padding: 2.4rem 0 2rem; + + @include mobile { + width: calc(100vw - 3.2rem); + height: 100%; + } + } + @include mobile { + & .dc-form-submit-button { + all: unset; + } + } +} + +/** @define poi-icon-row; weak */ +.poi-icon-row { + display: flex; + margin-bottom: 16px; + + &__icon-container { + align-items: center; + display: flex; + flex-direction: column; + color: var(--text-less-prominent); + + &:not(:first-child):not(:last-child) { + margin: 0 40px; + } + p { + font-size: var(--text-size-xxs); + } + p:first-of-type { + line-height: 1.5; + margin-top: 8px; + font-weight: bold; + } + @include mobile { + &:not(:first-child):not(:last-child) { + margin: 0; + } + & .dc-icon { + width: 8.8rem; + height: auto; + + /* iPhone SE screen width fixes due to UI space restrictions */ + @media only screen and (max-width: 340px) { + width: 7rem; + } + } + p { + line-height: 20px; + } + } + } + @include mobile { + display: grid; + grid-gap: 2.4rem; + grid-template-columns: 1fr 1fr 1fr; + margin-top: 4rem; + } +} diff --git a/packages/reports/src/sass/app/modules/portfolio.scss b/packages/reports/src/sass/app/modules/portfolio.scss new file mode 100644 index 000000000000..e95e64667de9 --- /dev/null +++ b/packages/reports/src/sass/app/modules/portfolio.scss @@ -0,0 +1,87 @@ +// TODO: Combine whatever selector / rule that's still needed from this and merge into positions_drawer +/** @define portfolio; weak */ +.portfolio { + padding: 1.5em 1.2em; + height: 100%; + + &--card-view { + background: var(--general-main-2); + } + &__table { + height: 100%; + } + &__row { + grid-template-columns: 6.5em 7em 1fr 6em 9em 6em 5em; + } + /* postcss-bem-linter: ignore */ + .payout, + .indicative, + .purchase, + .remaining_time { + justify-content: flex-end; + } + .container { + background: var(--general-section-1); + } + .contract-type { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + + .type-wrapper { + width: 2em; + height: 2em; + padding: 0.5em; + margin-bottom: 0.3em; + border-radius: 100%; + display: flex; + justify-content: center; + align-items: center; + background: var(--state-normal); + } + } + .reference a { + color: $COLOR_SKY_BLUE; + text-decoration: none; + } + .table__body .indicative { + font-weight: bold; + } + .indicative { + text-align: right; + + &--price-moved-up { + color: var(--text-profit-success); + } + &--price-moved-down { + color: var(--text-loss-danger); + } + &--no-resale { + color: var(--text-general); + } + } +} + +// empty portfolio message +/** @define portfolio-empty */ +.portfolio-empty { + height: calc(100vh - 240px); + display: flex; + + &__wrapper { + display: flex; + flex-direction: column; + align-self: center; + justify-content: center; + align-items: center; + width: 100%; + } + &__icon { + @extend %inline-icon.disabled; + height: 4.8em; + width: 4.8em; + margin-bottom: 1.6em; + } +} diff --git a/packages/reports/src/sass/app/modules/reports.scss b/packages/reports/src/sass/app/modules/reports.scss new file mode 100644 index 000000000000..2745e324883a --- /dev/null +++ b/packages/reports/src/sass/app/modules/reports.scss @@ -0,0 +1,983 @@ +@import '../_common/components/composite-calendar'; + +$side-padding: 1.2em; + +/** @define reports; weak */ +.reports { + height: 100%; + + &__meta { + width: 100%; + display: flex; + justify-content: space-between; + padding: 0 2.4rem 1.6rem 0; + + @include mobile { + padding: 0 1.6rem 1.6rem; + } + + flex-direction: column-reverse; + padding-bottom: 0; + + &-filter { + position: relative; + display: flex; + width: 100%; + @include desktop { + max-width: 36rem; + margin-left: auto; + } + &--statement { + @include desktop { + max-width: 50rem; + } + } + } + + @include desktop { + align-items: center; + } + + @include mobile { + flex-direction: column; + padding-bottom: 0; + + &-filter { + padding: 0 0 1.6rem; + } + #dt_calendar_input { + text-align: left; + padding-left: 3rem; + } + } + } + &__mobile-wrapper { + display: flex; + flex-direction: column; + width: 100%; + } + &__route-selection { + padding: 1.6rem; + } + &__content { + width: 100%; + display: flex; + flex: 1; + flex-direction: column; + @media only screen and (min-width: 1280px) { + overflow: visible; + } + } + .unknown-icon { + margin-left: 8px; + fill: var(--text-general); + border-radius: $BORDER_RADIUS; + } + /* postcss-bem-linter: ignore */ + .dc-tabs--open-positions { + flex: 1; + grid-template-rows: 36px calc(100% - 36px); + grid-template-columns: 100%; + + .dc-tabs__content { + display: flex; + height: 100%; + } + } + /* postcss-bem-linter: ignore */ + .statement__row--detail { + overflow: hidden; + min-height: 63px; + display: flex; + align-items: center; + padding: 0; + justify-content: center; + background-color: var(--general-section-1); + + /* postcss-bem-linter: ignore */ + &-text { + padding: 1.12em; + word-break: break-all; + + .dc-popover__wrapper { + display: inline-block; + margin-left: 1rem; + } + } + } + .dc-vertical-tab__content--floating { + margin-right: 0; + } + .table__head { + height: auto; + .table__cell { + @include tablet-up { + white-space: break-spaces; + } + } + } +} + +/** @define reports-page-wrapper; weak */ +.reports-page-wrapper { + height: 100%; +} + +/** @define statement; weak */ +.statement { + .table__head { + font-weight: bold; + align-items: flex-start; + height: auto; + + .table__cell { + height: auto; + } + @include desktop { + white-space: normal; + } + } + + @include desktop { + height: 100%; + } + @include mobile { + flex: 1; + + &__data-list-body { + height: 100%; + + .action_type { + display: flex; + flex: none; + align-items: center; + + &__row-title { + display: none; + } + } + .balance { + display: flex; + + &__row-title { + flex: 50%; + } + } + } + } + + &__content { + width: 100%; + max-height: 100%; + } + /* + TABLE STYLES + */ + &__table { + height: calc(100% - 42px); + flex: 1; + min-width: 85rem; + } + &__row { + /* icon refId currency tr_time transaction cred/debt balance */ + /* stylelint-disable-next-line declaration-colon-space-after */ + grid-template-columns: + minmax(120px, 0.8fr) minmax(85px, 1.4fr) minmax(110px, 1.2fr) minmax(85px, 1.2fr) minmax(85px, 1fr) + minmax(85px, 1.2fr) minmax(85px, 1fr); + + .date { + text-align: left; + } + } + .amount, + .balance { + justify-content: flex-end; + } + .amount { + font-weight: bold; + + &--profit { + color: var(--text-profit-success); + } + &--loss { + color: var(--text-loss-danger); + } + } + .market-symbol-icon { + @include mobile { + width: 80px; + } + } + /* + MOBILE CARDS + */ + &--card-view { + background: var(--general-main-2); + overflow: hidden; + + .statement__filter { + padding: 0 $side-padding; + border-bottom: 1px solid var(--general-section-1); + + &-content { + padding: 0; + margin: 0 auto; + max-width: 450px; + display: grid; + grid-template-columns: 1fr 3em 1fr; + text-align: center; + + .datepicker__input-field { + width: 100%; + } + } + &-label { + display: none; + } + } + .statement__content { + padding: 0; + } + .statement__card-list { + padding: 0 $side-padding; + height: 100%; + } + } + &__statement-header { + justify-content: flex-end; + } + &__account-statistics { + background-color: var(--general-section-1); + @include desktop { + margin: 1.6rem 0; + width: 100%; + } + @include mobile { + margin: 0.8rem 0 1.6rem; + order: 1; + } + height: 4.8rem; + display: flex; + flex-direction: row; + text-align: left; + + &-item { + flex: 1; + display: flex; + + &:last-child { + border-right: unset; + } + &:first-child { + .statement__account-statistics--is-rectangle { + padding-left: 0; + } + } + &:not(:first-child) { + justify-content: center; + } + } + &-total-withdrawal { + @include desktop { + min-width: 19rem; + } + @include mobile { + min-width: 12.3rem; + } + } + &--is-rectangle { + height: 100%; + display: flex; + justify-content: center; + margin: auto; + + @include desktop { + padding: 0.4rem 1.6rem; + } + @include mobile { + flex-direction: column; + } + } + &-title { + margin: auto; + + @include mobile { + font-size: 1rem; + margin-bottom: 0; + } + } + &-amount { + margin: auto; + + @include desktop { + margin-left: 1rem; + } + @include mobile { + font-size: 1.4rem; + margin-top: 1rem; + } + } + } +} + +/** @define statement-card */ +.statement-card { + &__header { + font-size: 1em; + padding: 0.5em; + border-bottom: 1px solid var(--general-section-1); + display: flex; + justify-content: space-between; + } + /* postcss-bem-linter: ignore */ + &__refid a { + color: $COLOR_SKY_BLUE; + text-decoration: none; + } + &__body { + padding: 0.5em; + font-size: 1.2em; + } + &__desc { + margin-bottom: 0.7em; + } + &__row { + display: grid; + grid-template-columns: repeat(3, 1fr); + font-weight: bold; + } + &__cell-text { + vertical-align: middle; + } + &__amount { + &--sell, + &--deposit { + color: var(--text-profit-success); + } + &--buy, + &--withdrawal { + color: var(--text-loss-danger); + } + } + &__icon { + display: inline-block; + height: 1.6em; + width: 1.6em; + background-size: 1.6em 1.6em; + vertical-align: middle; + margin-right: 0.5em; + } +} + +/** @define statement-empty */ +.statement-empty { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + &__icon { + height: 6.4em; + width: 6.4em; + margin-bottom: 1.4em; + } + &__text { + font-size: 1.4em; + } +} + +/** @define profit-table; weak */ +.profit-table { + .table__head, + .table__foot { + font-weight: bold; + } + .table__head { + white-space: normal; + .table__cell { + align-items: flex-start; + } + } + + @include desktop { + height: 100%; + } + + @include mobile { + flex: 1; + + &__data-list-body { + height: calc(100% - 50px); + + .sell_time__row-title { + display: flex; + align-items: center; + + .dc-icon { + margin-left: 4px; + } + } + } + &__data-list-footer { + height: 50px; + min-height: 50px; + font-weight: bold; + + .data-list__row__content { + font-size: 1.2rem; + color: var(--text-prominent); + } + .data-list__row { + padding: 0; + } + } + } + + &__content { + width: 100%; + max-height: 100%; + } + /* + TABLE STYLES + */ + &__table { + height: calc(100% - 42px); + flex: 1; + min-width: 90rem; + } + &__row { + /* icon refId currency buy_time buy_price sell_time sell_price profit/loss */ + /* stylelint-disable-next-line declaration-colon-space-after */ + grid-template-columns: + minmax(120px, 0.6fr) minmax(130px, 1fr) minmax(85px, 1fr) minmax(85px, 1.2fr) minmax(85px, 1fr) + minmax(85px, 1.2fr) minmax(95px, 1fr) minmax(130px, 1fr); + } + .buy_price, + .sell_price, + .profit_loss { + @include desktop { + justify-content: flex-end; + text-align: right; + } + @include mobile { + justify-content: center; + } + } + .sell_time, + .purchase_time { + text-align: left; + min-width: 120px; + } + .profit_loss { + font-weight: bold; + @include tablet-up { + word-break: break-word; + } + @include mobile { + display: flex; + + &__row-title { + flex: 50%; + } + } + } + .market-symbol-icon { + @include mobile { + width: 80px; + } + } + .duration-type { + flex: none; + position: relative; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + padding: 0px 16px; + font-size: 1.4rem; + font-weight: bold; + + &__background { + position: absolute; + height: 100%; + width: 100%; + opacity: 0.16; + border-radius: 16px; + } + &__ticks { + color: $color-yellow; + + &__background { + background: $color-yellow; + } + } + &__seconds { + color: $color-green-1; + + &__background { + background: $color-green-1; + } + } + &__minutes { + color: $color-blue-1; + + &__background { + background: $color-blue-1; + } + } + &__hours { + color: $COLOR_BLUE; + + &__background { + background: $COLOR_BLUE; + } + } + &__days { + color: $color-purple; + + &__background { + background: $color-purple; + } + } + } +} + +/** @define open-positions; weak */ +.open-positions { + height: 100%; + + @include mobile { + flex: 1; + padding-top: 0.8rem; + + &-multiplier { + /* postcss-bem-linter: ignore */ + & .data-list__item { + background-color: var(--general-section-1); + border-radius: $BORDER_RADIUS; + border: 1px solid var(--border-disabled); + padding: 0; + + /* postcss-bem-linter: ignore */ + & .dc-progress-slider--completed { + display: none; + } + /* postcss-bem-linter: ignore */ + & .dc-contract-card { + background-color: var(--general-main-2); + /* postcss-bem-linter: ignore */ + &__wrapper { + background-color: var(--general-main-2); + max-width: unset; + margin: 0; + } + /* postcss-bem-linter: ignore */ + &-item__footer { + background-color: var(--general-main-2); + border-radius: $BORDER_RADIUS; + } + /* postcss-bem-linter: ignore */ + &__grid-underlying-trade { + border-bottom: 1px solid var(--border-disabled); + margin-bottom: 5px; + } + /* postcss-bem-linter: ignore */ + &__grid-items { + grid-template-columns: 1fr 1fr 1fr; + margin-top: 0.4rem; + margin-bottom: 0.4rem; + } + /* postcss-bem-linter: ignore */ + &__sell-button { + border-top: 1px solid var(--border-disabled); + } + /* postcss-bem-linter: ignore */ + &-item { + /* postcss-bem-linter: ignore */ + &__total-profit-loss { + border-color: var(--border-disabled); + } + /* postcss-bem-linter: ignore */ + &:nth-child(1) { + order: 0; + } + /* postcss-bem-linter: ignore */ + &:nth-child(3), + &:nth-child(5) { + order: 2; + } + /* postcss-bem-linter: ignore */ + &:nth-child(6) { + order: 6; + } + } + } + /* postcss-bem-linter: ignore */ + & .dc-contract-card-dialog-toggle { + border-color: var(--border-disabled); + } + } + /* postcss-bem-linter: ignore */ + & .open-positions__data-list-body { + padding: 0; + height: calc(100% - 48px); + } + /* postcss-bem-linter: ignore */ + & .open-positions__data-list-footer { + height: 48px; + min-height: 48px; + font-weight: bold; + align-items: center; + padding: 0; + + /* postcss-bem-linter: ignore */ + &--content { + padding: 0 1.6rem; + display: grid; + grid-template-columns: 0.7fr 1fr; + + /* postcss-bem-linter: ignore */ + .profit { + align-items: flex-start; + } + } + } + } + &__data-list { + margin-top: 0.8rem; + } + &__data-list-body { + height: calc(100% - 95px); + + .dc-progress-bar__container { + max-width: 120px; + align-self: center; + } + } + &__data-list-footer { + height: 95px; + min-height: 95px; + font-weight: bold; + align-items: flex-start; + padding: 0.8rem 4rem 0 1rem; + + &--title { + font-size: 1.4rem; + font-weight: bold; + color: var(--text-prominent); + } + &--content { + flex: 1; + padding: 0.8rem 1.6rem 0; + display: flex; + justify-content: space-between; + + .purchase, + .indicative { + padding-bottom: 8px; + } + } + .data-list__row-title { + font-size: 1rem; + line-height: 1.4rem; + } + .data-list__row-content { + font-size: 1rem; + line-height: 1.4rem; + color: var(--text-prominent); + } + } + .dc-contract-card__no-resale-msg { + display: flex; + font-size: 1.4rem; + color: var(--text-general); + justify-content: center; + padding: 0.8rem 0rem; + } + } + + &__content { + width: fit-content; + max-height: 100%; + } + &__indicative { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + width: 100%; + + &--amount { + display: flex; + align-items: center; + @include desktop { + line-height: 2; + } + } + .dc-btn--sell { + height: 2.4rem; + } + &-no-resale-msg { + clear: both; + text-align: center; + font-size: smaller; + } + } + &__profit-loss { + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + + &--movement { + width: 16px; + height: 16px; + + &-complete, + &-complete:after { + display: none; + } + &:after { + content: ''; + width: 16px; + } + } + &--negative { + color: var(--text-loss-danger); + + &:before { + content: '-'; + color: inherit; + } + } + &--positive { + color: var(--text-profit-success); + + &:before { + content: '+'; + color: inherit; + } + } + } + /* + TABLE STYLES + */ + &__table { + height: calc(100% - 24px); + flex: 1; + margin-top: 20px; + + .table__head { + height: auto; + white-space: normal; + @include tablet-up { + .profit, + .indicative { + white-space: break-spaces; + } + } + + .table__cell { + font-weight: bold; + align-items: flex-start; + } + } + .table__body { + .open-positions__row_wrapper { + border-bottom: 1px solid var(--general-section-1); + } + } + .table__foot { + font-weight: bold; + white-space: normal; + } + } + &__row { + /* type refId currency buy_price payout_limit indicative_profit/loss indicative_price rem_time */ + /* stylelint-disable-next-line declaration-colon-space-after */ + grid-template-columns: + minmax(110px, 0.7fr) minmax(130px, 0.8fr) minmax(100px, 1.1fr) minmax(90px, 1.1fr) minmax(90px, 1.1fr) + minmax(120px, 1.1fr) minmax(120px, 1.1fr) minmax(85px, 1.1fr); + width: 100%; + grid-auto-rows: 100%; + + &_wrapper { + display: flex; + flex-direction: row; + height: 100%; + } + } + &__reports-meta { + @include mobile { + padding-bottom: 0px; + } + } + .buy_price, + .payout, + .indicative, + .purchase, + .multiplier, + .currency, + .profit { + @include desktop { + justify-content: center; + } + } + .type { + padding-right: 0; + } + .dc-progress-slider { + border: none; + margin: 0; + + &__ticks { + display: flex; + align-items: center; + justify-content: space-between; + + &-step { + background: var(--state-hover); + } + &-wrapper { + margin-top: 6px; + } + &-caption { + padding: 0; + margin-right: 8px; + white-space: nowrap; + } + } + } + .market-symbol-icon { + @include mobile { + width: 80px; + } + } +} + +/** @define open-positions-multiplier; weak */ +.open-positions-multiplier { + .open-positions { + &__row { + /* icon multiplier currency stake cancellation buy_price limit_order current_stake total_profit_loss action */ + /* stylelint-disable-next-line declaration-colon-space-after */ + grid-template-columns: + minmax(85px, 1fr) minmax(125px, 1fr) minmax(65px, 1fr) minmax(105px, 1fr) minmax(100px, 1fr) + minmax(105px, 1fr) minmax(105px, 1fr) minmax(105px, 1fr) minmax(125px, 1fr) minmax(90px, 1fr); + + &-action { + display: flex; + flex-direction: column; + flex: 1; + + .dc-remaining-time { + margin-left: 0.4rem; + font-size: inherit; + } + .dc-btn { + height: 2.4rem; + padding: 0 0.8rem; + min-width: 93px; + + .dc-btn__text { + font-size: 1.2rem; + } + &:first-child { + margin-bottom: 0.4rem; + } + } + } + .limit_order, + .cancellation, + .bid_price { + @include desktop { + justify-content: center; + text-align: center; + width: 100%; + white-space: break-spaces; + } + } + .limit_order { + flex-direction: column; + align-items: flex-end; + text-align: right; + justify-content: flex-start; + } + .action { + padding-bottom: 0; + justify-content: center; + } + } + &__bid_price { + font-weight: bold; + + &--negative { + color: var(--text-loss-danger); + } + &--positive { + color: var(--text-profit-success); + } + } + } +} + +.open-positions, +.statement, +.profit-table { + /* postcss-bem-linter: ignore */ + .data-list__body, + .data-list__footer { + padding: 0 1.6rem; + } + /* postcss-bem-linter: ignore */ + .data-list__item { + background-color: var(--general-section-1); + } + .currency { + &__wrapper { + background: var(--border-active); + border-radius: $BORDER_RADIUS; + padding: 0 0.4rem; + } + } +} + +/** @define empty-trade-history; weak*/ +.empty-trade-history { + position: absolute; + top: 20%; + left: 10%; + width: 50%; + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + margin: auto; + + @media only screen and (max-width: 769px) { + position: static; + width: 50%; + } + + &__icon { + width: 96px; + height: 96px; + margin-bottom: 16px; + @include colorIcon(var(--text-disabled)); + } + &__text { + line-height: 20px; + } + .dc-btn { + width: 100%; + height: 40px; + border: 1px solid var(--button-secondary-default); + color: var(--text-general); + background: transparent; + + &:hover { + background: var(--button-secondary-hover); + } + } +} diff --git a/packages/reports/src/sass/app/modules/smart-chart.scss b/packages/reports/src/sass/app/modules/smart-chart.scss new file mode 100644 index 000000000000..3ae7a4689178 --- /dev/null +++ b/packages/reports/src/sass/app/modules/smart-chart.scss @@ -0,0 +1,247 @@ +/** @define chart-spot */ +.chart-spot { + display: flex; + flex-direction: column; + + &__spot { + position: absolute; + bottom: -11px; + margin-left: -11.5px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + color: var(--text-prominent); + background-color: var(--general-main-1); + border: 2px solid var(--text-less-prominent); + + &--lost { + border-color: var(--text-loss-danger); + background: var(--text-loss-danger); + } + &--won { + background-color: var(--text-profit-success); + border-color: var(--text-profit-success); + } + &--won, + &--lost { + color: $COLOR_WHITE; + } + @include mobile { + bottom: -9.5px; + margin-left: -8px; + font-size: 0.8rem; + } + } + &__entry { + left: -12px; + top: -12px; + position: relative; + border: 6px solid var(--brand-red-coral); + background-color: var(--general-main-2); + + @include mobile { + border-width: 3px; + left: -9px; + top: -9px; + } + } + &__spot, + &__entry { + width: 24px; + height: 24px; + border-radius: 50%; + + @include mobile { + width: 16px; + height: 16px; + } + } + &__sell { + border-radius: 50%; + width: 2px; + height: 2px; + margin-left: -2px; + margin-top: -2px; + background-color: var(--text-prominent); + border: 2px solid var(--text-prominent); + } +} + +/** @define chart-spot-label */ +.chart-spot-label { + &__info-container { + width: 100%; + position: relative; + } + &__time-value-container { + position: absolute; + transform: translateX(-50%); + display: flex; + flex-direction: column; + + &--top { + bottom: 21px; + + .chart-spot-label__time-container { + margin-bottom: 2px; + } + @include mobile { + bottom: 9.5px; + } + } + &--bottom { + top: 18px; + flex-direction: column-reverse; + + .chart-spot-label__time-container { + margin-top: 2px; + } + @include mobile { + top: 7.5px; + } + } + } + &__time-container { + display: flex; + align-items: center; + justify-content: center; + padding: 0px 8px; + + /* postcss-bem-linter: ignore */ + svg { + /* postcss-bem-linter: ignore */ + g { + stroke: var(--text-prominent); + } + } + } + &__time-icon { + margin-right: 2px; + } + &__value-container { + background: var(--text-less-prominent); + font-size: 1.4rem; + padding: 4px 8px; + border-radius: 11px; + + /* postcss-bem-linter: ignore */ + p { + font-weight: bold; + color: $color-white; + margin-top: 2px; + } + &--lost { + background-color: var(--text-loss-danger); + } + &--won { + background-color: var(--text-profit-success); + } + &--won, + &--lost { + color: var(--text-colored-background); + } + @include mobile { + font-size: 1rem; + display: flex; + justify-content: center; + align-items: center; + padding: 0.2rem; + } + } +} + +/** @define chart-marker-line */ +.chart-marker-line { + height: 94.5%; + margin-bottom: 0.8em; + z-index: 0 !important; + bottom: -100%; + transition: none; + + &__wrapper { + border-left-width: 2px; + padding-left: 0.5em; + height: 100%; + border-color: var(--text-less-prominent); + + &:after { + background: linear-gradient(to bottom, var(--general-main-1) 3%, transparent 10%); + position: absolute; + content: ' '; + top: 0px; + left: -1px; + height: 100%; + width: 3px; + } + } + &__icon { + position: absolute; + bottom: -23px; + left: -11px; + white-space: nowrap; + + &--time { + /* postcss-bem-linter: ignore */ + path { + fill: var(--text-less-prominent); + } + } + &--won { + /* postcss-bem-linter: ignore */ + circle { + fill: var(--text-profit-success); + } + } + &--lost { + /* postcss-bem-linter: ignore */ + circle { + fill: var(--text-loss-danger); + } + } + @include mobile { + bottom: -15px; + left: -7px; + width: 16px; + height: 16px; + } + } + &--solid { + border-left-style: solid; + } + &--dash { + border-left-style: dashed; + } +} + +/** @define sc-toolbar-widget; weak */ +.sc-toolbar-widget { + &--bottom { + .ciq-menu { + margin: 0px; + } + } +} + +/** @define smartcharts-mobile; weak */ +.smartcharts-mobile { + .cq-modal-dropdown { + left: 0px; + top: 0px; + } + .sc-chart-type, + .sc-interval { + .sc-tooltip__inner { + display: none; + } + } + .cq-chart-title .sc-dialog__body { + height: 100% !important; + } + .sc-categorical-display { + height: calc(100% - 38px) !important; + } + .cq-menu-dropdown .title .title-text { + display: inline; + } +} diff --git a/packages/reports/src/sass/app/modules/trading-mobile.scss b/packages/reports/src/sass/app/modules/trading-mobile.scss new file mode 100644 index 000000000000..cc78321b8dfa --- /dev/null +++ b/packages/reports/src/sass/app/modules/trading-mobile.scss @@ -0,0 +1,409 @@ +@include mobile { + /** @define dc-collapsible; weak */ + .dc-collapsible { + &:before { + content: ''; + position: absolute; + pointer-events: none; + opacity: 1; + z-index: -1; + border-radius: $BORDER_RADIUS; + top: 0; + left: 0; + width: 100%; + height: 100%; + transition: opacity 0.25s; + background: var(--general-section-1); + } + &--is-expanded { + background: transparent; + + &:before { + opacity: 0.75; + } + } + } + /** @define barrier; weak */ + .barrier { + .draggable { + pointer-events: none; + + & .price { + padding-left: 0; + + &:after { + content: none; + } + } + } + &__widget { + display: grid; + grid-template-columns: 3.5fr 1fr; + height: 4rem; + margin: 0 0 0.8rem; + background: var(--general-main-1); + border-radius: $BORDER_RADIUS; + + &-title { + font-size: 1.4rem; + font-weight: 400; + text-transform: none; + color: var(--text-less-prominent); + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 0.8rem; + } + } + &__fields { + width: 100%; + + &-input { + width: 100%; + padding: 0; + font-weight: bold; + border: none; + + &--is-offset { + pointer-events: none; + } + &:focus, + &:active, + &:hover { + border: none; + } + } + &-single { + margin: 0; + height: 100%; + display: flex; + align-items: center; + } + & .dc-tooltip { + width: 100%; + } + & .dc-input-wrapper { + &__input { + outline: 0; + border: none; + } + &__button { + transform: scale(1.4); + stroke: var(--text-general); + + &:hover, + &:active { + background: none; + } + } + } + } + } + /** @define allow-equals; weak */ + .allow-equals { + .dc-checkbox__label { + font-weight: bold; + color: var(--text-prominent); + margin-right: 0.8rem; + } + } + /** @define dc-modal; weak */ + .dc-modal { + &__container_trade-params { + border-radius: 2px; + box-shadow: 0 16px 16px 0 var(--shadow-menu-2), 0 0 16px 0 var(--shadow-menu-2); + overflow-y: scroll; + + /* postcss-bem-linter: ignore */ + .dc-relative-datepicker { + margin-top: -0.8rem; + max-width: 110px; + margin-left: auto; + margin-right: auto; + + /* iPhone SE screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 480px) { + margin-top: -4.6rem; + } + } + /* iPhone SE screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 480px) { + top: 0.4rem; + } + } + &-header { + /* postcss-bem-linter: ignore */ + &--trade-params { + line-height: 1.4; + border-bottom-width: 1px; + + /* postcss-bem-linter: ignore */ + .dc-modal-header__close { + padding: 0.8rem 0.8rem 0; + margin: 0.4rem 0.4rem 0.2rem; + } + } + } + } + /** @define dc-tabs; weak */ + .dc-tabs { + /* postcss-bem-linter: ignore */ + &--trade-params__multiplier-tabs { + /* postcss-bem-linter: ignore */ + .dc-tabs__content { + display: flex; + flex-direction: column; + min-height: 400px; + + @media only screen and (max-height: 480px) { + min-height: 360px; + } + } + } + } + /** @define trade-params; weak */ + .trade-params { + &__error-popup { + top: 12rem !important; + opacity: 0.8; + z-index: 2 !important; + + &--has-numpad { + z-index: 9999 !important; + top: 0.8rem !important; + } + } + &__duration { + &-tickpicker { + height: 328px; + + .dc-tick-picker { + max-width: 100%; + height: 100%; + align-items: center; + justify-content: center; + } + } + } + &__amount { + &-keypad { + width: 100%; + padding: 1.6rem; + height: auto; + margin-top: 0.8rem; + margin-bottom: 0.8rem; + display: flex; + align-items: center; + justify-content: center; + + .dc-numpad--is-currency, + .dc-numpad--is-regular { + max-width: 100%; + grid-template-columns: repeat(4, 1fr); + grid-gap: 16px; + } + .dc-numpad__increment, + .dc-numpad__decrement { + height: 48px !important; + + .dc-btn__text { + font-size: 3rem; + font-weight: normal; + } + &[disabled] { + .dc-btn__text { + color: var(--text-disabled); + } + } + } + .dc-numpad__number { + border-radius: 2.4rem; + background-color: var(--general-section-2); + width: 48px; + height: 48px !important; + font-weight: 700; + text-transform: none; + line-height: 1.75; + color: var(--text-prominent); + text-align: left !important; + + &--is-left-aligned { + padding: 0 0 0 0.2rem; + } + } + .dc-numpad__bkspace, + .dc-numpad__ok { + .dc-numpad__number { + height: 100% !important; + } + } + .dc-numpad__bkspace { + .dc-numpad__number { + &[disabled] { + background-color: var(--general-disabled); + } + .dc-text { + font-size: 1.8rem; + /* -webkit-touch-callout only is supported on iOS webkit engine, thus it should apply iOS only styles */ + @supports (-webkit-touch-callout: none) { + @media only screen and (min-width: 360px) { + font-size: 3rem; + } + } + } + } + } + /* iPhone SE screen height fixes due to UI space restrictions */ + @media only screen and (max-height: 480px) { + transform: scale(1, 0.92); + transform-origin: top; + margin-top: -0.4rem; + } + } + } + &__header { + @include mobile { + padding: 0.5rem 0; + + &-label { + line-height: 2rem; + } + &-value { + line-height: 1.8rem; + font-size: 1.2rem; + + &--has-error { + color: var(--status-danger); + font-weight: bold; + } + } + } + } + &__contract-type-container { + display: flex; + + .contract-type-widget { + flex: 1; + } + } + &__multiplier { + &-radio-group { + flex-direction: column; + align-items: flex-start; + padding: 1.6rem; + margin-top: 0rem; + flex: 1; + + &--empty { + display: none; + } + .dc-radio-group__item { + min-height: 4.8rem; + max-height: 4.8rem; + width: 100%; + align-items: center; + margin-bottom: 0.8rem; + padding: 0.8rem; + border-radius: $BORDER_RADIUS; + border: 1px solid var(--border-normal); + font-size: 1.4rem; + flex-direction: row; + + &--selected { + border: 1px solid var(--brand-secondary); + } + } + } + &-amount-text { + padding: 1.6rem 4rem 0; + line-height: 1.4rem; + text-align: center; + color: var(--text-general); + } + &-risk-management-dialog { + display: grid; + grid-template-rows: auto auto auto 1fr; + + &--no-cancel { + grid-template-rows: auto auto 1fr; + } + &-bottom-separator { + border-bottom: 1px solid var(--border-disabled); + height: calc(100% - 1.6rem); + } + &-apply-button { + display: flex; + align-items: flex-end; + margin: 0 1.6rem; + + .dc-btn { + flex: 1; + height: 4rem; + } + } + .trade-container__fieldset { + padding: 1rem 1.6rem; + margin-bottom: 0; + border-bottom: 1px solid var(--border-disabled); + border-radius: 0; + + .dc-input-field { + z-index: 0; + } + .dc-popover { + padding: 0.6rem 1rem; + } + } + .dc-checkbox__box { + margin-left: 0rem; + } + .dc-radio-group { + padding: 1.6rem 0rem; + } + } + &-ic-info-wrapper { + display: flex; + justify-content: flex-start; + position: absolute; + top: 0.6rem; + left: 0.2rem; + z-index: 2; + + .dc-popover { + padding: 0.5rem 1rem; + } + } + &-deal-cancellation-dialog { + .dc-checkbox { + margin-top: 2.6rem; + + .dc-checkbox__box { + margin-left: 0; + } + } + } + &-trade-info { + display: flex; + flex-direction: column; + padding-bottom: 1.6rem; + align-items: center; + + div:nth-child(2) { + padding-top: 1.6rem; + } + + &-tooltip-text { + text-align: right; + border-bottom: 1px dotted var(--text-general); + display: flex; + + *:first-child { + &:before { + content: ': '; + } + } + } + } + } + } +} diff --git a/packages/reports/src/sass/app/modules/trading.scss b/packages/reports/src/sass/app/modules/trading.scss new file mode 100644 index 000000000000..241efc5fef0d --- /dev/null +++ b/packages/reports/src/sass/app/modules/trading.scss @@ -0,0 +1,675 @@ +@keyframes slide { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} + +.loader { + overflow: hidden; + background-color: rgba(var(--general-main-1), 0.16); + + &--loading { + width: inherit; + height: inherit; + animation: slide 1s cubic-bezier(1, 0, 0.5, 0) infinite; + background-image: linear-gradient(to left, rgba(255, 255, 255, 0), $color-white 10%, rgba(255, 255, 255, 0)); + opacity: 0.32; + } +} + +.app-contents { + &--contract-mode, + &--is-disabled { + .trade-container { + .popover { + display: none; + } + .dc-tooltip:before, + .dc-tooltip:after { + display: none; + } + } + } + .sidebar__items--blur { + .popover { + display: none; + } + .dc-tooltip:before, + .dc-tooltip:after { + display: none; + } + } +} + +/** @define modal-dialog; weak */ +.dc-mobile-dialog { + &__contracts-modal-list { + /* postcss-bem-linter: ignore */ + .category-wrapper { + background-color: var(--general-main-1) !important; + } + } + .dc-select-native__placeholder:not(.dc-select-native__placeholder--has-value) { + background: initial; + } +} + +/** @define trade-container; weak */ +.trade-container { + background: var(--general-main-1); + + &__fieldset { + border-radius: $BORDER_RADIUS; + padding: 0.8rem; + margin-bottom: 0.4rem; + background-color: var(--general-section-1); + border-color: var(--general-section-1); + color: var(--text-general); + @include mobile { + padding: 0; + margin-bottom: 0.8rem; + background-color: transparent; + } + + /* postcss-bem-linter: ignore */ + > div:first-child { + margin-top: 0; + } + &-header { + position: relative; + + &--inline { + display: flex; + justify-content: flex-end; + } + } + &-info { + @include typeface(--paragraph-left-normal-black); + color: var(--text-general); + margin: auto; + + &--left { + transform: translateX(5px); + } + } + &-wrapper { + &--disabled { + .btn-purchase__box-shadow { + opacity: 0; + } + } + } + .dc-datepicker { + .dc-input { + border: 1px solid var(--fill-normal); + background-color: var(--general-main-1); + margin-bottom: 0; + + &__field { + height: 3.2rem; + } + } + } + } + &__input { + align-self: center; + appearance: none; + box-sizing: border-box; + border-radius: $BORDER_RADIUS; + background: var(--fill-normal); + color: var(--text-general); + border: 1px solid var(--fill-normal); + + &:hover { + border-color: var(--border-hover); + } + &:active, + &:focus { + border-color: var(--border-active); + } + } + &__error { + justify-content: center; + align-items: center; + display: flex; + + &-info { + @include typeface(--small-left-light-black, none); + @include toEm(padding, 5px 5px 5px 8px, 1.2em); + @include toEm(margin-bottom, 10px, 1.2em); + color: var(--text-general); + background-color: rgba($color-red, 0.16); + display: block; + border: 1px solid var(--brand-red-coral); + border-radius: $BORDER_RADIUS; + } + } + &__currency { + position: absolute; + height: 3.2rem; + right: 4rem; + align-items: center; + justify-content: center; + display: flex; + background: transparent; + border-color: transparent; + z-index: 2; + color: inherit; + + &--symbol { + font-size: 1.4rem; + line-height: 1.5; + padding-bottom: 0.2rem; + } + &:before { + @include typeface(--paragraph-center-normal-black); + color: inherit; + } + & ~ .trade-container__input { + padding: 0 30px; + } + } + &__price, + &__order-input { + line-height: 0.9rem; + border: 0; + width: 100%; + display: flex; + justify-content: space-between; + position: relative; + color: var(--text-prominent); + + &-info { + display: flex; + justify-self: left; + align-items: center; + + @include mobile { + color: var(--text-colored-background); + width: 100%; + justify-content: space-between; + align-items: center; + + &-wrapper { + display: flex; + justify-content: flex-end; + align-items: center; + } + } + @include desktop { + min-height: 2.1rem; + } + + &--disabled { + opacity: 0.32; + } + &--slide { + width: 92px; + height: 8px; + margin: 6.5px 0; + @extend .loader; + + .trade-container__price-info-basis { + @extend .loader--loading; + } + } + &--fade &-value { + opacity: 0; + } + &--fade { + .trade-container__price-info-movement { + opacity: 0; + } + } + &-value { + font-size: 1.4rem; + font-weight: 700; + text-align: left; + line-height: 1.25; + margin-left: 0.1rem; + color: var(--text-prominent); + opacity: 1; + transition: 0.3s; + + @include mobile { + color: var(--text-colored-background); + } + } + &-basis { + margin-left: 0; + font-weight: normal; + @include typeface(--paragraph-left-normal-black); + color: var(--text-less-prominent); + @include mobile { + @include typeface(--xsmall-left-normal-black); + color: var(--text-colored-background); + } + } + &-currency { + margin-left: 4px; + margin-right: 1px; + display: inline-block; + position: relative; + font-weight: bold; + } + &-movement { + margin-left: 4px; + width: 16px; + height: 16px; + bottom: 1px; + position: relative; + } + } + } + &__price-info-currency { + @include mobile { + font-size: 0.9rem; + } + } + &__barriers { + display: flex; + flex-direction: column; + + &:first-child { + padding-right: 8px; + } + &-input { + padding-left: 3px; + } + &-single { + width: 100%; + } + &-multiple { + &-input { + padding-left: 25px; + padding-right: 9px; + text-align: center; + } + &:first-of-type { + padding-right: 8px; + } + } + &--up, + &--down { + position: absolute; + margin-top: 15px; + } + &--up { + right: 86.5%; + } + &--down { + right: 39%; + } + } + &__allow-equals { + /* postcss-bem-linter: ignore */ + &__label { + color: var(--text-general); + } + } + &__currency-options { + // fix for Safari: + // display: inline-block causes input cursor to seemingly appears duplicated + display: grid; + grid-template-columns: 3fr 1.5fr; + grid-gap: 0.4rem; + + .dc-dropdown-container { + /* postcss-bem-linter: ignore */ + &__currency { + margin-top: 0.8em; + min-width: unset; + + /* postcss-bem-linter: ignore */ + .dc-dropdown__select-arrow { + top: 9px; + } + /* postcss-bem-linter: ignore */ + .dc-dropdown__display { + border-radius: $BORDER_RADIUS; + + /* postcss-bem-linter: ignore */ + .symbols { + font-size: 1.4em; + } + } + } + } + } + &__amount { + &--multipliers { + & .trade-container__input { + left: 3.6rem; + } + } + & .dc-tooltip--error { + & .dc-input-wrapper { + border: 1px solid var(--status-danger); + + &:hover { + border-color: var(--status-danger); + } + } + } + & .dc-input-wrapper { + border-radius: $BORDER_RADIUS; + background: var(--fill-normal); + border: 1px solid var(--fill-normal); + height: 3.2rem; + box-sizing: border-box; + + &__button { + top: 1px; + + &--increment { + right: 1px; + } + &--decrement { + left: 1px; + } + } + &:hover { + border-color: var(--border-hover); + } + &:active, + &:focus { + border-color: var(--border-active); + } + } + & .trade-container__input { + font-size: 1.4rem; + max-width: calc(100% - 7.2rem); + border-radius: 0 $BORDER_RADIUS $BORDER_RADIUS 0; + background: none; + border: none; + padding: 0 0 0.2rem; + + &.input--error { + border: none !important; + } + } + } + &__multiplier { + display: flex; + flex-direction: column; + + &-dropdown { + /* postcss-bem-linter: ignore */ + .dc-dropdown__display-text { + padding-right: unset; + padding-left: 1em; + } + /* postcss-bem-linter: ignore */ + .dc-dropdown__display-text, + .dc-list__item-text { + text-transform: unset; + } + } + /* postcss-bem-linter: ignore */ + .dc-popover { + align-self: flex-end; + } + } + &__deal-cancellation-popover { + width: 28rem; + } + &__cancel-deal-info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + &--row-layout { + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .trade-container__price-info-currency { + margin-left: -0.1rem; + } + @include mobile { + padding: 0.4rem 0 0 !important; + margin-bottom: -0.5rem; + + .trade-container__price-info-currency { + font-size: 0.9rem !important; + } + .trade-container__price-info-basis { + font-size: 0.8rem; + } + } + } + .trade-container__price-info-currency { + font-size: 1.2rem; + } + @include mobile { + padding: 0 0.4rem; + min-height: 1.4rem; + + .trade-container__price-info-basis { + color: var(--text-general); + } + .trade-container__price-info-value { + color: var(--text-prominent); + font-size: 1rem; + } + .trade-container__price-info-currency { + font-size: 0.9rem; + } + } + } + &__multipliers-trade-info { + display: flex; + justify-content: space-around; + gap: 2.8rem; + + &-tooltip-text { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 0.8rem; + text-align: right; + + span { + border-bottom: 1px dotted var(--text-general); + } + } + } + &__popover { + /* postcss-bem-linter: ignore */ + .dc-checkbox { + margin-top: 0.8rem; + + /* postcss-bem-linter: ignore */ + &__label { + font-size: inherit; + } + } + } + /* postcss-bem-linter: ignore */ + .symbols.disabled { + color: var(--text-disabled); + } + .dc-dropdown--left { + .dc-dropdown__display-text { + padding-left: 15px; + padding-right: unset; + } + .dc-dropdown__select-arrow { + left: 0.3rem; + } + } +} + +/** @define dc-input-field; weak */ +.dc-input-field { + .dc-input-wrapper__icon { + top: 6px; + } + &--has-error { + .dc-input { + border: 1px solid $COLOR_RED !important; + } + } +} + +/** @define purchase-container; weak */ +.purchase-container { + position: relative; + + &__option { + padding-top: 8px; + padding-bottom: 8px; + + &:not(:only-child) { + &:nth-last-child(2) { + border-bottom-width: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + margin-bottom: 0 !important; + } + &:nth-last-child(1) { + border-top-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + margin-bottom: 0 !important; + + .trade-container__price { + margin-top: -4px; + } + } + } + @include mobile { + padding: initial; + position: relative; + + &:not(&--has-cancellation) { + .btn-purchase--multiplier { + height: 86px; + } + } + } + } + &__loading { + background: rgba(255, 255, 255, 0.6); + border-radius: $BORDER_RADIUS; + width: calc(100% + 2px); + height: calc(100% + 2px); + left: -2px; + } + @include mobile { + display: grid; + grid-template-areas: 'a b'; + grid-column-gap: 0.5rem; + } + //&__lock { + // position: absolute; + // display: flex; + // justify-content: center; + // align-items: center; + // background: $COLOR_LIGHT_BLACK_4_SOLID; + // opacity: 0.95; + // min-height: 202px; + // width: calc(100% - 6px); + // height: calc(100% - 2px); + // z-index: 3; + // top: 0; + // left: 2px; + // right: 0; + // flex-direction: column; + // bottom: 0; + // border-radius: $BORDER_RADIUS; + // @include themify($themes) { + // background: themed('background_container_color'); + // } + // + // &-icon { + // width: 64px; + // height: 64px; + // @extend %inline-icon.white; + // } + // &-button { + // height: 32px; + // margin: 0 1rem 1.5rem; + // width: 90px; + // line-height: 100%; + // background: transparent; + // } + // &-header { + // margin: 2px 8px 8px; + // padding: 0; + // @include themify($themes) { + // color: themed('text_color'); + // } + // } + // &-message { + // text-align: center; + // padding: 8px; + // margin: 1rem 1rem 0; + // border-radius: $BORDER_RADIUS; + // font-size: 10px; + // @include themify($themes) { + // background: themed('background_cover'); + // color: themed('text_primary_color'); + // } + // } + //} +} + +/** @define duration-container */ +.duration-container { + /* postcss-bem-linter: ignore */ + .dropdown-container { + margin-top: 0.8em; + } +} + +/** @define dc-collapsible */ +@include mobile { + .dc-collapsible { + position: absolute; + bottom: 0; + z-index: 999; + margin: 0 auto; + left: 0; + width: calc(100vw - 1.6rem); + transform: translate(0.8rem, 0); + } +} + +/** @define market-unavailable-modal */ +@include mobile { + .market-unavailable-modal { + @media only screen and (max-height: 600px) { + align-items: flex-start; + padding-top: 10rem; + + .dc-dialog__dialog { + margin-top: 0; + } + } + + /* postcss-bem-linter: ignore */ + .dc-dialog__footer { + flex-direction: column; + align-items: center; + width: auto; + + .dc-btn { + margin: 0; + width: 100%; + + &--primary { + order: 1; + margin-bottom: 1rem; + } + &--secondary { + order: 2; + } + } + } + } +} From 3fc26e062f1dbb581d91cc59dda69c4ffe817380 Mon Sep 17 00:00:00 2001 From: mahdiyeh-fs Date: Sun, 27 Mar 2022 15:19:40 +0430 Subject: [PATCH 02/64] add config to core --- .circleci/config.yml | 1 + .github/CODEOWNERS | 4 ++ packages/core/build/config.js | 5 ++ .../core/src/App/Constants/routes-config.js | 61 ++++++++++--------- packages/reports/src/init-store.js | 2 +- 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index babd2e81b14c..646385fac36d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,6 +64,7 @@ commands: - "packages/bot-skeleton/node_modules" - "packages/bot-web-ui/node_modules" - "packages/cashier/node_modules" + - "packages/reports/node_modules" - "packages/components/node_modules" - "packages/core/node_modules" - "packages/indicators/node_modules" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7bc8c901a133..2a3e964280a4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -113,6 +113,10 @@ /packages/trader/**/* @matin-binary @balakrishna-binary @akmal-binary +# deriv-app/reports +# ============================================================== + +/packages/reports/**/* @matin-binary @balakrishna-binary # ============================================================== diff --git a/packages/core/build/config.js b/packages/core/build/config.js index d81b7dac6846..97710a82a8a2 100644 --- a/packages/core/build/config.js +++ b/packages/core/build/config.js @@ -49,6 +49,10 @@ const copyConfig = base => { from: path.resolve(__dirname, '../node_modules/@deriv/trader/dist/trader'), to: 'trader', }, + { + from: path.resolve(__dirname, '../node_modules/@deriv/reports/dist/reports'), + to: 'reports', + }, { from: path.resolve(__dirname, '../node_modules/@deriv/appstore/dist/appstore'), to: 'appstore', @@ -140,6 +144,7 @@ const generateSWConfig = is_release => ({ /^bot\//, /^media\//, /^trader\//, + /^reports\//, /^cashier\//, /^js\/core\.[a-z_]*-json\./, ], diff --git a/packages/core/src/App/Constants/routes-config.js b/packages/core/src/App/Constants/routes-config.js index 2f6573ca440a..a576a9089c4b 100644 --- a/packages/core/src/App/Constants/routes-config.js +++ b/packages/core/src/App/Constants/routes-config.js @@ -14,6 +14,11 @@ const Trader = React.lazy(() => { return import(/* webpackChunkName: "trader" */ '@deriv/trader'); }); +const Reports = React.lazy(() => { + // eslint-disable-next-line import/no-unresolved + return import(/* webpackChunkName: "reports" */ '@deriv/reports'); +}); + const Account = React.lazy(() => { // eslint-disable-next-line import/no-unresolved return import(/* webpackChunkName: "account" */ '@deriv/account'); @@ -42,6 +47,34 @@ const getModules = ({ is_appstore }) => { // Don't use `Localize` component since native html tag like `option` cannot render them getTitle: () => localize('Bot'), }, + { + path: routes.reports, + component: Reports, + getTitle: () => localize('Reports'), + icon_component: 'IcReports', + is_authenticated: true, + routes: [ + { + path: routes.positions, + component: Reports, + getTitle: () => localize('Open positions'), + icon_component: 'IcOpenPositions', + default: true, + }, + { + path: routes.profit, + component: Reports, + getTitle: () => localize('Profit table'), + icon_component: 'IcProfitTable', + }, + { + path: routes.statement, + component: Reports, + getTitle: () => localize('Statement'), + icon_component: 'IcStatement', + }, + ], + }, { path: routes.account_deactivated, component: Account, @@ -219,34 +252,6 @@ const getModules = ({ is_appstore }) => { getTitle: () => localize('MT5'), is_authenticated: false, }, - { - path: routes.reports, - component: Trader, - getTitle: () => localize('Reports'), - icon_component: 'IcReports', - is_authenticated: true, - routes: [ - { - path: routes.positions, - component: Trader, - getTitle: () => localize('Open positions'), - icon_component: 'IcOpenPositions', - default: true, - }, - { - path: routes.profit, - component: Trader, - getTitle: () => localize('Profit table'), - icon_component: 'IcProfitTable', - }, - { - path: routes.statement, - component: Trader, - getTitle: () => localize('Statement'), - icon_component: 'IcStatement', - }, - ], - }, { path: routes.contract, component: Trader, diff --git a/packages/reports/src/init-store.js b/packages/reports/src/init-store.js index 71149f32bcd8..145e9fd5796f 100644 --- a/packages/reports/src/init-store.js +++ b/packages/reports/src/init-store.js @@ -1,7 +1,7 @@ import { configure } from 'mobx'; import RootStore from './Stores'; import { setWebsocket } from '@deriv/shared'; -import ServerTime from '_common/base/server_time'; +import ServerTime from './_common/base/server_time'; configure({ enforceActions: 'observed' }); From 8ba0a559f389248260ce6fe6316762ee5ae9dfd2 Mon Sep 17 00:00:00 2001 From: mahdiyeh-fs Date: Sun, 27 Mar 2022 16:17:40 +0430 Subject: [PATCH 03/64] fix event-source-polyfil version --- packages/reports/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reports/package.json b/packages/reports/package.json index 8ebfa110df83..8a6dd05209ed 100644 --- a/packages/reports/package.json +++ b/packages/reports/package.json @@ -98,7 +98,7 @@ "canvas-toBlob": "^1.0.0", "classnames": "^2.2.6", "crc-32": "^1.2.0", - "event-source-polyfill": "^1.0.5", + "event-source-polyfill": "1.0.25", "extend": "^3.0.2", "formik": "^2.1.4", "i18next": "^20.3.2", From da8c6c98671158f6a788301874d9844cb57f6ad9 Mon Sep 17 00:00:00 2001 From: mahdiyeh-fs Date: Sun, 10 Apr 2022 16:05:14 +0430 Subject: [PATCH 04/64] resolve build errors --- packages/core/package.json | 1 + packages/reports/build/constants.js | 4 +- .../Elements/ContentLoader/index.js | 3 + .../Elements/ContentLoader/positions-card.jsx | 35 ++ .../ContentLoader/reports-table-row.jsx | 28 ++ .../Elements/ContentLoader/trade-params.jsx | 44 +++ .../PositionsDrawerCard/index.js | 1 + .../positions-drawer-card.jsx | 230 ++++++++++++ .../__tests__/result-mobile.spec.js | 20 + .../helpers/__tests__/positions-helper.js | 28 ++ .../Elements/PositionsDrawer/helpers/index.js | 1 + .../helpers/positions-helper.js | 49 +++ .../Elements/PositionsDrawer/index.js | 1 + .../PositionsDrawer/positions-drawer.jsx | 159 ++++++++ .../PositionsDrawer/positions-modal-card.jsx | 341 ++++++++++++++++++ .../PositionsDrawer/result-mobile.jsx | 52 +++ .../Form/CompositeCalendar/calendar-icon.jsx | 11 + .../composite-calendar-mobile.jsx | 250 +++++++++++++ .../CompositeCalendar/composite-calendar.jsx | 191 ++++++++++ .../Form/CompositeCalendar/index.js | 1 + .../Form/CompositeCalendar/list-item.jsx | 22 ++ .../Form/CompositeCalendar/side-list.jsx | 27 ++ .../CompositeCalendar/two-month-picker.jsx | 116 ++++++ .../__tests__/range-slider.spec.js | 25 ++ .../RangeSlider/__tests__/tick-steps.spec.js | 13 + .../App/Components/Form/RangeSlider/index.js | 1 + .../Form/RangeSlider/range-slider.jsx | 119 ++++++ .../Form/RangeSlider/tick-steps.jsx | 45 +++ .../App/Components/Form/TimePicker/dialog.jsx | 128 +++++++ .../App/Components/Form/TimePicker/index.js | 1 + .../Form/TimePicker/time-picker.jsx | 128 +++++++ .../src/App/Components/Form/fieldset.jsx | 51 +++ .../App/Components/Form/number-selector.jsx | 45 +++ .../Routes/__tests__/binary-link.spec.js | 50 +++ .../Routes/__tests__/helpers.spec.js | 87 +++++ .../__tests__/route-with-sub-routes.spec.js | 32 ++ .../src/App/Components/Routes/binary-link.jsx | 33 ++ .../App/Components/Routes/binary-routes.jsx | 16 + .../src/App/Components/Routes/helpers.js | 37 ++ .../src/App/Components/Routes/index.js | 4 + .../Routes/route-with-sub-routes.jsx | 57 +++ .../Containers/ProgressSliderStream/index.js | 1 + .../progress-slider-stream.jsx | 36 ++ .../reports/src/Components/amount-cell.jsx | 2 +- .../src/Components/filter-component.jsx | 2 +- .../src/Components/indicative-cell.jsx | 2 +- .../src/Components/market-symbol-icon-row.jsx | 2 +- .../src/Components/placeholder-component.jsx | 2 +- .../src/Components/profit_loss_cell.jsx | 2 +- .../src/Constants/data-table-constants.js | 13 +- .../reports/src/Constants/routes-config.js | 2 +- .../reports/src/Containers/open-positions.jsx | 11 +- .../reports/src/Containers/profit-table.jsx | 13 +- packages/reports/src/Containers/statement.jsx | 12 +- packages/reports/src/Helpers/contract-type.js | 112 ++++++ packages/reports/src/Helpers/digits.js | 5 + .../reports/src/Helpers/market-underlying.js | 2 +- .../Stores/Modules/Contract/Constants/ui.js | 9 + .../Contract/Constants/validation-rules.js | 68 ++++ .../Contract/Helpers/__tests__/logic.js | 125 +++++++ .../Contract/Helpers/chart-marker-helpers.js | 116 ++++++ .../Modules/Contract/Helpers/chart-markers.js | 89 +++++ .../Contract/Helpers/chart-notifications.js | 9 + .../Modules/Contract/Helpers/contract-type.js | 2 + .../Modules/Contract/Helpers/limit-orders.js | 100 +++++ .../Stores/Modules/Contract/Helpers/logic.js | 111 ++++++ .../Modules/Contract/Helpers/multiplier.js | 40 ++ .../Modules/Contract/contract-replay-store.js | 291 +++++++++++++++ .../Stores/Modules/Contract/contract-store.js | 311 ++++++++++++++++ .../Modules/Contract/contract-trade-store.js | 195 ++++++++++ .../Portfolio/Helpers/format-response.js | 5 +- .../Modules/Profit/Helpers/format-response.js | 4 +- .../Modules/SmartChart/Constants/barriers.js | 36 ++ .../Modules/SmartChart/Constants/markers.js | 59 +++ .../SmartChart/Helpers/__tests__/barriers.js | 59 +++ .../Modules/SmartChart/Helpers/barriers.js | 28 ++ .../Modules/SmartChart/chart-barrier-store.js | 80 ++++ .../Modules/SmartChart/chart-marker-store.js | 11 + .../Statement/Helpers/format-response.js | 2 +- .../src/Stores/Modules/Trading/trade-store.js | 4 +- packages/reports/src/Stores/base-store.js | 2 +- .../src/Validator/__tests__/error.spec.js | 53 +++ packages/reports/src/Validator/errors.js | 40 ++ packages/reports/src/Validator/index.js | 1 + packages/reports/src/Validator/validator.js | 112 ++++++ .../src/_common/components/loading.jsx | 15 + packages/reports/src/_common/contract.js | 272 ++++++++++++++ .../templates/_common/components/loading.jsx | 15 + .../_common/components/loading.tsx | 0 .../src/templates/app/components/loading.jsx | 44 --- 90 files changed, 4930 insertions(+), 84 deletions(-) create mode 100644 packages/reports/src/App/Components/Elements/ContentLoader/index.js create mode 100644 packages/reports/src/App/Components/Elements/ContentLoader/positions-card.jsx create mode 100644 packages/reports/src/App/Components/Elements/ContentLoader/reports-table-row.jsx create mode 100644 packages/reports/src/App/Components/Elements/ContentLoader/trade-params.jsx create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/index.js create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/__tests__/result-mobile.spec.js create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/__tests__/positions-helper.js create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/index.js create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/index.js create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx create mode 100644 packages/reports/src/App/Components/Elements/PositionsDrawer/result-mobile.jsx create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/calendar-icon.jsx create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar.jsx create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/index.js create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/list-item.jsx create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/side-list.jsx create mode 100644 packages/reports/src/App/Components/Form/CompositeCalendar/two-month-picker.jsx create mode 100644 packages/reports/src/App/Components/Form/RangeSlider/__tests__/range-slider.spec.js create mode 100644 packages/reports/src/App/Components/Form/RangeSlider/__tests__/tick-steps.spec.js create mode 100644 packages/reports/src/App/Components/Form/RangeSlider/index.js create mode 100644 packages/reports/src/App/Components/Form/RangeSlider/range-slider.jsx create mode 100644 packages/reports/src/App/Components/Form/RangeSlider/tick-steps.jsx create mode 100644 packages/reports/src/App/Components/Form/TimePicker/dialog.jsx create mode 100644 packages/reports/src/App/Components/Form/TimePicker/index.js create mode 100644 packages/reports/src/App/Components/Form/TimePicker/time-picker.jsx create mode 100644 packages/reports/src/App/Components/Form/fieldset.jsx create mode 100644 packages/reports/src/App/Components/Form/number-selector.jsx create mode 100644 packages/reports/src/App/Components/Routes/__tests__/binary-link.spec.js create mode 100644 packages/reports/src/App/Components/Routes/__tests__/helpers.spec.js create mode 100644 packages/reports/src/App/Components/Routes/__tests__/route-with-sub-routes.spec.js create mode 100644 packages/reports/src/App/Components/Routes/binary-link.jsx create mode 100644 packages/reports/src/App/Components/Routes/binary-routes.jsx create mode 100644 packages/reports/src/App/Components/Routes/helpers.js create mode 100644 packages/reports/src/App/Components/Routes/index.js create mode 100644 packages/reports/src/App/Components/Routes/route-with-sub-routes.jsx create mode 100644 packages/reports/src/App/Containers/ProgressSliderStream/index.js create mode 100644 packages/reports/src/App/Containers/ProgressSliderStream/progress-slider-stream.jsx create mode 100644 packages/reports/src/Helpers/contract-type.js create mode 100644 packages/reports/src/Helpers/digits.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Constants/ui.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Constants/validation-rules.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/__tests__/logic.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/chart-marker-helpers.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/chart-markers.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/chart-notifications.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/contract-type.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/limit-orders.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/logic.js create mode 100644 packages/reports/src/Stores/Modules/Contract/Helpers/multiplier.js create mode 100644 packages/reports/src/Stores/Modules/Contract/contract-replay-store.js create mode 100644 packages/reports/src/Stores/Modules/Contract/contract-store.js create mode 100644 packages/reports/src/Stores/Modules/Contract/contract-trade-store.js create mode 100644 packages/reports/src/Stores/Modules/SmartChart/Constants/barriers.js create mode 100644 packages/reports/src/Stores/Modules/SmartChart/Constants/markers.js create mode 100644 packages/reports/src/Stores/Modules/SmartChart/Helpers/__tests__/barriers.js create mode 100644 packages/reports/src/Stores/Modules/SmartChart/Helpers/barriers.js create mode 100644 packages/reports/src/Stores/Modules/SmartChart/chart-barrier-store.js create mode 100644 packages/reports/src/Stores/Modules/SmartChart/chart-marker-store.js create mode 100644 packages/reports/src/Validator/__tests__/error.spec.js create mode 100644 packages/reports/src/Validator/errors.js create mode 100644 packages/reports/src/Validator/index.js create mode 100644 packages/reports/src/Validator/validator.js create mode 100644 packages/reports/src/_common/components/loading.jsx create mode 100644 packages/reports/src/_common/contract.js create mode 100644 packages/reports/src/templates/_common/components/loading.jsx rename packages/trader/src/{templates => }/_common/components/loading.tsx (100%) delete mode 100644 packages/trader/src/templates/app/components/loading.jsx diff --git a/packages/core/package.json b/packages/core/package.json index 85e40e7bb9de..b053b322d736 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -99,6 +99,7 @@ "@deriv/deriv-api": "^1.0.8", "@deriv/deriv-charts": "^0.5.1", "@deriv/p2p": "^0.7.3", + "@deriv/reports": "^1.0.0", "@deriv/shared": "^1.0.0", "@deriv/trader": "^3.8.0", "@deriv/translations": "^1.0.0", diff --git a/packages/reports/build/constants.js b/packages/reports/build/constants.js index 898a8794ae56..22a9b9e45ad3 100644 --- a/packages/reports/build/constants.js +++ b/packages/reports/build/constants.js @@ -11,7 +11,6 @@ const StylelintPlugin = require('stylelint-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const { copyConfig, @@ -39,6 +38,8 @@ const ALIASES = { Modules: path.resolve(__dirname, '../src/Modules'), Sass: path.resolve(__dirname, '../src/sass'), Stores: path.resolve(__dirname, '../src/Stores'), + App: path.resolve(__dirname, '../src/App'), + Helpers: path.resolve(__dirname, '../src/Helpers'), }; const rules = (is_test_env = false, is_mocha_only = false) => [ @@ -124,7 +125,6 @@ const plugins = (base, is_test_env, is_mocha_only) => [ new IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), new MiniCssExtractPlugin(cssConfig()), new CircularDependencyPlugin({ exclude: /node_modules/, failOnError: true }), - new ForkTsCheckerWebpackPlugin(), ...(IS_RELEASE ? [] : [ diff --git a/packages/reports/src/App/Components/Elements/ContentLoader/index.js b/packages/reports/src/App/Components/Elements/ContentLoader/index.js new file mode 100644 index 000000000000..90c86e01a015 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/ContentLoader/index.js @@ -0,0 +1,3 @@ +export * from './positions-card.jsx'; +export * from './reports-table-row.jsx'; +export * from './trade-params.jsx'; diff --git a/packages/reports/src/App/Components/Elements/ContentLoader/positions-card.jsx b/packages/reports/src/App/Components/Elements/ContentLoader/positions-card.jsx new file mode 100644 index 000000000000..3e2708bc34b4 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/ContentLoader/positions-card.jsx @@ -0,0 +1,35 @@ +import ContentLoader from 'react-content-loader'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const PositionsCardLoader = ({ speed }) => ( + + + + + + + + + + + + + + + + + +); + +PositionsCardLoader.propTypes = { + speed: PropTypes.number, +}; + +export { PositionsCardLoader }; diff --git a/packages/reports/src/App/Components/Elements/ContentLoader/reports-table-row.jsx b/packages/reports/src/App/Components/Elements/ContentLoader/reports-table-row.jsx new file mode 100644 index 000000000000..328ea93c733f --- /dev/null +++ b/packages/reports/src/App/Components/Elements/ContentLoader/reports-table-row.jsx @@ -0,0 +1,28 @@ +import ContentLoader from 'react-content-loader'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const ReportsTableRowLoader = ({ speed }) => ( + + + + + + + + + + +); + +ReportsTableRowLoader.propTypes = { + speed: PropTypes.number, +}; + +export { ReportsTableRowLoader }; diff --git a/packages/reports/src/App/Components/Elements/ContentLoader/trade-params.jsx b/packages/reports/src/App/Components/Elements/ContentLoader/trade-params.jsx new file mode 100644 index 000000000000..5b1588d8f0ca --- /dev/null +++ b/packages/reports/src/App/Components/Elements/ContentLoader/trade-params.jsx @@ -0,0 +1,44 @@ +import ContentLoader from 'react-content-loader'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { DesktopWrapper, MobileWrapper } from '@deriv/components'; + +const TradeParamsLoader = ({ speed }) => ( + <> + + + + + + + + + + + + + + + + + + +); + +TradeParamsLoader.propTypes = { + speed: PropTypes.number, +}; + +export { TradeParamsLoader }; diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/index.js b/packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/index.js new file mode 100644 index 000000000000..0097ff7902b7 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/index.js @@ -0,0 +1 @@ +export default from './positions-drawer-card.jsx'; diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx b/packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx new file mode 100644 index 000000000000..9b454cf9e45e --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx @@ -0,0 +1,230 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { ContractCard } from '@deriv/components'; +import { getContractPath, isCryptoContract, isMultiplierContract } from '@deriv/shared'; +import { getCardLabels, getContractTypeDisplay } from '_common/contract'; +import { connect } from 'Stores/connect'; +import { connectWithContractUpdate } from 'Stores/Modules/Contract/Helpers/multiplier'; + +import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic'; + +const PositionsDrawerCard = ({ + addToast, + className, + display_name, + contract_info, + contract_update, + currency, + current_focus, + getContractById, + is_mobile, + is_sell_requested, + is_unsupported, + is_link_disabled, + profit_loss, + onClickCancel, + onClickSell, + onClickRemove, + onFooterEntered, + onMouseEnter, + onMouseLeave, + removeToast, + result, + setCurrentFocus, + server_time, + should_show_transition, + should_show_cancellation_warning, + status, + toggleCancellationWarning, + toggleUnsupportedContractModal, +}) => { + const is_multiplier = isMultiplierContract(contract_info.contract_type); + const is_crypto = isCryptoContract(contract_info.underlying); + const has_progress_slider = !is_multiplier || (is_crypto && is_multiplier); + const has_ended = !!getEndTime(contract_info); + + const loader_el = ( +
+ +
+ ); + + const card_header = ( + + ); + + const card_body = ( + { + if (typeof onMouseLeave === 'function') onMouseLeave(); + }} + is_mobile={is_mobile} + is_multiplier={is_multiplier} + is_sold={has_ended} + has_progress_slider={is_mobile && has_progress_slider} + removeToast={removeToast} + server_time={server_time} + setCurrentFocus={setCurrentFocus} + should_show_cancellation_warning={should_show_cancellation_warning} + status={status} + toggleCancellationWarning={toggleCancellationWarning} + /> + ); + + const card_footer = ( + + ); + + const contract_el = ( + + {card_header} + {card_body} + + ); + + const supported_contract_card = ( +
0 && !result, + 'dc-contract-card--red': !is_multiplier && profit_loss < 0 && !result, + })} + onClick={() => toggleUnsupportedContractModal(true)} + > + {contract_info.underlying ? contract_el : loader_el} +
+ ); + + const unsupported_contract_card = is_link_disabled ? ( +
0 && !result, + 'dc-contract-card--red': !is_multiplier && profit_loss < 0 && !result, + })} + > + {contract_info.underlying ? contract_el : loader_el} +
+ ) : ( + 0 && !result, + 'dc-contract-card--red': !is_multiplier && profit_loss < 0 && !result, + })} + to={{ + pathname: `/contract/${contract_info.contract_id}`, + }} + > + {contract_info.underlying ? contract_el : loader_el} + + ); + + return ( + +
{ + if (typeof onMouseEnter === 'function') onMouseEnter(); + }} + onMouseLeave={() => { + if (typeof onMouseLeave === 'function') onMouseLeave(); + }} + onClick={() => { + if (typeof onMouseLeave === 'function') onMouseLeave(); + }} + > + {is_unsupported ? supported_contract_card : unsupported_contract_card} + {card_footer} +
+
+ ); +}; + +PositionsDrawerCard.propTypes = { + addToast: PropTypes.func, + className: PropTypes.string, + contract_info: PropTypes.object, + currency: PropTypes.string, + current_focus: PropTypes.string, + current_tick: PropTypes.number, + duration: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + duration_unit: PropTypes.string, + exit_spot: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + id: PropTypes.number, + indicative: PropTypes.number, + is_loading: PropTypes.bool, + is_mobile: PropTypes.bool, + is_sell_requested: PropTypes.bool, + is_unsupported: PropTypes.bool, + is_valid_to_sell: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), + onClickRemove: PropTypes.func, + onClickSell: PropTypes.func, + onClickCancel: PropTypes.func, + profit_loss: PropTypes.number, + result: PropTypes.string, + sell_time: PropTypes.number, + setCurrentFocus: PropTypes.func, + status: PropTypes.string, + toggleUnsupportedContractModal: PropTypes.func, + type: PropTypes.string, +}; + +export default connect(({ modules, ui, client, common }) => ({ + currency: client.currency, + server_time: common.server_time, + addToast: ui.addToast, + current_focus: ui.current_focus, + onClickCancel: modules.portfolio.onClickCancel, + onClickSell: modules.portfolio.onClickSell, + onClickRemove: modules.portfolio.removePositionById, + getContractById: modules.contract_trade.getContractById, + is_mobile: ui.is_mobile, + removeToast: ui.removeToast, + setCurrentFocus: ui.setCurrentFocus, + should_show_cancellation_warning: ui.should_show_cancellation_warning, + toggleCancellationWarning: ui.toggleCancellationWarning, + toggleUnsupportedContractModal: ui.toggleUnsupportedContractModal, +}))(PositionsDrawerCard); diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/__tests__/result-mobile.spec.js b/packages/reports/src/App/Components/Elements/PositionsDrawer/__tests__/result-mobile.spec.js new file mode 100644 index 000000000000..a37c521b4405 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/__tests__/result-mobile.spec.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import ResultMobile from '../result-mobile'; + +describe('ResultMobile', () => { + it('should ResultMobile be in the DOM', () => { + const wrapper = render(); + expect(wrapper.getByTestId('result_mobile')).toBeInTheDocument(); + }); + + it('should ResultMobile render LOST if result is won ', () => { + const wrapper = render(); + expect(wrapper.getByText('Won')).toBeInTheDocument(true); + }); + + it('should ResultMobile render LOST if result is not won ', () => { + const wrapper = render(); + expect(wrapper.getByText('Lost')).toBeInTheDocument(true); + }); +}); diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/__tests__/positions-helper.js b/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/__tests__/positions-helper.js new file mode 100644 index 000000000000..a05ee3e3f125 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/__tests__/positions-helper.js @@ -0,0 +1,28 @@ +import { expect } from 'chai'; +import * as PositionsHelper from '../positions-helper'; + +describe('positions-helper', () => { + describe('addCommaToNumber', () => { + it('should work as expected with number steps of thousands leading to a comma separated string', () => { + const number = 1224500.3153; + expect(PositionsHelper.addCommaToNumber(number)).to.eql('1,224,500.3153'); + }); + }); + describe('getBarrierLabel', () => { + it('should return Target label if contract has a digit contract type', () => { + const contract_info = { + contract_type: 'DIGITDIFF', + }; + expect(PositionsHelper.getBarrierLabel(contract_info)).to.eql('Target'); + }); + }); + describe('getBarrierValue', () => { + it('should return correct target value according to digit type mapping if contract type is digit', () => { + const contract_info = { + contract_type: 'DIGITDIFF', + barrier: '1', + }; + expect(PositionsHelper.getBarrierValue(contract_info)).to.eql('Not 1'); + }); + }); +}); diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/index.js b/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/index.js new file mode 100644 index 000000000000..9233600ea4f4 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/index.js @@ -0,0 +1 @@ +export * from './positions-helper'; diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js b/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js new file mode 100644 index 000000000000..96803078b143 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js @@ -0,0 +1,49 @@ +import { localize } from '@deriv/translations'; +import { isHighLow } from '@deriv/shared'; +import { getContractTypesConfig } from 'Stores/Modules/Trading/Constants/contract'; +import { isCallPut } from 'Stores/Modules/Contract/Helpers/contract-type'; + +export const addCommaToNumber = (num, decimal_places) => { + if (!num || isNaN(num)) { + return num; + } + const n = String(decimal_places ? (+num).toFixed(decimal_places) : num); + const p = n.indexOf('.'); + return n.replace(/\d(?=(?:\d{3})+(?:\.|$))/g, (m, i) => (p <= 0 || i < p ? `${m},` : m)); +}; + +export const getBarrierLabel = contract_info => { + if (isDigitType(contract_info.contract_type)) { + return localize('Target'); + } + return localize('Barrier'); +}; + +export const getBarrierValue = contract_info => { + if (isDigitType(contract_info.contract_type)) { + return digitTypeMap(contract_info)[contract_info.contract_type]; + } + return addCommaToNumber(contract_info.barrier); +}; + +export const isDigitType = contract_type => /digit/.test(contract_type.toLowerCase()); + +const digitTypeMap = contract_info => ({ + DIGITDIFF: localize('Not {{barrier}}', { barrier: contract_info.barrier }), + DIGITEVEN: localize('Even'), + DIGITMATCH: localize('Equals {{barrier}}', { barrier: contract_info.barrier }), + DIGITODD: localize('Odd'), + DIGITOVER: localize('Over {{barrier}}', { barrier: contract_info.barrier }), + DIGITUNDER: localize('Under {{barrier}}', { barrier: contract_info.barrier }), +}); + +export const filterByContractType = ({ contract_type, shortcode }, trade_contract_type) => { + const is_call_put = isCallPut(trade_contract_type); + const is_high_low = isHighLow({ shortcode }); + const trade_types = is_call_put + ? ['CALL', 'CALLE', 'PUT', 'PUTE'] + : getContractTypesConfig()[trade_contract_type]?.trade_types; + const match = trade_types?.includes(contract_type); + if (trade_contract_type === 'high_low') return is_high_low; + return match && !is_high_low; +}; diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/index.js b/packages/reports/src/App/Components/Elements/PositionsDrawer/index.js new file mode 100644 index 000000000000..5107a3e8c1c5 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/index.js @@ -0,0 +1 @@ +export default from './positions-drawer.jsx'; diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx b/packages/reports/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx new file mode 100644 index 000000000000..b23586f187a2 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx @@ -0,0 +1,159 @@ +import classNames from 'classnames'; +import { PropTypes as MobxPropTypes } from 'mobx-react'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { CSSTransition } from 'react-transition-group'; +import { Icon, DataList, Text } from '@deriv/components'; +import { routes, useNewRowTransition } from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import EmptyPortfolioMessage from 'Components/empty-portfolio-message.jsx'; +import { connect } from 'Stores/connect'; +import PositionsDrawerCard from './PositionsDrawerCard'; +import { filterByContractType } from './helpers'; + +const PositionsDrawerCardItem = ({ row: portfolio_position, measure, onHoverPosition, is_new_row }) => { + const { in_prop } = useNewRowTransition(is_new_row); + + React.useEffect(() => { + measure(); + }, [portfolio_position.contract_info.is_sold, measure]); + + return ( + +
+ { + onHoverPosition(true, portfolio_position); + }} + onMouseLeave={() => { + onHoverPosition(false, portfolio_position); + }} + onFooterEntered={measure} + should_show_transition={is_new_row} + /> +
+
+ ); +}; + +const PositionsDrawer = ({ + all_positions, + error, + is_positions_drawer_on, + onHoverPosition, + symbol, + toggleDrawer, + trade_contract_type, + onMount, +}) => { + const drawer_ref = React.useRef(null); + const list_ref = React.useRef(null); + const scrollbar_ref = React.useRef(null); + + React.useEffect(() => { + onMount(); + }, [onMount]); + + React.useEffect(() => { + list_ref?.current?.scrollTo(0); + scrollbar_ref?.current?.scrollToTop(); + }, [symbol, trade_contract_type]); + + const positions = all_positions.filter( + p => + p.contract_info && + symbol === p.contract_info.underlying && + filterByContractType(p.contract_info, trade_contract_type) + ); + + const body_content = ( + } + keyMapper={row => row.id} + row_gap={8} + /> + ); + + return ( + +
+
+
+ + {localize('Recent positions')} + +
+ +
+
+
+ {positions.length === 0 || error ? : body_content} +
+
+ + + {localize('Go to Reports')} + + +
+
+ + ); + // } +}; + +PositionsDrawer.propTypes = { + all_positions: MobxPropTypes.arrayOrObservableArray, + children: PropTypes.any, + error: PropTypes.string, + is_mobile: PropTypes.bool, + is_positions_drawer_on: PropTypes.bool, + onChangeContractUpdate: PropTypes.func, + onClickContractUpdate: PropTypes.func, + onMount: PropTypes.func, + symbol: PropTypes.string, + toggleDrawer: PropTypes.func, +}; + +export default connect(({ modules, ui }) => ({ + all_positions: modules.portfolio.all_positions, + error: modules.portfolio.error, + onHoverPosition: modules.portfolio.onHoverPosition, + onMount: modules.portfolio.onMount, + symbol: modules.trade.symbol, + trade_contract_type: modules.trade.contract_type, + is_mobile: ui.is_mobile, + is_positions_drawer_on: ui.is_positions_drawer_on, + toggleDrawer: ui.togglePositionsDrawer, +}))(PositionsDrawer); diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx b/packages/reports/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx new file mode 100644 index 000000000000..7ff6c183e58c --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx @@ -0,0 +1,341 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { CSSTransition } from 'react-transition-group'; +import { ContractCard, CurrencyBadge, Icon, Money, ProgressSliderMobile, Text } from '@deriv/components'; +import { + getContractPath, + isCryptoContract, + isMultiplierContract, + isHighLow, + isCryptocurrency, + hasContractEntered, + isOpen, +} from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import { BinaryLink } from 'App/Components/Routes'; +import { connect } from 'Stores/connect'; +import { getSymbolDisplayName } from 'Stores/Modules/Trading/Helpers/active-symbols'; + +import { connectWithContractUpdate } from 'Stores/Modules/Contract/Helpers/multiplier'; +import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic'; +import { PositionsCardLoader } from '../ContentLoader'; + +import { getContractTypeDisplay, getCardLabels } from '_common/contract'; +import { getMarketInformation } from 'Helpers/market-underlying'; +import ResultMobile from './result-mobile.jsx'; + +const PositionsModalCard = ({ + active_symbols, + addToast, + className, + contract_info, + contract_update, + currency, + current_focus, + current_tick, + getContractById, + id, + indicative, + is_loading, + is_mobile, + is_sell_requested, + is_unsupported, + onClickSell, + profit_loss, + onClickCancel, + removeToast, + result, + sell_price, + server_time, + setCurrentFocus, + should_show_cancellation_warning, + status, + toggleCancellationWarning, + togglePositions, + toggleUnsupportedContractModal, + type, +}) => { + const loader_el = ( +
+ +
+ ); + const is_multiplier = isMultiplierContract(contract_info.contract_type); + const is_crypto = isCryptoContract(contract_info.underlying); + const has_progress_slider = !is_multiplier || (is_crypto && is_multiplier); + const has_ended = !!getEndTime(contract_info); + const fallback_result = profit_loss >= 0 ? 'won' : 'lost'; + + const should_show_sell = hasContractEntered(contract_info) && isOpen(contract_info); + const display_name = getSymbolDisplayName(active_symbols, getMarketInformation(contract_info.shortcode).underlying); + + const contract_options_el = ( + +
+
+ + + {contract_info.display_name} + +
+
+ +
+ +
+ +
+
+
+ +
+
+
+ {result ? localize('Profit/Loss:') : localize('Potential profit/loss:')} +
+
+ {!result ? localize('Indicative price:') : localize('Payout:')} +
+
0, + })} + > + +
+ {status === 'profit' && } + {status === 'loss' && } +
+
+
+ +
+ {status === 'profit' && } + {status === 'loss' && } +
+
+
+
+
+ + {localize('Purchase price:')} + + + + +
+
+ + {localize('Potential payout:')} + + + {contract_info.payout ? ( + + ) : ( + -i + )} + +
+
+ + {result || !!contract_info.is_sold ? ( + + ) : ( + + )} +
+
+ ); + + const card_multiplier_header = ( + + ); + + const card_multiplier_body = ( + + ); + + const card_multiplier_footer = ( + + ); + + const contract_multiplier_el = ( + + + {card_multiplier_header} + {card_multiplier_body} + {card_multiplier_footer} + + + ); + + const contract_el = is_multiplier ? contract_multiplier_el : contract_options_el; + + return ( +
+ {is_unsupported ? ( +
toggleUnsupportedContractModal(true)} + > + {contract_info.underlying ? contract_el : loader_el} +
+ ) : ( + + + {contract_info.underlying ? contract_el : loader_el} + + + )} +
+ ); +}; + +PositionsModalCard.propTypes = { + className: PropTypes.string, + contract_info: PropTypes.object, + currency: PropTypes.string, + current_focus: PropTypes.string, + current_tick: PropTypes.number, + duration: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + duration_unit: PropTypes.string, + exit_spot: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + id: PropTypes.number, + indicative: PropTypes.number, + is_loading: PropTypes.bool, + is_mobile: PropTypes.bool, + is_sell_requested: PropTypes.bool, + is_unsupported: PropTypes.bool, + is_valid_to_sell: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), + onClickRemove: PropTypes.func, + onClickSell: PropTypes.func, + onClickCancel: PropTypes.func, + profit_loss: PropTypes.number, + result: PropTypes.string, + sell_time: PropTypes.number, + server_time: PropTypes.object, + setCurrentFocus: PropTypes.func, + status: PropTypes.string, + togglePositions: PropTypes.func, + toggleUnsupportedContractModal: PropTypes.func, + type: PropTypes.string, +}; + +export default connect(({ common, ui, modules }) => ({ + active_symbols: modules.trade.active_symbols, + addToast: ui.addToast, + current_focus: ui.current_focus, + getContractById: modules.contract_trade.getContractById, + is_mobile: ui.is_mobile, + removeToast: ui.removeToast, + server_time: common.server_time, + setCurrentFocus: ui.setCurrentFocus, + should_show_cancellation_warning: ui.should_show_cancellation_warning, + toggleCancellationWarning: ui.toggleCancellationWarning, + updateLimitOrder: modules.contract_trade.updateLimitOrder, +}))(PositionsModalCard); diff --git a/packages/reports/src/App/Components/Elements/PositionsDrawer/result-mobile.jsx b/packages/reports/src/App/Components/Elements/PositionsDrawer/result-mobile.jsx new file mode 100644 index 000000000000..a1578df1b511 --- /dev/null +++ b/packages/reports/src/App/Components/Elements/PositionsDrawer/result-mobile.jsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { CSSTransition } from 'react-transition-group'; +import { Icon } from '@deriv/components'; +import { localize } from '@deriv/translations'; + +const ResultMobile = ({ is_visible, result }) => { + const is_contract_won = result === 'won'; + return ( + + +
+ + {is_contract_won ? ( + + {localize('Won')} + + + ) : ( + + {localize('Lost')} + + + )} + +
+
+
+ ); +}; + +ResultMobile.propTypes = { + is_visible: PropTypes.bool, + result: PropTypes.string, +}; + +export default ResultMobile; diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/calendar-icon.jsx b/packages/reports/src/App/Components/Form/CompositeCalendar/calendar-icon.jsx new file mode 100644 index 000000000000..ddae60f2e40e --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/calendar-icon.jsx @@ -0,0 +1,11 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Icon } from '@deriv/components'; + +const CalendarIcon = ({ onClick }) => ; + +CalendarIcon.propTypes = { + onClick: PropTypes.func, +}; + +export default CalendarIcon; diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx b/packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx new file mode 100644 index 000000000000..c9e736552155 --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx @@ -0,0 +1,250 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, DatePicker, Icon, InputField, MobileDialog, Text } from '@deriv/components'; +import { localize } from '@deriv/translations'; +import { toMoment } from '@deriv/shared'; + +export const RadioButton = ({ id, className, selected_value, value, label, onChange }) => { + return ( + + ); +}; +const CUSTOM_KEY = 'custom'; + +const CompositeCalendarMobile = React.memo( + ({ input_date_range, current_focus, duration_list, onChange, setCurrentFocus }) => { + const date_range = input_date_range || duration_list.find(range => range.value === 'all_time'); + + const [from, setFrom] = React.useState(from && toMoment(from).format('DD MMM YYYY')); + const [to, setTo] = React.useState(to && toMoment(to).format('DD MMM YYYY')); + const [is_open, setIsOpen] = React.useState(false); + + const [applied_date_range, setAppliedDateRange] = React.useState(date_range); + const [selected_date_range, setSelectedDateRange] = React.useState(date_range); + + const selectDateRange = (_selected_date_range, is_today) => { + const new_from = _selected_date_range.duration; + onChange( + { + from: + is_today || new_from + ? toMoment().startOf('day').subtract(new_from, 'day').add(1, 's').unix() + : null, + to: toMoment().endOf('day').unix(), + is_batch: true, + }, + { + date_range: _selected_date_range, + } + ); + }; + + const selectCustomDateRange = () => { + const today = toMoment().format('DD MMM YYYY'); + + const new_from = from || to || today; + const new_to = to || today; + + const new_date_range = Object.assign(selected_date_range, { + label: `${new_from} - ${new_to}`, + }); + + onChange( + { + from: toMoment(new_from, 'DD MMM YYYY').startOf('day').add(1, 's').unix(), + to: toMoment(new_to, 'DD MMM YYYY').endOf('day').unix(), + is_batch: true, + }, + { + date_range: new_date_range, + } + ); + }; + + const applyDateRange = () => { + if (selected_date_range.onClick) { + selectDateRange(selected_date_range); + } else if (selected_date_range.value === CUSTOM_KEY) { + selectCustomDateRange(); + } + setAppliedDateRange(selected_date_range); + setIsOpen(false); + }; + + const selectToday = () => { + const new_date_range = { + duration: 0, + label: localize('Today'), + }; + selectDateRange(new_date_range, true); + setAppliedDateRange(new_date_range); + setSelectedDateRange(new_date_range); + setIsOpen(false); + }; + + const selectDate = (e, key) => { + setSelectedDateRange({ value: CUSTOM_KEY }); + + const value = e.target?.value ? toMoment(e.target.value).format('DD MMM YYYY') : ''; + + if (key === 'from') { + setFrom(value); + } + + if (key === 'to') { + setTo(value); + } + }; + + const getMobileFooter = () => { + return ( +
+
+ ); + }; + + const onDateRangeChange = _date_range => { + setSelectedDateRange( + duration_list.find(range => _date_range && range.value === _date_range.value) || _date_range + ); + }; + + const openDialog = () => { + setSelectedDateRange(applied_date_range); + setIsOpen(true); + }; + + const today = toMoment().format('YYYY-MM-DD'); + const max_date = to ? toMoment(to, 'DD MMM YYYY').format('YYYY-MM-DD') : today; + const min_date = from && toMoment(from, 'DD MMM YYYY').format('YYYY-MM-DD'); + + return ( + +
+ } + onClick={openDialog} + setCurrentFocus={setCurrentFocus} + value={applied_date_range.label} + /> +
+ setIsOpen(false)} + content_height_offset='94px' + footer={getMobileFooter()} + > +
+
+ {duration_list.map(duration => ( + + ))} +
+
+ + +
+ selectDate(e, 'from')} + /> + selectDate(e, 'to')} + /> +
+
+
+
+
+ ); + } +); + +CompositeCalendarMobile.displayName = 'CompositeCalendarMobile'; + +CompositeCalendarMobile.propTypes = { + current_focus: PropTypes.string, + duration_list: PropTypes.array, + from: PropTypes.number, + onChange: PropTypes.func, + setCurrentFocus: PropTypes.func, + to: PropTypes.number, +}; +export default CompositeCalendarMobile; diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar.jsx b/packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar.jsx new file mode 100644 index 000000000000..875fb85cfe0b --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/composite-calendar.jsx @@ -0,0 +1,191 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Loadable from 'react-loadable'; +import { DesktopWrapper, InputField, MobileWrapper, useOnClickOutside } from '@deriv/components'; +import { localize } from '@deriv/translations'; +import { daysFromTodayTo, epochToMoment, toMoment } from '@deriv/shared'; +import { connect } from 'Stores/connect'; +import CompositeCalendarMobile from './composite-calendar-mobile.jsx'; +import SideList from './side-list.jsx'; +import CalendarIcon from './calendar-icon.jsx'; + +const TwoMonthPicker = Loadable({ + loader: () => import(/* webpackChunkName: "two-month-picker" */ './two-month-picker.jsx'), + loading: () => null, + render(loaded, props) { + const Component = loaded.default; + return ; + }, +}); + +const CompositeCalendar = props => { + const { current_focus, onChange, setCurrentFocus, to, from } = props; + + const [show_to, setShowTo] = React.useState(false); + const [show_from, setShowFrom] = React.useState(false); + const [list] = React.useState([ + { + value: 'all_time', + label: localize('All time'), + onClick: () => selectDateRange(), + duration: 0, + }, + { + value: 'last_7_days', + label: localize('Last 7 days'), + onClick: () => selectDateRange(7), + duration: 7, + }, + { + value: 'last_30_days', + label: localize('Last 30 days'), + onClick: () => selectDateRange(30), + duration: 30, + }, + { + value: 'last_60_days', + label: localize('Last 60 days'), + onClick: () => selectDateRange(60), + duration: 60, + }, + { + value: 'last_quarter', + label: localize('Last quarter'), + onClick: () => selectDateRange(90), + duration: 90, + }, + ]); + + const wrapper_ref = React.useRef(); + + useOnClickOutside(wrapper_ref, event => { + event.stopPropagation(); + event.preventDefault(); + hideCalendar(); + }); + + const selectDateRange = new_from => { + hideCalendar(); + applyBatch({ + from: new_from ? toMoment().startOf('day').subtract(new_from, 'day').add(1, 's').unix() : null, + to: toMoment().endOf('day').unix(), + is_batch: true, + }); + }; + + const getToDateLabel = () => { + const date = epochToMoment(to); + return daysFromTodayTo(date) === 0 ? localize('Today') : date.format('MMM, DD YYYY'); + }; + + const getFromDateLabel = () => { + const date = epochToMoment(from); + return from ? date.format('MMM, DD YYYY') : ''; + }; + + const hideCalendar = () => { + setShowFrom(false); + setShowTo(false); + }; + + const showCalendar = e => { + if (e === 'from') { + setShowFrom(true); + } + if (e === 'to') { + setShowTo(true); + } + }; + + const setToDate = date => { + updateState('to', epochToMoment(date).endOf('day').unix()); + }; + + const setFromDate = date => { + updateState('from', date); + hideCalendar(); + }; + + const updateState = (key, value) => { + apply(key, value); + hideCalendar(); + }; + + const applyBatch = values => { + onChange(values); + }; + + const apply = (key, value) => { + applyBatch({ + [key]: value, + }); + }; + + const isPeriodDisabledTo = date => { + return date + 1 <= from || date > toMoment().endOf('day').unix(); + }; + + const isPeriodDisabledFrom = date => { + return date - 1 >= to; + }; + + return ( + + +
+ showCalendar('from')} + setCurrentFocus={setCurrentFocus} + value={getFromDateLabel()} + /> + showCalendar('to')} + setCurrentFocus={setCurrentFocus} + value={getToDateLabel()} + /> +
+ {show_to && ( +
+ + +
+ )} + {show_from && ( +
+ + +
+ )} +
+ + + +
+ ); +}; + +CompositeCalendar.displayName = 'CompositeCalendar'; + +CompositeCalendar.propTypes = { + current_focus: PropTypes.string, + from: PropTypes.number, + onChange: PropTypes.func, + setCurrentFocus: PropTypes.func, + to: PropTypes.number, +}; +export default React.memo( + connect(({ ui }) => ({ + current_focus: ui.current_focus, + setCurrentFocus: ui.setCurrentFocus, + }))(CompositeCalendar) +); diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/index.js b/packages/reports/src/App/Components/Form/CompositeCalendar/index.js new file mode 100644 index 000000000000..d8d748d8a924 --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/index.js @@ -0,0 +1 @@ +export default from './composite-calendar.jsx'; diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/list-item.jsx b/packages/reports/src/App/Components/Form/CompositeCalendar/list-item.jsx new file mode 100644 index 000000000000..98b3f9e551f6 --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/list-item.jsx @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import React from 'react'; + +const ListItem = ({ onClick, is_active, label }) => ( +
  • + {label} +
  • +); + +ListItem.propTypes = { + label: PropTypes.oneOfType([PropTypes.func, PropTypes.node, PropTypes.array]), + is_active: PropTypes.bool, + onClick: PropTypes.func, +}; + +export default ListItem; diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/side-list.jsx b/packages/reports/src/App/Components/Form/CompositeCalendar/side-list.jsx new file mode 100644 index 000000000000..69102262d646 --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/side-list.jsx @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { toMoment } from '@deriv/shared'; +import ListItem from './list-item.jsx'; + +const isActive = (from, to, flag) => { + if (flag === 0) { + return toMoment().endOf('day').unix() === to && from === null; + } + return Math.ceil(to / 86400) - Math.ceil(from / 86400) === flag; +}; + +const SideList = ({ items, from, to }) => ( +
      + {items.map(item => { + const { duration, ...rest_of_props } = item; + const is_active = isActive(from, to, duration); + return ; + })} +
    +); + +SideList.propTypes = { + items: PropTypes.array, +}; + +export default SideList; diff --git a/packages/reports/src/App/Components/Form/CompositeCalendar/two-month-picker.jsx b/packages/reports/src/App/Components/Form/CompositeCalendar/two-month-picker.jsx new file mode 100644 index 000000000000..d671140e0529 --- /dev/null +++ b/packages/reports/src/App/Components/Form/CompositeCalendar/two-month-picker.jsx @@ -0,0 +1,116 @@ +import PropTypes from 'prop-types'; +import moment from 'moment'; +import React from 'react'; +import { Calendar } from '@deriv/components'; +import { addMonths, diffInMonths, epochToMoment, subMonths, toMoment } from '@deriv/shared'; + +const TwoMonthPicker = React.memo(({ onChange, isPeriodDisabled, value }) => { + const [left_pane_date, setLeftPaneDate] = React.useState(subMonths(value, 1).unix()); + const [right_pane_date, setRightPaneDate] = React.useState(value); + + const navigateFrom = e => { + setLeftPaneDate(e.unix()); + setRightPaneDate(addMonths(e, 1).unix()); + }; + + /** + * Only allow previous months to be available to navigate. Disable other periods + * + * @param date + * @param range + * @returns {boolean} + */ + const validateFromArrows = (date, range) => { + return range === 'year' || diffInMonths(epochToMoment(left_pane_date), date) !== -1; + }; + + /** + * Validate values to be date_from < date_to + */ + const shouldDisableDate = date => { + return isPeriodDisabled(date.unix()); + }; + + /** + * Only allow next month to be available to navigate (unless next month is in the future). + * Disable other periods + * + * @param date + * @param range + * @returns {boolean} + */ + const validateToArrows = (date, range) => { + if (range === 'year') return true; // disallow year arrows + const r_date = epochToMoment(right_pane_date).startOf('month'); + if (diffInMonths(toMoment().startOf('month'), r_date) === 0) return true; // future months are disallowed + return diffInMonths(r_date, date) !== 1; + }; + + const jumpToCurrentMonth = () => { + const current_month = toMoment().endOf('month').unix(); + setLeftPaneDate(epochToMoment(current_month).endOf('month').subtract(1, 'month').unix()); + setRightPaneDate(current_month); + }; + + const navigateTo = e => { + setLeftPaneDate(subMonths(e, 1).unix()); + setRightPaneDate(toMoment(e).unix()); + }; + + const updateSelectedDate = e => { + onChange(moment.utc(e.currentTarget.dataset.date, 'YYYY-MM-DD').unix()); + }; + + return ( + +
    + ({})} + /> + +
    +
    + ({})} + /> + + +
    +
    + ); +}); + +TwoMonthPicker.displayName = 'TwoMonthPicker'; + +TwoMonthPicker.propTypes = { + isPeriodDisabled: PropTypes.func, + onChange: PropTypes.func, + value: PropTypes.number, +}; +export default TwoMonthPicker; diff --git a/packages/reports/src/App/Components/Form/RangeSlider/__tests__/range-slider.spec.js b/packages/reports/src/App/Components/Form/RangeSlider/__tests__/range-slider.spec.js new file mode 100644 index 000000000000..fb4c8f47e682 --- /dev/null +++ b/packages/reports/src/App/Components/Form/RangeSlider/__tests__/range-slider.spec.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import RangeSlider from '../range-slider'; + +describe('RangeSlider', () => { + it('should show 1 Tick if the value is 1', () => { + const value = 1; + const wrapper = render(); + expect(wrapper.getAllByText('1 Tick')).toHaveLength(1); + }); + + it('should show 2 Ticks if the value is 2', () => { + const value = 2; + const wrapper = render(); + expect(wrapper.getAllByText('2 Ticks')).toHaveLength(1); + }); + + it('should change the input with onchange target value', () => { + const ChangeMock = jest.fn(); + const wrapper = render(); + const input = wrapper.getByLabelText('range-input'); + fireEvent.change(input, { target: { value: 5 } }); + expect(input.value).toBe('5'); + }); +}); diff --git a/packages/reports/src/App/Components/Form/RangeSlider/__tests__/tick-steps.spec.js b/packages/reports/src/App/Components/Form/RangeSlider/__tests__/tick-steps.spec.js new file mode 100644 index 000000000000..095ffe7a42c7 --- /dev/null +++ b/packages/reports/src/App/Components/Form/RangeSlider/__tests__/tick-steps.spec.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TickSteps from '../tick-steps.jsx'; + +describe('TickSteps', () => { + const min_value = 1; + const max_value = 10; + const arr_ticks = [...Array(max_value - min_value + 1).keys()]; + it.each(arr_ticks)('should render 10 tick steps', item => { + const wrapper = render(); + expect(wrapper.getAllByTestId(`tick_step_${item + min_value}`)).toHaveLength(1); + }); +}); diff --git a/packages/reports/src/App/Components/Form/RangeSlider/index.js b/packages/reports/src/App/Components/Form/RangeSlider/index.js new file mode 100644 index 000000000000..6bb5b03cf31d --- /dev/null +++ b/packages/reports/src/App/Components/Form/RangeSlider/index.js @@ -0,0 +1 @@ +export default from './range-slider.jsx'; diff --git a/packages/reports/src/App/Components/Form/RangeSlider/range-slider.jsx b/packages/reports/src/App/Components/Form/RangeSlider/range-slider.jsx new file mode 100644 index 000000000000..9330f508c71e --- /dev/null +++ b/packages/reports/src/App/Components/Form/RangeSlider/range-slider.jsx @@ -0,0 +1,119 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { localize } from '@deriv/translations'; +import { Text } from '@deriv/components'; +import TickSteps from './tick-steps.jsx'; + +const RangeSlider = ({ className, name, value, min_value, max_value, onChange }) => { + const [hover_value, setHoverValue] = React.useState(0); + const range_slider_ref = React.useRef(); + + const handleChange = e => { + const target_value = +e.target.value; + if (target_value !== value) { + resetOnHover(); + onChange({ target: { name, value: target_value } }); + } + }; + + const handleClick = index => { + if (index !== value) { + resetOnHover(); + onChange({ target: { name, value: index } }); + } + }; + + const onMouseEnter = index => { + if (index) { + setHoverValue(index); + range_slider_ref.current.style.width = getRangeSliderTrackWidth(index, true); + } + }; + + const onMouseLeave = e => { + const { offsetX, offsetY } = e.nativeEvent; + if (offsetY <= -3 || offsetY >= 3 || offsetX < -3 || offsetX > 3) { + resetOnHover(); + } + }; + + const getRangeSliderTrackWidth = (slider_value, is_hover) => { + const width = (slider_value - min_value) * (10 / (max_value - min_value)); + return `${width * 2 + (is_hover ? 0.8 : 1.4)}em`; + }; + + const resetOnHover = () => { + if (hover_value) { + setHoverValue(0); + range_slider_ref.current.style.width = 0; + } + }; + + const display_value = hover_value || value; + const steps = !isNaN(max_value - min_value) ? max_value - min_value : 10; + return ( +
    max_value, + })} + > +