From d9981ea77f5515e22808f85b386e438bc891bda4 Mon Sep 17 00:00:00 2001 From: WRadoslaw <92513933+WRadoslaw@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:01:11 +0200 Subject: [PATCH] :octocat: CRT flow previews (#4692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Setup preview * 🉑 POC Line chart (#4495) * Add nivo package * Add line chart * Add story for line chart * Fix styling * Chart setup * Style first preview * Correct chart previews * Fix slider mask * Scroll down of form change * Minor improvements on chart --- .../components/CrtDrawer/CrtDrawer.styles.ts | 8 +- .../src/components/CrtDrawer/CrtDrawer.tsx | 7 +- .../_charts/LineChart/LineChart.tsx | 11 +-- .../CreateTokenDrawer/CreateTokenDrawer.tsx | 29 ++++-- .../CreateTokenDrawer.types.ts | 2 +- .../steps/SetupTokenStep.tsx | 38 +++++++- .../TokenIssuanceStep/TokenIssuanceStep.tsx | 97 ++++++++++++++++++- .../TokenIssuanceStep.utils.ts | 71 ++++++++++++++ .../_crt/CreateTokenDrawer/steps/styles.ts | 49 ++++++++++ .../_crt/CreateTokenDrawer/steps/types.ts | 4 + .../CrtBasicInfoWidget/CrtBasicInfoWidget.tsx | 64 ++++++++++++ .../components/_inputs/Slider/RatioSlider.tsx | 2 +- 12 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 packages/atlas/src/components/_crt/CreateTokenDrawer/steps/styles.ts create mode 100644 packages/atlas/src/components/_crt/CrtBasicInfoWidget/CrtBasicInfoWidget.tsx diff --git a/packages/atlas/src/components/CrtDrawer/CrtDrawer.styles.ts b/packages/atlas/src/components/CrtDrawer/CrtDrawer.styles.ts index 136dff65c0..d89fc0ce57 100644 --- a/packages/atlas/src/components/CrtDrawer/CrtDrawer.styles.ts +++ b/packages/atlas/src/components/CrtDrawer/CrtDrawer.styles.ts @@ -11,17 +11,23 @@ export const StyledBottomDrawer = styled(BottomDrawer)` export const Container = styled.div` display: grid; - height: 100%; + overflow: auto; ${media.md} { + height: 100%; grid-template-columns: 1fr 1fr; } ` export const PreviewContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; width: 100%; background-color: #000; height: 100%; + overflow: hidden; + ${media.md} { grid-column: 1; grid-row: 1; diff --git a/packages/atlas/src/components/CrtDrawer/CrtDrawer.tsx b/packages/atlas/src/components/CrtDrawer/CrtDrawer.tsx index abc31bb8ea..4a6d4fdbf9 100644 --- a/packages/atlas/src/components/CrtDrawer/CrtDrawer.tsx +++ b/packages/atlas/src/components/CrtDrawer/CrtDrawer.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactNode } from 'react' +import { Fragment, MutableRefObject, ReactNode } from 'react' import { SvgActionChevronR } from '@/assets/icons' import { BottomDrawerProps } from '@/components/_overlays/BottomDrawer' @@ -19,13 +19,14 @@ export type CrtDrawerProps = { preview?: ReactNode steps: T[] activeStep: number + formWrapperRef?: MutableRefObject } & BottomDrawerProps -export const CrtDrawer = ({ children, preview, steps, activeStep, ...drawerProps }: CrtDrawerProps) => { +export const CrtDrawer = ({ children, preview, steps, activeStep, formWrapperRef, ...drawerProps }: CrtDrawerProps) => { const smMatch = useMediaMatch('sm') return ( - + {steps.map((step, idx) => ( diff --git a/packages/atlas/src/components/_charts/LineChart/LineChart.tsx b/packages/atlas/src/components/_charts/LineChart/LineChart.tsx index 0aecb90387..e733961aac 100644 --- a/packages/atlas/src/components/_charts/LineChart/LineChart.tsx +++ b/packages/atlas/src/components/_charts/LineChart/LineChart.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled' import { LineSvgProps, Point, ResponsiveLine } from '@nivo/line' +import { ReactNode } from 'react' -import { Text } from '@/components/Text' import { cVar, sizes } from '@/styles' const defaultJoystreamProps: Omit = { @@ -54,7 +54,7 @@ const defaultJoystreamProps: Omit = { }, } export type LineChartProps = { - tooltip?: (point: Point) => string + tooltip?: (point: Point) => ReactNode } & Omit export const LineChart = (props: LineChartProps) => { return ( @@ -62,11 +62,7 @@ export const LineChart = (props: LineChartProps) => { {...defaultJoystreamProps} {...props} tooltip={(point) => ( - - - {props.tooltip ? props.tooltip(point.point) : String(point.point.data.y)} - - + {props.tooltip ? props.tooltip(point.point) : String(point.point.data.y)} )} /> ) @@ -75,4 +71,5 @@ export const LineChart = (props: LineChartProps) => { const ChartTooltip = styled.div` background-color: ${cVar('colorBackgroundStrong')}; padding: ${sizes(1)} ${sizes(2)}; + border-radius: ${cVar('radiusSmall')}; ` diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx index 57fc0fbb0b..df294c90c2 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState } from 'react' +import { ReactNode, useMemo, useRef, useState } from 'react' import { flushSync } from 'react-dom' import { CSSTransition, SwitchTransition } from 'react-transition-group' @@ -41,6 +41,8 @@ export const CreateTokenDrawer = ({ show, onClose }: CreateTokenDrawerProps) => useState['primaryButton']>() const nodeRef = useRef(null) const [isGoingBack, setIsGoingBack] = useState(false) + const [preview, setPreview] = useState() + const formRef = useRef(null) const [openDialog, closeDialog] = useConfirmationModal({ type: 'warning', title: 'Discard changes?', @@ -60,6 +62,12 @@ export const CreateTokenDrawer = ({ show, onClose }: CreateTokenDrawerProps) => }, }) + const scrollFormDown = () => { + if (formRef.current) { + formRef.current.scrollTo({ top: formRef.current.scrollHeight, behavior: 'smooth' }) + } + } + const secondaryButton = useMemo(() => { switch (activeStep) { case CREATE_TOKEN_STEPS.setup: @@ -90,6 +98,13 @@ export const CreateTokenDrawer = ({ show, onClose }: CreateTokenDrawerProps) => } }, [activeStep, onClose, openDialog]) + const commonProps = { + setPrimaryButtonProps, + setPreview, + scrollFormDown, + form: formData.current, + } + return ( primaryButton: primaryButtonProps ?? {}, secondaryButton, }} + preview={preview} + formWrapperRef={formRef} >
{activeStep === CREATE_TOKEN_STEPS.setup && ( { formData.current = { ...formData.current, ...data } setActiveStep(CREATE_TOKEN_STEPS.issuance) }} - setPrimaryButtonProps={setPrimaryButtonProps} /> )} {activeStep === CREATE_TOKEN_STEPS.issuance && ( { formData.current = { ...formData.current, ...data } setActiveStep(CREATE_TOKEN_STEPS.summary) }} - setPrimaryButtonProps={setPrimaryButtonProps} /> )} - {activeStep === CREATE_TOKEN_STEPS.summary && ( - - )} + {activeStep === CREATE_TOKEN_STEPS.summary && }
diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.types.ts b/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.types.ts index 89ffac6a21..648a8d9959 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.types.ts +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/CreateTokenDrawer.types.ts @@ -9,7 +9,7 @@ export type SetupStepForm = { export type IssuanceStepForm = { creatorIssueAmount?: number - assuranceType: 'safe' | 'risky' | 'secure' | 'default' | 'custom' + assuranceType: 'safe' | 'risky' | 'secure' | 'custom' cliff: '0' | '1' | '3' | '6' | null vesting: '0' | '1' | '3' | '6' | null firstPayout?: number diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx index d8810f11ad..c0cc165737 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/SetupTokenStep.tsx @@ -1,8 +1,14 @@ +import { useLayoutEffect } from 'react' import { Controller, useForm } from 'react-hook-form' import { SvgActionLock, SvgActionUnlocked } from '@/assets/icons' import { Text } from '@/components/Text' import { SetupStepForm } from '@/components/_crt/CreateTokenDrawer/CreateTokenDrawer.types' +import { + BottomPlaceholder, + LeftPlaceholder, + WidgetPreviewContainer, +} from '@/components/_crt/CreateTokenDrawer/steps/styles' import { CrtFormWrapper } from '@/components/_crt/CrtFormWrapper' import { FormField } from '@/components/_inputs/FormField' import { Input } from '@/components/_inputs/Input' @@ -12,6 +18,8 @@ import { useMountEffect } from '@/hooks/useMountEffect' import { CommonStepProps } from './types' +import { CrtBasicInfoWidget } from '../../CrtBasicInfoWidget/CrtBasicInfoWidget' + const accessOptions = [ { label: 'Anyone', @@ -31,16 +39,19 @@ type SetupTokenStepProps = { onSubmit: (form: SetupStepForm) => void } & CommonStepProps -export const SetupTokenStep = ({ setPrimaryButtonProps, onSubmit, form }: SetupTokenStepProps) => { +export const SetupTokenStep = ({ setPrimaryButtonProps, onSubmit, form, setPreview }: SetupTokenStepProps) => { const { register, control, handleSubmit, + watch, formState: { errors }, } = useForm({ defaultValues: form, }) + const watchedForm = watch() + useMountEffect(() => { setPrimaryButtonProps({ text: 'Next step', @@ -48,6 +59,31 @@ export const SetupTokenStep = ({ setPrimaryButtonProps, onSubmit, form }: SetupT }) }) + useLayoutEffect(() => { + setPreview( + + + + + + + + + + + + + + ) + }, [setPreview, watchedForm.creatorReward, watchedForm.name, watchedForm.revenueShare]) + return ( void } & CommonStepProps -export const TokenIssuanceStep = ({ setPrimaryButtonProps, onSubmit, form }: TokenIssuanceStepProps) => { +export const TokenIssuanceStep = ({ + setPrimaryButtonProps, + onSubmit, + form, + setPreview, + scrollFormDown, +}: TokenIssuanceStepProps) => { const { control, watch, @@ -57,6 +76,7 @@ export const TokenIssuanceStep = ({ setPrimaryButtonProps, onSubmit, form }: Tok const creatorIssueAmount = watch('creatorIssueAmount') const customVesting = watch('vesting') const customCliff = watch('cliff') + const firstPayout = watch('firstPayout') useEffect(() => { if (assuranceType !== 'custom') { @@ -66,6 +86,10 @@ export const TokenIssuanceStep = ({ setPrimaryButtonProps, onSubmit, form }: Tok } }, [assuranceType, setValue]) + useLayoutEffect(() => { + scrollFormDown() + }, [customVesting, scrollFormDown]) + const getAssuranceDetails = () => { switch (assuranceType) { case 'secure': @@ -148,6 +172,60 @@ export const TokenIssuanceStep = ({ setPrimaryButtonProps, onSubmit, form }: Tok } } + useLayoutEffect(() => { + const data = + assuranceType === 'custom' + ? generateChartData(Number(customCliff ?? 0), Number(customVesting ?? 0), firstPayout ? firstPayout : 0) + : generateChartData(...getDataBasedOnType(assuranceType)) + setPreview( + + + How your tokens will unlock over time + + { + const currentDate = new Date() + const timeInMonths = point.data.x === 'Now' ? 0 : +(point.data.x as string).split('M')[0] + return ( + + + {formatNumber(((creatorIssueAmount ?? 0) * (point.data.y as number)) / 100)} ${form.name} + + + {point.data.x !== 'Now' + ? formatDate(new Date(currentDate.setMonth(currentDate.getMonth() + timeInMonths))) + : 'Now'} + + + ) + }} + yScale={{ + type: 'linear', + min: 0, + max: 'auto', + stacked: false, + reverse: false, + }} + axisLeft={{ + tickSize: 5, + tickPadding: 5, + tickValues: [0, 25, 50, 75, 100], + format: (tick) => `${tick}%`, + }} + gridYValues={[0, 25, 50, 75, 100]} + data={[ + { + id: 1, + color: cVar('colorTextPrimary'), + data, + }, + ]} + /> + + ) + }, [assuranceType, creatorIssueAmount, customCliff, customVesting, firstPayout, form.name, setPreview]) + return ( } + render={({ field }) => ( + { + flushSync(() => { + field.onChange(val) + }) + scrollFormDown() + }} + options={assuranceOptions} + /> + )} /> {getAssuranceDetails()} diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts index bc356f25e2..855950e685 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/TokenIssuanceStep/TokenIssuanceStep.utils.ts @@ -1,3 +1,4 @@ +import { Datum } from '@nivo/line' import { z } from 'zod' export const assuranceOptions = [ @@ -111,3 +112,73 @@ export const createTokenIssuanceSchema = (tokenName: string) => message: 'Select vesting for your token.', } ) + +export const generateChartData = (cliffTime: number, vestingTime: number, firstPayout = 0) => { + if (!cliffTime && !vestingTime) { + return [ + { + x: '', + y: '100%', + }, + { + x: 'Now', + y: '100%', + }, + ] + } + const data: Datum[] = [] + for (let i = 1; i <= cliffTime; i++) { + if (!cliffTime) break + if (!data.length) { + data.push({ + x: 'Now', + y: '0%', + }) + } + data.push({ + x: `${i}M`, + y: '0%', + }) + } + + if (firstPayout || !vestingTime) { + const lastDatum = data[data.length - 1] + if (lastDatum) { + data.push({ + x: lastDatum.x, + y: `${vestingTime ? firstPayout : 100}%`, + }) + } else { + data.push({ + x: 'Now', + y: `${vestingTime ? firstPayout : 100}%`, + }) + } + } + for (let i = cliffTime + 1; i <= cliffTime + vestingTime; i++) { + const partToVest = 100 - firstPayout + const vestingPerTick = partToVest / vestingTime + if (!data.length) { + data.push({ + x: 'Now', + y: '0%', + }) + } + data.push({ + x: `${i}M`, + y: `${vestingPerTick * (i - cliffTime) + firstPayout}%`, + }) + } + return data +} + +export const getDataBasedOnType = (type: 'secure' | 'safe' | 'risky'): [number, number, number] => { + switch (type) { + case 'secure': + return [6, 12, 50] + case 'safe': + return [0, 6, 50] + case 'risky': + return [0, 0, 0] + } +} diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/styles.ts b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/styles.ts new file mode 100644 index 0000000000..f7577860cb --- /dev/null +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/styles.ts @@ -0,0 +1,49 @@ +import styled from '@emotion/styled' + +import { cVar, sizes } from '@/styles' + +export const WidgetPreviewContainer = styled.div` + display: grid; + gap: ${sizes(4)}; + width: fit-content; + margin-top: 100px; + position: relative; +` + +export const LeftPlaceholder = styled.div` + grid-column: 1/2; + grid-row: 1/3; + background-color: ${cVar('colorBackgroundMuted')}; + opacity: 0.5; + position: absolute; + top: 0; + right: calc(100% + ${sizes(4)}); + width: 500px; + height: 1000px; +` + +export const BottomPlaceholder = styled.div` + height: 200px; + width: 100%; + background-color: ${cVar('colorBackgroundMuted')}; + padding: ${sizes(4)}; + opacity: 0.5; +` + +export const PreviewContainer = styled.div` + height: 300px; + margin: 100px 0; + display: flex; + flex-direction: column; + width: 100%; + + h1 { + padding: 0 60px; + } +` + +export const TooltipBox = styled.div` + text-align: center; + padding: ${sizes(1)}; + border-radius: ${cVar('radiusSmall')}; +` diff --git a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/types.ts b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/types.ts index 9f7474cd3e..a68b9eab9c 100644 --- a/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/types.ts +++ b/packages/atlas/src/components/_crt/CreateTokenDrawer/steps/types.ts @@ -1,8 +1,12 @@ +import { ReactNode } from 'react' + import { CrtDrawerProps } from '@/components/CrtDrawer' import { CreateTokenForm } from '../CreateTokenDrawer.types' export type CommonStepProps = { setPrimaryButtonProps: (props: NonNullable['primaryButton']) => void + setPreview: (preview: ReactNode) => void form: CreateTokenForm + scrollFormDown: () => void } diff --git a/packages/atlas/src/components/_crt/CrtBasicInfoWidget/CrtBasicInfoWidget.tsx b/packages/atlas/src/components/_crt/CrtBasicInfoWidget/CrtBasicInfoWidget.tsx new file mode 100644 index 0000000000..c45d6fa366 --- /dev/null +++ b/packages/atlas/src/components/_crt/CrtBasicInfoWidget/CrtBasicInfoWidget.tsx @@ -0,0 +1,64 @@ +import styled from '@emotion/styled' + +import { JoyTokenIcon } from '@/components/JoyTokenIcon' +import { WidgetTile } from '@/components/WidgetTile' +import { DetailsContent } from '@/components/_nft/NftTile' +import { useMediaMatch } from '@/hooks/useMediaMatch' +import { sizes } from '@/styles' + +type CrtBasicInfoWidgetProps = { + name?: string + totalRevenue?: string + revenueShare?: number + creatorReward?: number +} + +export const CrtBasicInfoWidget = ({ creatorReward, revenueShare, totalRevenue, name }: CrtBasicInfoWidgetProps) => { + const smMatch = useMediaMatch('sm') + + return ( + + {totalRevenue && ( + } + withDenomination + /> + )} + {revenueShare && ( + } + withDenomination + /> + )} + {creatorReward && ( + } + withDenomination + /> + )} + + } + /> + ) +} + +const DetailsBox = styled.div` + display: flex; + gap: ${sizes(4)}; +` diff --git a/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx b/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx index 538343df09..1bf01bb570 100644 --- a/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx +++ b/packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx @@ -52,7 +52,7 @@ export const RatioSlider = forwardRef( - +