diff --git a/packages/atlas/package.json b/packages/atlas/package.json index 86f9483d9a..b774dc6078 100644 --- a/packages/atlas/package.json +++ b/packages/atlas/package.json @@ -44,6 +44,7 @@ "@loadable/component": "^5.15.2", "@lottiefiles/react-lottie-player": "^3.5.0", "@nivo/line": "^0.83.0", + "@nivo/pie": "^0.83.0", "@segment/analytics-next": "^1.53.0", "@sentry/react": "^7.53.1", "@talismn/connect-wallets": "^1.2.1", diff --git a/packages/atlas/src/components/_charts/PieChart/PieChart.stories.tsx b/packages/atlas/src/components/_charts/PieChart/PieChart.stories.tsx new file mode 100644 index 0000000000..91db2ef955 --- /dev/null +++ b/packages/atlas/src/components/_charts/PieChart/PieChart.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, StoryFn } from '@storybook/react' + +import { PieChart, PieChartProps } from './PieChart' + +export default { + title: 'charts/PieChart', + component: PieChart, +} as Meta + +const Template: StoryFn = (args) => ( +
+ +
+) + +const data = [ + { + index: 0, + id: 'japan', + value: 40, + }, + { + index: 1, + id: 'korea', + value: 60, + }, +] + +export const Default = Template.bind({}) +Default.args = { + data, +} diff --git a/packages/atlas/src/components/_charts/PieChart/PieChart.tsx b/packages/atlas/src/components/_charts/PieChart/PieChart.tsx new file mode 100644 index 0000000000..f0461fdf48 --- /dev/null +++ b/packages/atlas/src/components/_charts/PieChart/PieChart.tsx @@ -0,0 +1,74 @@ +import { ComputedDatum, PieSvgProps, ResponsivePie } from '@nivo/pie' +import { MayHaveLabel } from '@nivo/pie/dist/types/types' +import { animated } from '@react-spring/web' +import { useState } from 'react' + +import { Text } from '@/components/Text' +import { cVar } from '@/styles' + +export type PieDatum = { + id: string + value: number + index: number +} & MayHaveLabel +type ReponsiveProps = Omit, 'width' | 'height'> + +export const joystreamColors = ['#9FACFF', '#7174FF', '#BECAFF', '#1B186C'] + +const defaultJoystreamProps: Omit = { + isInteractive: true, + enableArcLinkLabels: false, + arcLabelsRadiusOffset: 0.5, + arcLabelsComponent: (datum) => { + return ( + + + + {datum.datum.formattedValue} + + + + ) + }, + theme: { + tooltip: { + container: { + background: cVar('colorBackgroundStrong'), + }, + }, + }, +} + +export type PieChartProps = { + onDataHover?: (data: PieDatum | null) => void + hoveredData: PieDatum | null + hoverOpacity?: boolean +} & ReponsiveProps +export const PieChart = (props: PieChartProps) => { + const [hoveredEntry, setHoveredEntry] = useState | null>(null) + + const getColor = (entry: Omit, 'color' | 'fill' | 'arc'>) => { + const color = joystreamColors[entry.data.index % joystreamColors.length] + if (!props.hoverOpacity || entry.id === (props.hoveredData ? props.hoveredData.id : hoveredEntry?.id)) { + return color + } else { + return `${color}4D` + } + } + + return ( + { + setHoveredEntry(entry) + props.onDataHover?.(entry.data) + }} + onMouseLeave={() => { + setHoveredEntry(null) + props.onDataHover?.(null) + }} + colors={getColor} + {...defaultJoystreamProps} + {...props} + /> + ) +} diff --git a/packages/atlas/src/components/_charts/PieChart/index.ts b/packages/atlas/src/components/_charts/PieChart/index.ts new file mode 100644 index 0000000000..6017b793ef --- /dev/null +++ b/packages/atlas/src/components/_charts/PieChart/index.ts @@ -0,0 +1 @@ +export * from './PieChart' diff --git a/packages/atlas/src/components/_crt/CrtHoldersWidget/CrtHoldersWidget.stories.tsx b/packages/atlas/src/components/_crt/CrtHoldersWidget/CrtHoldersWidget.stories.tsx new file mode 100644 index 0000000000..dd9060a428 --- /dev/null +++ b/packages/atlas/src/components/_crt/CrtHoldersWidget/CrtHoldersWidget.stories.tsx @@ -0,0 +1,79 @@ +import { ApolloProvider } from '@apollo/client' +import { Meta, StoryFn } from '@storybook/react' + +import { createApolloClient } from '@/api' +import { CrtHoldersWidget, CrtHoldersWidgetProps } from '@/components/_crt/CrtHoldersWidget/CrtHoldersWidget' +import { AuthProvider } from '@/providers/auth/auth.provider' +import { JoystreamProvider } from '@/providers/joystream/joystream.provider' +import { OverlayManagerProvider } from '@/providers/overlayManager' +import { SegmentAnalyticsProvider } from '@/providers/segmentAnalytics/segment.provider' +import { UserProvider } from '@/providers/user/user.provider' +import { WalletProvider } from '@/providers/wallet/wallet.provider' + +export default { + title: 'crt/CrtHoldersWidget', + component: CrtHoldersWidget, + args: { + holders: [ + { + value: 50, + name: 'Bedeho', + members: [ + { + handle: 'Bedeho', + avatarUrls: [], + }, + ], + }, + { + value: 30, + name: 'Dima', + members: [ + { + handle: 'Dima', + avatarUrls: [], + }, + ], + }, + { + name: 'Others', + value: 20, + members: [ + { + handle: 'Radek', + avatarUrls: [], + }, + { + handle: 'Theo', + avatarUrls: [], + }, + ], + }, + ], + }, + decorators: [ + (Story) => { + const apolloClient = createApolloClient() + + return ( + + + + + + + + + + + + + + + + ) + }, + ], +} as Meta + +export const Default: StoryFn = (args) => diff --git a/packages/atlas/src/components/_crt/CrtHoldersWidget/CrtHoldersWidget.tsx b/packages/atlas/src/components/_crt/CrtHoldersWidget/CrtHoldersWidget.tsx new file mode 100644 index 0000000000..7d0dc60084 --- /dev/null +++ b/packages/atlas/src/components/_crt/CrtHoldersWidget/CrtHoldersWidget.tsx @@ -0,0 +1,175 @@ +import styled from '@emotion/styled' +import { useMemo, useState } from 'react' + +import { SvgActionChevronR } from '@/assets/icons' +import { Avatar } from '@/components/Avatar' +import { AvatarGroup } from '@/components/Avatar/AvatarGroup' +import { FlexBox } from '@/components/FlexBox' +import { Text } from '@/components/Text' +import { TextButton } from '@/components/_buttons/Button' +import { PieChart, PieDatum, joystreamColors } from '@/components/_charts/PieChart' +import { Widget } from '@/components/_crt/CrtStatusWidget/CrtStatusWidget.styles' +import { useUser } from '@/providers/user/user.hooks' +import { cVar } from '@/styles' + +export type HolderDatum = { + value: number + name: string + members: { + handle: string + avatarUrls: string[] + }[] +} + +export type CrtHoldersWidgetProps = { + holders: HolderDatum[] +} + +export const CrtHoldersWidget = ({ holders }: CrtHoldersWidgetProps) => { + const { activeMembership } = useUser() + const [hoveredHolder, setHoveredHolder] = useState(null) + const chartData = useMemo( + () => + holders.map((holder, index) => ({ + id: holder.name, + value: holder.value, + members: holder.members, + index, + })), + [holders] + ) + const owner = useMemo( + () => chartData.find((holder) => holder.id === activeMembership?.handle), + [chartData, activeMembership?.handle] + ) + return ( + } iconPlacement="right"> + Show more + + } + customNode={ + + + + TOTAL SUPPLY + + + `${value}%`} + /> + + + + + + YOU OWN + + {owner && ( + setHoveredHolder(owner)} + onMouseExit={() => setHoveredHolder(null)} + /> + )} + + + + TOP HOLDERS + + {chartData.map((row) => + row.id === activeMembership?.handle ? null : ( + setHoveredHolder(row)} + onMouseExit={() => setHoveredHolder(null)} + /> + ) + )} + + + + } + /> + ) +} + +type HoldersLegendEntryProps = { + name: string + value: number + color: string + isActive: boolean + onMouseEnter: () => void + onMouseExit: () => void + members: { + handle: string + avatarUrls: string[] + }[] +} + +const HoldersLegendEntry = ({ + name, + value, + color, + isActive, + onMouseExit, + onMouseEnter, + members, +}: HoldersLegendEntryProps) => { + return ( + + + + {members.length === 1 ? ( + + ) : ( + ({ urls: member.avatarUrls, tooltipText: member.handle }))} + avatarStrokeColor={cVar('colorBackgroundMuted')} + /> + )} + + {name} + + + + {value}% + + + ) +} + +const ColorBox = styled.div<{ color: string }>` + min-width: 24px; + min-height: 24px; + background-color: ${(props) => props.color}; +` + +const ChartWrapper = styled.div` + height: 300px; + width: 100%; +` diff --git a/packages/atlas/src/components/_crt/CrtHoldersWidget/index.ts b/packages/atlas/src/components/_crt/CrtHoldersWidget/index.ts new file mode 100644 index 0000000000..60235d8998 --- /dev/null +++ b/packages/atlas/src/components/_crt/CrtHoldersWidget/index.ts @@ -0,0 +1 @@ +export * from './CrtHoldersWidget' diff --git a/yarn.lock b/yarn.lock index 5096598dcc..3e912c0bc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4223,6 +4223,7 @@ __metadata: "@lottiefiles/react-lottie-player": ^3.5.0 "@modyfi/vite-plugin-yaml": ^1.0.3 "@nivo/line": ^0.83.0 + "@nivo/pie": ^0.83.0 "@rollup/plugin-babel": ^6.0.3 "@segment/analytics-next": ^1.53.0 "@sentry/react": ^7.53.1 @@ -4618,6 +4619,21 @@ __metadata: languageName: node linkType: hard +"@nivo/arcs@npm:0.83.0": + version: 0.83.0 + resolution: "@nivo/arcs@npm:0.83.0" + dependencies: + "@nivo/colors": 0.83.0 + "@nivo/core": 0.83.0 + "@react-spring/web": 9.4.5 || ^9.7.2 + "@types/d3-shape": ^2.0.0 + d3-shape: ^1.3.5 + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: cd76413c3bb826627534878a55d55c9cc11ac98895dc52cd14e33e3f07de95a126eb5e3412e39f8ebcf5327855959122961d7f07dff20ec5dcb3745bfcec1a67 + languageName: node + linkType: hard + "@nivo/axes@npm:0.83.0": version: 0.83.0 resolution: "@nivo/axes@npm:0.83.0" @@ -4717,6 +4733,23 @@ __metadata: languageName: node linkType: hard +"@nivo/pie@npm:^0.83.0": + version: 0.83.0 + resolution: "@nivo/pie@npm:0.83.0" + dependencies: + "@nivo/arcs": 0.83.0 + "@nivo/colors": 0.83.0 + "@nivo/core": 0.83.0 + "@nivo/legends": 0.83.0 + "@nivo/tooltip": 0.83.0 + "@types/d3-shape": ^2.0.0 + d3-shape: ^1.3.5 + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: f681d40d4ca2e67fba597cdb0c678454b5697ef8d98a53891b90cd19952b3db0b291512c52d7dd9d458b53f3c3b668825e3921beed6e947e9289708ef7455c30 + languageName: node + linkType: hard + "@nivo/recompose@npm:0.83.0": version: 0.83.0 resolution: "@nivo/recompose@npm:0.83.0"