From e619a013b883b4b7a2d7ca2e40723db29dfb273c Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Wed, 19 Jun 2024 16:50:02 +0200 Subject: [PATCH] feat: share PNL analytics (#678) --- .env.example | 3 + package.json | 1 + pnpm-lock.yaml | 123 ++++--- src/components/Icon.tsx | 6 + src/components/NumberValue.tsx | 4 +- src/components/QrCode.tsx | 12 +- src/constants/dialogs.ts | 1 + src/constants/twitter.ts | 1 + src/icons/download.svg | 5 + src/icons/index.ts | 2 + src/icons/logo.tsx | 81 +++++ src/icons/social-x.svg | 3 + src/layout/DialogManager.tsx | 2 + src/lib/twitter.ts | 18 ++ src/views/dialogs/SharePNLAnalyticsDialog.tsx | 306 ++++++++++++++++++ src/views/tables/PositionsTable.tsx | 22 +- .../PositionsTable/PositionsActionsCell.tsx | 41 ++- 17 files changed, 580 insertions(+), 51 deletions(-) create mode 100644 src/constants/twitter.ts create mode 100644 src/icons/download.svg create mode 100644 src/icons/logo.tsx create mode 100644 src/icons/social-x.svg create mode 100644 src/lib/twitter.ts create mode 100644 src/views/dialogs/SharePNLAnalyticsDialog.tsx diff --git a/.env.example b/.env.example index d07a68f03..5bf6743fe 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,9 @@ VITE_V3_TOKEN_ADDRESS= VITE_TOKEN_MIGRATION_URI= VITE_NUMIA_BASE_URL= +# URL for the qrcode that is generated within the modal share pnl analytics +VITE_SHARE_PNL_ANALYTICS_URL= + AMPLITUDE_API_KEY= AMPLITUDE_SERVER_URL= BUGSNAG_API_KEY= diff --git a/package.json b/package.json index 050c75b78..435b6b1d6 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@dydxprotocol/v4-client-js": "^1.1.20", "@dydxprotocol/v4-localization": "^1.1.127", "@ethersproject/providers": "^5.7.2", + "@hugocxl/react-to-image": "^0.0.9", "@js-joda/core": "^5.5.3", "@privy-io/react-auth": "^1.69.0", "@privy-io/wagmi-connector": "^0.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18f41f214..1ccd681d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: '@ethersproject/providers': specifier: ^5.7.2 version: 5.7.2 + '@hugocxl/react-to-image': + specifier: ^0.0.9 + version: 0.0.9(html-to-image@1.11.11)(react@18.2.0) '@js-joda/core': specifier: ^5.5.3 version: 5.5.3 @@ -519,10 +522,10 @@ packages: '@babel/helpers': 7.23.9 '@babel/parser': 7.23.9 '@babel/template': 7.23.9 - '@babel/traverse': 7.23.9(supports-color@5.5.0) + '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -632,7 +635,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.23.9 - '@babel/traverse': 7.23.9(supports-color@5.5.0) + '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 transitivePeerDependencies: - supports-color @@ -707,12 +710,29 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true + /@babel/traverse@7.23.9: + resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + /@babel/traverse@7.23.9(supports-color@5.5.0): resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} engines: {node: '>=6.9.0'} @@ -1871,7 +1891,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -2318,12 +2338,22 @@ packages: react: 18.2.0 dev: false + /@hugocxl/react-to-image@0.0.9(html-to-image@1.11.11)(react@18.2.0): + resolution: {integrity: sha512-UzPtjPb5k0V8oPKjmDvYnWtTNCuFh+2ysXF4+dXL0tnEaFDfu2M3iSt32pzKbJsZYoFu5X12JKKd9MKa2OsR6g==} + peerDependencies: + html-to-image: '>=1' + react: '>=16' + dependencies: + html-to-image: 1.11.11 + react: 18.2.0 + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -2570,7 +2600,7 @@ packages: '@babel/generator': 7.23.6 '@babel/parser': 7.23.9 '@babel/template': 7.23.9 - '@babel/traverse': 7.23.9(supports-color@5.5.0) + '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 '@ladle/react-context': 1.0.1(react-dom@18.2.0)(react@18.2.0) '@mdx-js/mdx': 3.0.0 @@ -2583,7 +2613,7 @@ packages: classnames: 2.3.2 commander: 11.1.0 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) get-port: 7.1.0 globby: 14.0.0 history: 5.3.0 @@ -2759,7 +2789,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) semver: 7.6.2 superstruct: 1.0.3 transitivePeerDependencies: @@ -2772,7 +2802,7 @@ packages: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) semver: 7.6.2 superstruct: 1.0.3 transitivePeerDependencies: @@ -2787,7 +2817,7 @@ packages: '@noble/hashes': 1.3.3 '@scure/base': 1.1.5 '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) pony-cause: 2.1.10 semver: 7.6.2 superstruct: 1.0.3 @@ -3283,7 +3313,7 @@ packages: typescript: optional: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.0 @@ -3300,7 +3330,7 @@ packages: engines: {node: '>=16.3.0'} hasBin: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.3.1 @@ -7062,7 +7092,7 @@ packages: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.1.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -7088,7 +7118,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.1.3 transitivePeerDependencies: @@ -7115,7 +7145,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.3) '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.3) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.1.3) typescript: 5.1.3 @@ -7139,7 +7169,7 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -9037,7 +9067,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -9046,7 +9076,7 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -10675,7 +10705,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 8.1.1 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -10946,7 +10975,7 @@ packages: /dns-over-http-resolver@1.2.3(node-fetch@3.3.2): resolution: {integrity: sha512-miDiVSI6KSNbi4SVifzO/reD8rMnxgrlnkrlkugOLQpWQTe2qMdHsZp5DmfKjxNE+/T3VAAYLQUZMv9SMr6+AA==} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) native-fetch: 3.0.0(node-fetch@3.3.2) receptacle: 1.3.2 transitivePeerDependencies: @@ -11510,7 +11539,7 @@ packages: eslint: '*' eslint-plugin-import: '*' dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.16.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) @@ -11751,7 +11780,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -12148,7 +12177,7 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -12565,7 +12594,7 @@ packages: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) fs-extra: 11.2.0 transitivePeerDependencies: - supports-color @@ -12840,7 +12869,6 @@ packages: /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: true /has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -13091,6 +13119,10 @@ packages: lru-cache: 10.2.2 dev: true + /html-to-image@1.11.11: + resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} + dev: false + /html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} dev: true @@ -13127,7 +13159,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -13150,7 +13182,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -13160,7 +13192,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true @@ -13362,7 +13394,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -13437,7 +13469,7 @@ packages: any-signal: 2.1.2 blob-to-it: 1.0.4 browser-readablestream-to-it: 1.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) err-code: 3.0.1 ipfs-core-types: 0.8.4(node-fetch@3.3.2) ipfs-unixfs: 6.0.9 @@ -13536,7 +13568,7 @@ packages: resolution: {integrity: sha512-fBYkRjN3/fc6IQujUF4WBEyOXegK715w+wx9IErV6H2B5JXsMnHOBceUKn3L90dj+wJfHs6T+hM/OZiTT6mQCw==} dependencies: cborg: 1.10.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) err-code: 3.0.1 interface-datastore: 6.1.1 libp2p-crypto: 0.21.2 @@ -14303,7 +14335,7 @@ packages: content-disposition: 0.5.4 content-type: 1.0.5 cookies: 0.8.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) delegates: 1.0.0 depd: 2.0.0 destroy: 1.2.0 @@ -15305,7 +15337,7 @@ packages: resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} dependencies: '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -15736,7 +15768,7 @@ packages: resolution: {integrity: sha512-8I2V7H2Ch0NvW7qWcjmS0/9Lhr0T6x7RD6PDirhvWEkUQvy83x8BA4haYMr09r/rig7hcgYSjYh6cd4U7G1vLA==} dependencies: '@open-draft/until': 1.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) headers-utils: 1.2.5 strict-event-emitter: 0.1.0 transitivePeerDependencies: @@ -16061,7 +16093,7 @@ packages: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) get-uri: 6.0.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 @@ -16478,7 +16510,7 @@ packages: requiresBuild: true dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 lru-cache: 7.18.3 @@ -16494,7 +16526,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.4 lru-cache: 7.18.3 @@ -16544,7 +16576,7 @@ packages: '@puppeteer/browsers': 1.4.6(typescript@5.1.3) chromium-bidi: 0.4.16(devtools-protocol@0.0.1147663) cross-fetch: 4.0.0(encoding@0.1.13) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) devtools-protocol: 0.0.1147663 typescript: 5.1.3 ws: 8.13.0 @@ -16638,7 +16670,7 @@ packages: dependencies: '@assemblyscript/loader': 0.9.4 bl: 5.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) minimist: 1.2.8 node-fetch: 2.6.12(encoding@0.1.13) readable-stream: 3.6.2 @@ -17608,7 +17640,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) socks: 2.8.1 transitivePeerDependencies: - supports-color @@ -18018,7 +18050,6 @@ packages: engines: {node: '>=10'} dependencies: has-flag: 4.0.0 - dev: true /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -18938,7 +18969,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.1 @@ -18985,7 +19016,7 @@ packages: vite: optional: true dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.0.1(typescript@5.1.3) vite: 5.0.12(@types/node@20.12.13) @@ -19107,7 +19138,7 @@ packages: cac: 6.7.14 chai: 4.3.7 concordance: 5.0.4 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) local-pkg: 0.4.3 magic-string: 0.30.8 pathe: 1.1.1 @@ -19193,7 +19224,7 @@ packages: dependencies: chalk: 4.1.2 commander: 9.5.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@8.1.1) transitivePeerDependencies: - supports-color dev: true diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 1d99df7b0..82a746923 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -29,6 +29,7 @@ import { DepositIcon, DepthChartIcon, DiscordIcon, + DownloadIcon, EtherscanIcon, ExportKeysIcon, FeedbackIcon, @@ -70,6 +71,7 @@ import { SendIcon, ShareIcon, ShowIcon, + SocialXIcon, StarIcon, SunIcon, TerminalIcon, @@ -167,6 +169,8 @@ export enum IconName { Website = 'Website', Whitepaper = 'Whitepaper', Withdraw = 'Withdraw', + Download = 'Download', + SocialX = 'SocialX', } const icons = { @@ -250,6 +254,8 @@ const icons = { [IconName.Website]: WebsiteIcon, [IconName.Whitepaper]: WhitepaperIcon, [IconName.Withdraw]: WithdrawIcon, + [IconName.Download]: DownloadIcon, + [IconName.SocialX]: SocialXIcon, } as Record; type ElementProps = { diff --git a/src/components/NumberValue.tsx b/src/components/NumberValue.tsx index 498f96c6c..62b6135b9 100644 --- a/src/components/NumberValue.tsx +++ b/src/components/NumberValue.tsx @@ -9,11 +9,13 @@ export type NumberValueProps = { }; export const NumberValue = ({ className, value, withSubscript }: NumberValueProps) => { - const { significantDigits, decimalDigits, zeros, punctuationSymbol } = formatZeroNumbers(value); + const { currencySign, significantDigits, decimalDigits, zeros, punctuationSymbol } = + formatZeroNumbers(value); if (withSubscript) { return ( + {currencySign} {significantDigits} {punctuationSymbol} {Boolean(zeros) && ( diff --git a/src/components/QrCode.tsx b/src/components/QrCode.tsx index 142e711c6..ec67d7c28 100644 --- a/src/components/QrCode.tsx +++ b/src/components/QrCode.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -import QRCodeStyling from 'qr-code-styling'; +import QRCodeStyling, { Options } from 'qr-code-styling'; import styled from 'styled-components'; import { useAppSelector } from '@/state/appTypes'; @@ -15,12 +15,19 @@ type StyleProps = { className?: string; hasLogo?: boolean; size?: number; + options?: Partial; }; const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg'; const LIGHT_LOGO_MARK_URL = '/logos/logo-mark-light.svg'; -export const QrCode = ({ className, value, hasLogo, size = 300 }: ElementProps & StyleProps) => { +export const QrCode = ({ + className, + value, + hasLogo, + size = 300, + options, +}: ElementProps & StyleProps) => { const ref = useRef(null); const appTheme: AppTheme = useAppSelector(getAppTheme); @@ -57,6 +64,7 @@ export const QrCode = ({ className, value, hasLogo, size = 300 }: ElementProps & qrOptions: { errorCorrectionLevel: 'M', }, + ...options, }) ); diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts index 026c9588d..8b6cc1270 100644 --- a/src/constants/dialogs.ts +++ b/src/constants/dialogs.ts @@ -35,6 +35,7 @@ export enum DialogTypes { Unstake = 'Unstake', Withdraw = 'Withdraw', WithdrawalGated = 'WithdrawalGated', + SharePNLAnalytics = 'SharePNLAnalytics', } export enum TradeBoxDialogTypes { diff --git a/src/constants/twitter.ts b/src/constants/twitter.ts new file mode 100644 index 000000000..d7c849941 --- /dev/null +++ b/src/constants/twitter.ts @@ -0,0 +1 @@ +export const TWITTER_BASE_URL = 'https://x.com'; diff --git a/src/icons/download.svg b/src/icons/download.svg new file mode 100644 index 000000000..94c1363be --- /dev/null +++ b/src/icons/download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/index.ts b/src/icons/index.ts index 7157e4ce3..381651f8c 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -22,6 +22,7 @@ export { default as CurrencySignIcon } from './currency-sign.svg'; export { default as DepositIcon } from './deposit.svg'; export { default as DepthChartIcon } from './depth-chart.svg'; export { default as DiscordIcon } from './discord.svg'; +export { default as DownloadIcon } from './download.svg'; export { default as ExportKeysIcon } from './export-keys.svg'; export { default as FeedbackIcon } from './feedback.svg'; export { default as FileIcon } from './file.svg'; @@ -58,6 +59,7 @@ export { default as SearchIcon } from './search.svg'; export { default as SendIcon } from './send.svg'; export { default as ShareIcon } from './share.svg'; export { default as ShowIcon } from './show.svg'; +export { default as SocialXIcon } from './social-x.svg'; export { default as StarIcon } from './star.svg'; export { default as SunIcon } from './sun.svg'; export { default as TerminalIcon } from './terminal.svg'; diff --git a/src/icons/logo.tsx b/src/icons/logo.tsx new file mode 100644 index 000000000..e37b8a324 --- /dev/null +++ b/src/icons/logo.tsx @@ -0,0 +1,81 @@ +import { useAppThemeAndColorModeContext } from '@/hooks/useAppThemeAndColorMode'; + +type LogoIconProps = { id?: string; className?: string }; + +export const LogoIcon = ({ id, className }: LogoIconProps) => { + const theme = useAppThemeAndColorModeContext(); + const fill = theme.logoFill; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/icons/social-x.svg b/src/icons/social-x.svg new file mode 100644 index 000000000..ed29ca0e9 --- /dev/null +++ b/src/icons/social-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx index 03b9ab4f2..772b2ca13 100644 --- a/src/layout/DialogManager.tsx +++ b/src/layout/DialogManager.tsx @@ -28,6 +28,7 @@ import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog'; import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog'; import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog'; import { SelectMarginModeDialog } from '@/views/dialogs/SelectMarginModeDialog'; +import { SharePNLAnalyticsDialog } from '@/views/dialogs/SharePNLAnalyticsDialog'; import { StakeDialog } from '@/views/dialogs/StakeDialog'; import { StakingRewardDialog } from '@/views/dialogs/StakingRewardDialog'; import { TradeDialog } from '@/views/dialogs/TradeDialog'; @@ -93,6 +94,7 @@ export const DialogManager = () => { [DialogTypes.Trade]: , [DialogTypes.Transfer]: , [DialogTypes.Triggers]: , + [DialogTypes.SharePNLAnalytics]: , [DialogTypes.Unstake]: , [DialogTypes.Withdraw]: , [DialogTypes.WithdrawalGated]: , diff --git a/src/lib/twitter.ts b/src/lib/twitter.ts new file mode 100644 index 000000000..52a22c3ef --- /dev/null +++ b/src/lib/twitter.ts @@ -0,0 +1,18 @@ +import { TWITTER_BASE_URL } from '@/constants/twitter'; + +export interface TwitterIntent { + text: string; + related?: string; +} + +export const triggerTwitterIntent = (props: TwitterIntent) => { + const { text, related } = props; + const twitterIntent = new URL('intent/tweet', TWITTER_BASE_URL); + twitterIntent.searchParams.append('text', text); + + if (related) { + twitterIntent.searchParams.append('related', related); + } + + window.open(twitterIntent, '_blank'); +}; diff --git a/src/views/dialogs/SharePNLAnalyticsDialog.tsx b/src/views/dialogs/SharePNLAnalyticsDialog.tsx new file mode 100644 index 000000000..ae0f86aa7 --- /dev/null +++ b/src/views/dialogs/SharePNLAnalyticsDialog.tsx @@ -0,0 +1,306 @@ +import { useMemo } from 'react'; + +import { useToBlob } from '@hugocxl/react-to-image'; +import styled from 'styled-components'; + +import { AbacusPositionSides, Nullable } from '@/constants/abacus'; +import { ButtonAction } from '@/constants/buttons'; +import { STRING_KEYS } from '@/constants/localization'; +import { PositionSide } from '@/constants/trade'; + +import { useStringGetter } from '@/hooks/useStringGetter'; + +import { LogoIcon } from '@/icons/logo'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Button } from '@/components/Button'; +import { Dialog } from '@/components/Dialog'; +import { Icon, IconName } from '@/components/Icon'; +import { Output, OutputType, ShowSign } from '@/components/Output'; +import { QrCode } from '@/components/QrCode'; +import { Tag, TagSign } from '@/components/Tag'; + +import { useAppDispatch } from '@/state/appTypes'; +import { closeDialog } from '@/state/dialogs'; + +import { MustBigNumber } from '@/lib/numbers'; +import { triggerTwitterIntent } from '@/lib/twitter'; + +type ElementProps = { + marketId: string; + assetId: string; + leverage: Nullable; + oraclePrice: Nullable; + entryPrice: Nullable; + unrealizedPnlPercent: Nullable; + side: Nullable; + sideLabel: Nullable; + setIsOpen: (open: boolean) => void; +}; + +const copyBlobToClipboard = async (blob: Blob | null) => { + if (!blob) { + return; + } + + const item = new ClipboardItem({ 'image/png': blob }); + await navigator.clipboard.write([item]); +}; + +export const SharePNLAnalyticsDialog = ({ + marketId, + assetId, + side, + sideLabel, + leverage, + oraclePrice, + entryPrice, + unrealizedPnlPercent, + setIsOpen, +}: ElementProps) => { + const stringGetter = useStringGetter(); + const dispatch = useAppDispatch(); + + const [{ isLoading: isCopying }, convert, ref] = useToBlob({ + quality: 1.0, + onSuccess: copyBlobToClipboard, + }); + + const [{ isLoading: isSharing }, convertShare, refShare] = useToBlob({ + quality: 1.0, + onSuccess: async (blob) => { + await copyBlobToClipboard(blob); + + triggerTwitterIntent({ + text: `Check out my ${assetId} position on @dYdX\n\n#dYdX #${assetId}\n[paste image and delete this!]`, + related: 'dYdX', + }); + + dispatch(closeDialog()); + }, + }); + + const sideSign = useMemo(() => { + switch (side?.name) { + case PositionSide.Long: + return TagSign.Positive; + case PositionSide.Short: + return TagSign.Negative; + default: + return TagSign.Neutral; + } + }, [side]); + + const unrealizedPnlIsNegative = MustBigNumber(unrealizedPnlPercent).isNegative(); + + const [assetLeft, assetRight] = marketId.split('-'); + + return ( + + <$ShareableCard + ref={(domNode) => { + if (domNode) { + ref(domNode); + refShare(domNode); + } + }} + > + <$ShareableCardSide> + <$ShareableCardTitle> + <$AssetIcon symbol={assetId} /> + + + <$ShareableCardTitleAsset>{assetLeft}/{assetRight} + + + {sideLabel} + + + <$HighlightOutput + isNegative={unrealizedPnlIsNegative} + type={OutputType.Percent} + value={unrealizedPnlPercent} + showSign={ShowSign.None} + slotLeft={ + !unrealizedPnlIsNegative ? ( + <$ArrowUpIcon iconName={IconName.Arrow} /> + ) : ( + <$ArrowDownIcon iconName={IconName.Arrow} /> + ) + } + /> + + <$Logo /> + + +
+ <$ShareableCardStats> + <$ShareableCardStatLabel> + {stringGetter({ key: STRING_KEYS.ENTRY })} + + <$ShareableCardStatOutput type={OutputType.Fiat} value={entryPrice} withSubscript /> + + <$ShareableCardStatLabel> + {stringGetter({ key: STRING_KEYS.INDEX })} + + <$ShareableCardStatOutput type={OutputType.Fiat} value={oraclePrice} withSubscript /> + + <$ShareableCardStatLabel> + {stringGetter({ key: STRING_KEYS.LEVERAGE })} + + + <$ShareableCardStatOutput + type={OutputType.Multiple} + value={leverage} + showSign={ShowSign.None} + /> + + + <$QrCode + size={68} + options={{ + margin: 0, + backgroundOptions: { + color: 'var(--color-layer-3)', + }, + imageOptions: { + margin: 0, + }, + }} + value={import.meta.env.VITE_SHARE_PNL_ANALYTICS_URL} + /> +
+ + + <$Actions> + <$Action + action={ButtonAction.Secondary} + slotLeft={} + onClick={() => { + convert(); + }} + state={{ + isLoading: isCopying, + }} + > + {stringGetter({ key: STRING_KEYS.COPY })} + + <$Action + action={ButtonAction.Primary} + slotLeft={} + onClick={() => { + convertShare(); + }} + state={{ + isLoading: isSharing, + }} + > + {stringGetter({ key: STRING_KEYS.SHARE })} + + +
+ ); +}; + +const $Actions = styled.div` + display: flex; + gap: 1rem; +`; + +const $Action = styled(Button)` + flex: 1; +`; + +const $ShareableCard = styled.div` + ${layoutMixins.row} + gap: 0.5rem; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.25rem; + background-color: var(--color-layer-4); + padding: 1.75rem 1.25rem 1.25rem 1.25rem; + border-radius: 0.5rem; +`; + +const $ShareableCardSide = styled.div` + ${layoutMixins.flexColumn} + height: 100%; +`; + +const $ShareableCardTitle = styled.div` + ${layoutMixins.row}; + gap: 0.5rem; + font-weight: var(--fontWeight-medium); + margin-bottom: 0.75rem; +`; + +const $ShareableCardTitleAsset = styled.span` + color: var(--color-white); +`; + +const $ShareableCardStats = styled.div` + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.125rem; + row-gap: 0.5rem; +`; + +const $ShareableCardStatLabel = styled.div` + font: var(--font-base-medium); + text-align: right; +`; + +const $ShareableCardStatOutput = styled(Output)` + font: var(--font-base-medium); + color: var(--color-text-2); + + * { + color: var(--color-text-2); + } +`; + +const $AssetIcon = styled(AssetIcon)` + height: 1.625rem; +`; + +const $QrCode = styled(QrCode)` + width: 5.25rem; + height: 5.25rem; + margin-top: 1rem; + margin-left: auto; + + svg { + border: none; + } +`; + +const $HighlightOutput = styled(Output)<{ isNegative?: boolean }>` + font-size: 2.25rem; + color: var(--output-sign-color); + --secondary-item-color: currentColor; + --output-sign-color: ${({ isNegative }) => + isNegative !== undefined + ? isNegative + ? `var(--color-negative)` + : `var(--color-positive)` + : `var(--color-text-1)`}; +`; + +const $ArrowUpIcon = styled(Icon)<{ negative?: boolean }>` + font-size: 1.5rem; + margin-right: 0.5rem; + transform: rotateZ(-90deg); +`; + +const $ArrowDownIcon = styled(Icon)<{ negative?: boolean }>` + font-size: 1.5rem; + margin-right: 0.5rem; + transform: rotateZ(90deg); +`; + +const $Logo = styled(LogoIcon)` + width: 5.125rem; + margin-top: auto; + height: auto; +`; diff --git a/src/views/tables/PositionsTable.tsx b/src/views/tables/PositionsTable.tsx index a2d1c9691..1e53ab838 100644 --- a/src/views/tables/PositionsTable.tsx +++ b/src/views/tables/PositionsTable.tsx @@ -357,10 +357,30 @@ const getPositionsTableColumnDef = ({ isActionable: true, allowsSorting: false, hideOnBreakpoint: MediaQueryKeys.isTablet, - renderCell: ({ id, assetId, stopLossOrders, takeProfitOrders }) => ( + renderCell: ({ + id, + assetId, + stopLossOrders, + leverage, + side, + oraclePrice, + entryPrice, + takeProfitOrders, + unrealizedPnlPercent, + resources, + }) => ( ; + oraclePrice: Nullable; + entryPrice: Nullable; + unrealizedPnlPercent: Nullable; + side: Nullable; + sideLabel: Nullable; stopLossOrders: SubaccountOrder[]; takeProfitOrders: SubaccountOrder[]; isDisabled?: boolean; @@ -36,6 +42,12 @@ type ElementProps = { export const PositionsActionsCell = ({ marketId, assetId, + leverage, + oraclePrice, + entryPrice, + unrealizedPnlPercent, + side, + sideLabel, stopLossOrders, takeProfitOrders, isDisabled, @@ -85,6 +97,26 @@ export const PositionsActionsCell = ({ ); }; + const openShareDialog = () => { + dispatch( + openDialog({ + type: DialogTypes.SharePNLAnalytics, + dialogProps: { + marketId, + assetId, + leverage, + oraclePrice, + entryPrice, + unrealizedPnlPercent, + side, + sideLabel, + stopLossOrders, + takeProfitOrders, + }, + }) + ); + }; + return ( {!isDisabled && complianceState === ComplianceStates.FULL_ACCESS && ( @@ -100,6 +132,13 @@ export const PositionsActionsCell = ({ /> )} + <$TriggersButton + key="share" + onClick={openShareDialog} + iconName={IconName.Share} + shape={ButtonShape.Square} + disabled={isDisabled} + /> {showClosePositionAction && ( <$CloseButtonToggle