Skip to content

Commit

Permalink
馃捀 Buy token on sale modal (Joystream#4728)
Browse files Browse the repository at this point in the history
* Main modal

* Form step

* Terms step - initial

* Terms step - chart

* Storybook

* Add checkbox for terms

* Success modal

* Remove unnecessary component

* CR fixes

* CR fixes v2
  • Loading branch information
WRadoslaw committed Apr 22, 2024
1 parent da77aa8 commit b396faf
Show file tree
Hide file tree
Showing 8 changed files with 538 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
ref={mergeRefs([ref, textRef])}
>
{displayedValue ? <span>{displayedValue}</span> : <span>{formattedValue}</span>}
{withToken ? ` ${customTicker}` ?? ` ${atlasConfig.joystream.tokenTicker}` : null}
{withToken ? (customTicker ? ` ${customTicker}` : ` ${atlasConfig.joystream.tokenTicker}`) : null}
</StyledText>
{withDenomination === 'after' && (
<Text
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Meta, StoryFn } from '@storybook/react'

import { BuySaleTokenModal, BuySaleTokenModalProps } from '@/components/_crt/BuySaleTokenModal/BuySaleTokenModal'
import { JoystreamProvider } from '@/providers/joystream/joystream.provider'
import { OverlayManagerProvider } from '@/providers/overlayManager'

export default {
title: 'crt/BuySaleTokenModal',
component: BuySaleTokenModal,
decorators: [
(Story) => (
<JoystreamProvider>
<OverlayManagerProvider>
<Story />
</OverlayManagerProvider>
</JoystreamProvider>
),
],
} as Meta<BuySaleTokenModalProps>

const Template: StoryFn<BuySaleTokenModalProps> = (args) => <BuySaleTokenModal {...args} />

export const Default = Template.bind({})
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useMemo, useState } from 'react'

import { BuySaleTokenForm } from '@/components/_crt/BuySaleTokenModal/steps/BuySaleTokenForm'
import { BuySaleTokenSuccess } from '@/components/_crt/BuySaleTokenModal/steps/BuySaleTokenSuccess'
import { BuySaleTokenTerms, getTokenDetails } from '@/components/_crt/BuySaleTokenModal/steps/BuySaleTokenTerms'
import { DialogProps } from '@/components/_overlays/Dialog'
import { DialogModal } from '@/components/_overlays/DialogModal'
import { useMediaMatch } from '@/hooks/useMediaMatch'

export type BuySaleTokenModalProps = {
tokenId: string
onClose: () => void
}

enum BUY_SALE_TOKEN_STEPS {
form,
terms,
success,
}

export const BuySaleTokenModal = ({ tokenId, onClose }: BuySaleTokenModalProps) => {
const { title } = getTokenDetails(tokenId)
const userTokenAmount = 1000

const [activeStep, setActiveStep] = useState(BUY_SALE_TOKEN_STEPS.form)
const [primaryButtonProps, setPrimaryButtonProps] = useState<DialogProps['primaryButton']>()
const smMatch = useMediaMatch('sm')

const secondaryButton = useMemo(() => {
switch (activeStep) {
case BUY_SALE_TOKEN_STEPS.terms:
return {
text: 'Back',
onClick: () => {
setActiveStep(BUY_SALE_TOKEN_STEPS.form)
},
}
case BUY_SALE_TOKEN_STEPS.form:
return {
text: 'Cancel',
}
default:
return undefined
}
}, [activeStep])

const commonProps = {
setPrimaryButtonProps,
}

return (
<DialogModal
title={activeStep !== BUY_SALE_TOKEN_STEPS.success ? `Buy $${title}` : undefined}
onExitClick={activeStep !== BUY_SALE_TOKEN_STEPS.success ? onClose : undefined}
dividers={activeStep === BUY_SALE_TOKEN_STEPS.terms}
show
primaryButton={primaryButtonProps}
secondaryButton={secondaryButton}
noContentPadding={activeStep === BUY_SALE_TOKEN_STEPS.terms}
confetti={activeStep === BUY_SALE_TOKEN_STEPS.success && smMatch}
>
{activeStep === BUY_SALE_TOKEN_STEPS.form && (
<BuySaleTokenForm
{...commonProps}
onSubmit={() => setActiveStep(BUY_SALE_TOKEN_STEPS.terms)}
tokenId={tokenId}
/>
)}
{activeStep === BUY_SALE_TOKEN_STEPS.terms && (
<BuySaleTokenTerms
{...commonProps}
onSubmit={() => setActiveStep(BUY_SALE_TOKEN_STEPS.success)}
tokenId={tokenId}
tokenAmount={userTokenAmount}
/>
)}
{activeStep === BUY_SALE_TOKEN_STEPS.success && (
<BuySaleTokenSuccess {...commonProps} onClose={onClose} tokenName="JBC" />
)}
</DialogModal>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BuySaleTokenModal'
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useMemo, useState } from 'react'

import { FlexBox } from '@/components/FlexBox/FlexBox'
import { Information } from '@/components/Information'
import { JoyTokenIcon } from '@/components/JoyTokenIcon'
import { NumberFormat } from '@/components/NumberFormat'
import { Text } from '@/components/Text'
import { TextButton } from '@/components/_buttons/Button'
import { CommonProps } from '@/components/_crt/BuySaleTokenModal/steps/types'
import { FormField } from '@/components/_inputs/FormField'
import { TokenInput } from '@/components/_inputs/TokenInput'
import { DetailsContent } from '@/components/_nft/NftTile'
import { atlasConfig } from '@/config'
import { useMediaMatch } from '@/hooks/useMediaMatch'
import { useMountEffect } from '@/hooks/useMountEffect'

const getTokenDetails = (_: string) => ({
title: 'JBC',
pricePerUnit: 1000,
tokensOnSale: 67773,
userBalance: 100000,
})

type BuySaleTokenFormProps = {
tokenId: string
onSubmit: () => void
} & CommonProps

const currentJoyRate = 0.15

export const BuySaleTokenForm = ({ tokenId, setPrimaryButtonProps, onSubmit }: BuySaleTokenFormProps) => {
const { pricePerUnit, tokensOnSale, userBalance, title } = getTokenDetails(tokenId)

const [tokens, setTokens] = useState<number | null>(null)
const tokenInUsd = (tokens || 0) * pricePerUnit * currentJoyRate
const smMatch = useMediaMatch('sm')

const details = useMemo(
() => [
{
title: 'Tokens on sale',
content: (
<NumberFormat
value={tokensOnSale}
as="p"
variant="t200"
withDenomination="before"
withToken
customTicker={`$${title}`}
/>
),
tooltipText: 'Lorem ipsum',
},
{
title: 'You will get',
content: (
<NumberFormat
value={tokens || 0}
format={(tokens || 0) > 1_000_000 ? 'short' : 'full'}
as="p"
variant="t200"
withDenomination="before"
withToken
customTicker={`$${title}`}
/>
),
tooltipText: 'Lorem ipsum',
},
{
title: 'Fee',
content: <NumberFormat value={tokensOnSale} as="p" variant="t200" withDenomination="before" withToken />,
tooltipText: 'Lorem ipsum',
},
{
title: 'You will spend',
content: <NumberFormat value={tokensOnSale} as="p" variant="t200" withDenomination="before" withToken />,
tooltipText: 'Lorem ipsum',
},
],
[title, tokens, tokensOnSale]
)

useMountEffect(() => {
setPrimaryButtonProps({
text: 'Continue',
onClick: () => onSubmit(),
})
})

return (
<>
<FlexBox flow="column" gap={8}>
<FlexBox gap={6} equalChildren>
<DetailsContent
avoidIconStyling
tileSize={smMatch ? 'big' : 'bigSmall'}
caption="PRICE PER UNIT"
content={pricePerUnit}
icon={<JoyTokenIcon size={smMatch ? 24 : 16} variant="silver" />}
withDenomination
/>
<DetailsContent
avoidIconStyling
tileSize={smMatch ? 'big' : 'bigSmall'}
caption={`YOUR ${atlasConfig.joystream.tokenTicker} BALANCE`}
content={userBalance}
icon={<JoyTokenIcon size={smMatch ? 24 : 16} variant="silver" />}
withDenomination
/>
</FlexBox>
<FormField label="Tokens to spend">
<TokenInput
value={tokens}
onChange={setTokens}
placeholder="0"
nodeEnd={
<FlexBox gap={2} alignItems="baseline">
<Text variant="t300" as="p" color="colorTextMuted">
${tokenInUsd}
</Text>
<TextButton onClick={() => setTokens(Math.floor(userBalance / pricePerUnit))}>Max</TextButton>
</FlexBox>
}
/>
</FormField>

<FlexBox flow="column" gap={2}>
{details.map((row, i) => (
<FlexBox key={row.title} alignItems="center" justifyContent="space-between">
<FlexBox width="fit-content" alignItems="center">
<Text variant={i + 1 === details.length ? 't200-strong' : 't200'} as="p" color="colorText">
{row.title}
</Text>
<Information text={row.tooltipText} />
</FlexBox>
{row.content}
</FlexBox>
))}
</FlexBox>
</FlexBox>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { FC } from 'react'

import confettiAnimation from '@/assets/animations/confetti.json'
import { AppKV } from '@/components/AppKV'
import { LottiePlayer } from '@/components/LottiePlayer'
import { Text } from '@/components/Text'
import {
ContentWrapper,
IllustrationWrapper,
LottieContainer,
} from '@/components/_auth/SignUpModal/SignUpSteps/SignUpSuccessStep/SignUpSuccessStep.styles'
import { useMediaMatch } from '@/hooks/useMediaMatch'
import { useMountEffect } from '@/hooks/useMountEffect'

import { CommonProps } from './types'

type SignUpSuccessStepProps = {
tokenName?: string
coinImageUrl?: string
onClose: () => void
} & CommonProps

export const BuySaleTokenSuccess: FC<SignUpSuccessStepProps> = ({ tokenName, setPrimaryButtonProps, onClose }) => {
const smMatch = useMediaMatch('sm')

useMountEffect(() => {
setPrimaryButtonProps({
text: 'Continue',
onClick: () => onClose(),
})
})
return (
<>
<IllustrationWrapper>
<AppKV />
{!smMatch && (
<LottieContainer>
<LottiePlayer
size={{
height: 320,
width: 320,
}}
data={confettiAnimation}
/>
</LottieContainer>
)}
</IllustrationWrapper>
<ContentWrapper>
<Text variant="h500" as="h2" margin={{ bottom: 2 }}>
Congratulations on your purchase
</Text>
<Text variant="t200" as="p" color="colorText">
You are now an {tokenName} holder - you can buy, sell, transfer your token. You can also stake when creator
opens revenue share to claim your part of it.
</Text>
</ContentWrapper>
</>
)
}
Loading

0 comments on commit b396faf

Please sign in to comment.