diff --git a/packages/atlas/atlas.config.yml b/packages/atlas/atlas.config.yml index 27a0ea5eee..6f62281433 100644 --- a/packages/atlas/atlas.config.yml +++ b/packages/atlas/atlas.config.yml @@ -474,6 +474,8 @@ analytics: id: '$VITE_SEGMENT_ID' legal: + crtTnc: | + This is a temporary placeholder for the Creator Tokens Terms and Conditions. The final version will be added here soon. termsOfService: | # Terms of Service diff --git a/packages/atlas/package.json b/packages/atlas/package.json index 115f4a0116..3cc07b3bb6 100644 --- a/packages/atlas/package.json +++ b/packages/atlas/package.json @@ -117,13 +117,13 @@ "@joystream/prettier-config": "^1.0.0", "@modyfi/vite-plugin-yaml": "^1.0.3", "@rollup/plugin-babel": "^6.0.3", - "@storybook/addon-actions": "7.0.7", - "@storybook/addon-docs": "7.0.7", - "@storybook/addon-essentials": "7.0.7", - "@storybook/addon-links": "7.0.7", - "@storybook/addons": "7.0.7", - "@storybook/react-vite": "7.0.7", - "@storybook/theming": "7.0.7", + "@storybook/addon-actions": "7.4.6", + "@storybook/addon-docs": "7.4.6", + "@storybook/addon-essentials": "7.4.6", + "@storybook/addon-links": "7.4.6", + "@storybook/addons": "7.4.6", + "@storybook/react-vite": "7.4.6", + "@storybook/theming": "7.4.6", "@svgr/cli": "^6.5.1", "@testing-library/dom": "^8.19.0", "@testing-library/jest-dom": "^5.16.5", @@ -151,7 +151,7 @@ "react-hooks-testing-library": "^0.6.0", "rimraf": "^3.0.2", "rollup-plugin-visualizer": "^5.8.3", - "storybook": "7.0.7", + "storybook": "7.4.6", "style-dictionary": "^3.7.1", "vite": "^4.3.9", "vite-plugin-checker": "^0.5.2", diff --git a/packages/atlas/src/components/NumberFormat/NumberFormat.tsx b/packages/atlas/src/components/NumberFormat/NumberFormat.tsx index 24bdf9ffcf..c630bdba28 100644 --- a/packages/atlas/src/components/NumberFormat/NumberFormat.tsx +++ b/packages/atlas/src/components/NumberFormat/NumberFormat.tsx @@ -230,7 +230,7 @@ const dollarSmallNumberFormatter = new Intl.NumberFormat('en-US', { maximumSignificantDigits: 3, }) -const formatNumberShort = (num: number): string => { +export const formatNumberShort = (num: number): string => { return numberCompactFormatter.format(num).replaceAll(',', ' ') } diff --git a/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx b/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx index 3a26e5f6b2..e6dc2f0cb1 100644 --- a/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx +++ b/packages/atlas/src/components/_auth/SignUpModal/SignUpModal.tsx @@ -54,7 +54,7 @@ export const SignUpModal = () => { const [emailAlreadyTakenError, setEmailAlreadyTakenError] = useState(false) const [hasNavigatedBack, setHasNavigatedBack] = useState(false) const [primaryButtonProps, setPrimaryButtonProps] = useState({ text: 'Continue' }) - const [amountOfTokens, setAmountofTokens] = useState() + const [amountOfTokens] = useState() const [memberId, setMemberId] = useState(null) const syncState = useRef<'synced' | 'tried' | null>(null) const ytResponseData = useYppStore((state) => state.ytResponseData) @@ -128,7 +128,7 @@ export const SignUpModal = () => { setAuthModalOpenName(undefined) setYppModalOpenName('ypp-sync-options') } else { - setAmountofTokens(amountOfTokens) + amountOfTokens goToNextStep() } }, @@ -174,7 +174,7 @@ export const SignUpModal = () => { setAuthModalOpenName(undefined) setYppModalOpenName('ypp-sync-options') } else { - setAmountofTokens(amountOfTokens) + amountOfTokens goToNextStep() } }, diff --git a/packages/atlas/src/components/_charts/LineChart/LineChart.tsx b/packages/atlas/src/components/_charts/LineChart/LineChart.tsx index e733961aac..07d33cb005 100644 --- a/packages/atlas/src/components/_charts/LineChart/LineChart.tsx +++ b/packages/atlas/src/components/_charts/LineChart/LineChart.tsx @@ -4,6 +4,31 @@ import { ReactNode } from 'react' import { cVar, sizes } from '@/styles' +export const defaultChartTheme = { + tooltip: { + container: { + background: cVar('colorBackgroundStrong'), + }, + }, + + axis: { + ticks: { + line: { + stroke: cVar('colorBackgroundAlpha'), + }, + text: { + fill: cVar('colorTextMuted'), + font: cVar('typographyDesktopT100'), + }, + }, + }, + grid: { + line: { + stroke: cVar('colorBackgroundAlpha'), + }, + }, +} + const defaultJoystreamProps: Omit = { isInteractive: true, useMesh: true, @@ -29,29 +54,7 @@ const defaultJoystreamProps: Omit = { tickValues: 6, }, colors: (d) => d.color, - theme: { - tooltip: { - container: { - background: cVar('colorBackgroundStrong'), - }, - }, - axis: { - ticks: { - line: { - stroke: cVar('colorBackgroundAlpha'), - }, - text: { - fill: cVar('colorTextMuted'), - font: cVar('typographyDesktopT100'), - }, - }, - }, - grid: { - line: { - stroke: cVar('colorBackgroundAlpha'), - }, - }, - }, + theme: defaultChartTheme, } export type LineChartProps = { tooltip?: (point: Point) => ReactNode diff --git a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.stories.tsx b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.stories.tsx new file mode 100644 index 0000000000..8b5a38564a --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.stories.tsx @@ -0,0 +1,49 @@ +import { ApolloProvider } from '@apollo/client' +import { Meta, StoryFn } from '@storybook/react' + +import { createApolloClient } from '@/api' +import { CrtMarketSaleViewProps, MarketDrawer } from '@/components/_crt/MarketDrawer' +import { AuthProvider } from '@/providers/auth/auth.provider' +import { ConfirmationModalProvider } from '@/providers/confirmationModal' +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/CrtMarket', + component: MarketDrawer, + decorators: [ + (Story) => ( + + + + + + + + + + + + + + + + + + ), + ], +} as Meta + +const Template: StoryFn = (args) => + +export const Default = Template.bind({}) +Default.args = { + show: true, + onClose: () => { + return null + }, + tokenName: 'JBC', +} diff --git a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.styles.ts b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.styles.ts new file mode 100644 index 0000000000..1e112d2d9b --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.styles.ts @@ -0,0 +1,13 @@ +import styled from '@emotion/styled' + +import { sizes } from '@/styles' +import { Divider } from '@/views/global/NftSaleBottomDrawer/NftForm/AcceptTerms/AcceptTerms.styles' + +export const ChartBox = styled.div` + height: 300px; + width: 100%; +` + +export const HDivider = styled(Divider)` + margin: ${sizes(4)} 0; +` diff --git a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx new file mode 100644 index 0000000000..4c810a4d0c --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.tsx @@ -0,0 +1,108 @@ +import { useCallback, useRef, useState } from 'react' +import { flushSync } from 'react-dom' +import { CSSTransition, SwitchTransition } from 'react-transition-group' + +import { ActionDialogButtonProps } from '@/components/ActionBar' +import { CrtDrawer } from '@/components/CrtDrawer' +import { CrtMarketForm } from '@/components/_crt/MarketDrawer/MarketDrawer.types' +import { MarketDrawerPreview } from '@/components/_crt/MarketDrawer/MarketDrawerPreview' +import { MarketStep } from '@/components/_crt/MarketDrawer/steps/MarketStep' +import { SaleSummaryStep } from '@/components/_crt/MarketDrawer/steps/SaleSummaryStep' +import { atlasConfig } from '@/config' +import { transitions } from '@/styles' + +enum MARKET_STEPS { + market, + saleSummary, +} +const marketStepsNames: string[] = ['Market', 'Sale summary'] + +export type CrtMarketSaleViewProps = { + tokenName: string + show: boolean + onClose: () => void +} + +export const MarketDrawer = ({ show, onClose, tokenName }: CrtMarketSaleViewProps) => { + const [activeStep, setActiveStep] = useState(MARKET_STEPS.market) + const [marketData, setMarketData] = useState({ + price: 0, + tnc: atlasConfig.legal.crtTnc, + isChecked: true, + }) + const [primaryButtonProps, setPrimaryButtonProps] = useState({ text: 'Continue' }) + const [secondaryButtonProps, setSecondaryButtonProps] = useState({ text: 'Back' }) + const [isGoingBack, setIsGoingBack] = useState(false) + const nodeRef = useRef(null) + + const handleNextStep = useCallback( + ({ price, tnc }: CrtMarketForm) => { + setMarketData({ ...marketData, price, tnc }) + setActiveStep(MARKET_STEPS.saleSummary) + }, + [marketData] + ) + + const handleBackClick = useCallback(() => { + flushSync(() => { + setIsGoingBack(true) + }) + setActiveStep(MARKET_STEPS.market) + }, []) + + const stepContent = () => { + switch (activeStep) { + case MARKET_STEPS.market: + return ( + + ) + case MARKET_STEPS.saleSummary: + return ( + + ) + } + } + + return ( + } + > + + { + nodeRef.current?.addEventListener('transitionend', done, false) + }} + onEntered={() => setIsGoingBack(false)} + classNames={isGoingBack ? transitions.names.backwardSlideSwitch : transitions.names.forwardSlideSwitch} + > +
{stepContent()}
+
+
+
+ ) +} diff --git a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.types.ts b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.types.ts new file mode 100644 index 0000000000..38554f0ec2 --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawer.types.ts @@ -0,0 +1,5 @@ +export type CrtMarketForm = { + price: number + isChecked: boolean + tnc: string +} diff --git a/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawerPreview.tsx b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawerPreview.tsx new file mode 100644 index 0000000000..101e840f79 --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/MarketDrawerPreview.tsx @@ -0,0 +1,99 @@ +import { Datum } from '@nivo/line' + +import { SvgJoyTokenMonochrome16 } from '@/assets/icons' +import { NumberFormat, formatNumberShort } from '@/components/NumberFormat' +import { Text } from '@/components/Text' +import { LineChart, defaultChartTheme } from '@/components/_charts/LineChart' +import { TooltipBox } from '@/components/_crt/CreateTokenDrawer/steps/styles' +import { ChartBox } from '@/components/_crt/MarketDrawer/MarketDrawer.styles' +import { cVar } from '@/styles' + +type MarketDrawerPreviewProps = { + tokenName: string + startingPrice: number +} + +const DEFAULT_AMM_SENSIVITY = 1 +export const MarketDrawerPreview = ({ tokenName, startingPrice }: MarketDrawerPreviewProps) => { + const issuedTokens = [10 ** 3, 10 ** 4, 5 * 10 ** 4, 10 ** 5, 5 * 10 ** 5, 10 ** 6, 10 ** 7] + + const chartData: Datum[] = issuedTokens.map((num) => ({ + x: formatNumberShort(num), + y: DEFAULT_AMM_SENSIVITY * num + startingPrice, + })) + + const getTickValues = () => [ + ...new Set( + issuedTokens.map((elem) => { + const floor = Math.pow(10, Math.round(Math.log10(DEFAULT_AMM_SENSIVITY * elem + startingPrice))) + return Math.max(Math.floor(elem / floor), 1) * floor + }) + ), + ] + + return ( + <> + + { + return ( + + + + + + {point.data.xFormatted} {tokenName} supply + + + ) + }} + yScale={{ + type: 'log', + base: 2, + min: 'auto', + max: 'auto', + }} + axisLeft={{ + tickSize: 5, + tickPadding: 5, + tickValues: getTickValues(), + ticksPosition: 'before', + format: (tick) => formatNumberShort(tick), + // eslint-disable-next-line + // @ts-ignore + renderTick: ({ x, y, textX, textY, opacity, textBaseline, value, format }) => { + const iconX = textX - 18 + const iconY = textY - 8 + return ( + + + + {format && format(value)} + + + ) + }, + }} + gridYValues={getTickValues()} + data={[ + { + id: 1, + color: cVar('colorCoreBlue500'), + data: chartData, + }, + ]} + enableCrosshair={false} + /> + + + ) +} diff --git a/packages/atlas/src/components/_crt/MarketDrawer/index.ts b/packages/atlas/src/components/_crt/MarketDrawer/index.ts new file mode 100644 index 0000000000..19a92d5425 --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/index.ts @@ -0,0 +1 @@ +export * from './MarketDrawer' diff --git a/packages/atlas/src/components/_crt/MarketDrawer/steps/MarketStep.tsx b/packages/atlas/src/components/_crt/MarketDrawer/steps/MarketStep.tsx new file mode 100644 index 0000000000..39ff2dc0a3 --- /dev/null +++ b/packages/atlas/src/components/_crt/MarketDrawer/steps/MarketStep.tsx @@ -0,0 +1,191 @@ +import { FC, useCallback, useEffect } from 'react' +import { Controller, useForm } from 'react-hook-form' + +import { SvgActionPlay } from '@/assets/icons' +import { ActionDialogButtonProps } from '@/components/ActionBar' +import { FlexBox } from '@/components/FlexBox' +import { NumberFormat } from '@/components/NumberFormat' +import { ColumnBox } from '@/components/ProgressWidget/ProgressWidget.styles' +import { Text } from '@/components/Text' +import { TextButton } from '@/components/_buttons/Button' +import { CrtMarketForm } from '@/components/_crt/MarketDrawer/MarketDrawer.types' +import { Checkbox } from '@/components/_inputs/Checkbox' +import { FormField } from '@/components/_inputs/FormField' +import { TextArea } from '@/components/_inputs/TextArea' +import { TokenInput } from '@/components/_inputs/TokenInput' +import { atlasConfig } from '@/config' +import { useConfirmationModal } from '@/providers/confirmationModal' +import { useJoystream } from '@/providers/joystream' + +type MarketStepProps = { + setPrimaryButtonProps: (props: ActionDialogButtonProps) => void + setSecondaryButtonProps: (props: ActionDialogButtonProps) => void + tokenName: string + formDefaultValue: CrtMarketForm + onClose: () => void + onNextStep: (props: CrtMarketForm) => void +} + +const DEFAULT_MIN_PRICE = 100 + +export const MarketStep: FC = ({ + tokenName, + setPrimaryButtonProps, + onNextStep, + formDefaultValue, + setSecondaryButtonProps, + onClose, +}) => { + const { tokenPrice } = useJoystream() + const { + control, + handleSubmit, + resetField, + watch, + formState: { isDirty, errors }, + } = useForm({ + defaultValues: formDefaultValue, + }) + + const [openDialog, closeDialog] = useConfirmationModal({ + type: 'warning', + title: 'Discard changes?', + description: + 'You have unsaved changes which are going to be lost if you close this window. Are you sure you want to continue?', + primaryButton: { + variant: 'warning', + text: 'Confirm and discard', + onClick: () => { + closeDialog() + onClose() + }, + }, + secondaryButton: { + text: 'Cancel', + onClick: () => closeDialog(), + }, + }) + + const isChecked = watch('isChecked') + const price = watch('price') + + const tokenInUsd = (price || 0) * (tokenPrice || 0) + + const handleGoToNextStep = useCallback(() => { + handleSubmit((data) => { + onNextStep(data) + })() + }, [handleSubmit, onNextStep]) + + useEffect(() => { + setPrimaryButtonProps({ + text: 'Next', + onClick: () => { + handleGoToNextStep() + }, + }) + setSecondaryButtonProps({ + text: 'Cancel', + onClick: () => (isDirty ? onClose() : openDialog()), + }) + }, [handleGoToNextStep, isDirty, onClose, openDialog, setPrimaryButtonProps, setSecondaryButtonProps]) + + return ( + + + + Market + + } iconPlacement="left" color="colorTextPrimary"> + Learn more + + + + Automated market maker (AMM) will increase ${tokenName} price after each purchase and decrease its price when + someone sells it to the AMM. + + + You cannot set price lower than + + } + error={errors.price?.message} + > + ( + + ${tokenInUsd.toFixed(2)} + + } + /> + )} + rules={{ + validate: { + price: (value) => { + if (!value) { + return 'Enter starting token price' + } + return true + }, + minPrice: (value) => { + if (value < DEFAULT_MIN_PRICE) { + return `Price cannot be lower than ${DEFAULT_MIN_PRICE} ${atlasConfig.joystream.tokenTicker}` + } + return true + }, + }, + }} + name="price" + /> + + + + ( + { + if (checked) { + resetField('tnc') + } + onChange(checked) + }} + /> + )} + name="isChecked" + /> + { + if (!value) { + return 'You need to fill in the terms and conditions to proceed' + } + return true + }, + }, + }} + render={({ field: { value: tnc, onChange } }) => ( +