Skip to content
This repository has been archived by the owner on Jun 24, 2022. It is now read-only.

Create and implement a Stepper component. #2246

Merged
merged 13 commits into from
Jan 21, 2022
7 changes: 6 additions & 1 deletion src/custom/assets/cow-swap/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 129 additions & 0 deletions src/custom/components/Stepper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import styled from 'styled-components/macro'
import CheckCircle from 'assets/cow-swap/check.svg'
import { transparentize } from 'polished'

export const Wrapper = styled.div`
width: 100%;
display: flex;
flex-flow: row wrap;
margin: 12px 0 24px;
`

export const Step = styled.div<{
totalSteps: number
isActiveStep: boolean
completedStep: boolean
circleSize?: number
}>`
--circleSize: 42px;
display: flex;
flex-flow: column wrap;
align-items: center;
position: relative;
flex: 1 1 ${({ totalSteps }) => `calc(100% / ${totalSteps})`};
fairlighteth marked this conversation as resolved.
Show resolved Hide resolved

&::before,
&::after {
content: '';
position: absolute;
top: ${({ circleSize }) => (circleSize ? `calc(${circleSize / 2})px` : 'calc(var(--circleSize) / 2)')};
height: 1px;
border-top: 1px solid ${({ theme }) => theme.border2};
}

&::before {
left: 0;
right: 50%;
margin-right: ${({ circleSize }) => (circleSize ? `${circleSize}px` : 'var(--circleSize)')};
}

&::after {
right: 0;
left: 50%;
margin-left: ${({ circleSize }) => (circleSize ? `${circleSize}px` : 'var(--circleSize)')};
}

&:first-child::before,
&:last-child::after {
content: none;
display: none;
}

> span {
display: flex;
flex-flow: column wrap;
align-items: center;
justify-content: center;
width: ${({ circleSize }) => (circleSize ? `${circleSize}px` : 'var(--circleSize)')};
height: ${({ circleSize }) => (circleSize ? `${circleSize}px` : 'var(--circleSize)')};
margin: 0 auto 12px;
border-radius: ${({ circleSize }) => (circleSize ? `${circleSize}px` : 'var(--circleSize)')};
text-align: center;
line-height: 1;
font-size: 100%;
position: relative;
color: ${({ isActiveStep, completedStep, theme }) =>
completedStep ? theme.black : isActiveStep ? theme.black : transparentize(0.4, theme.text1)};
background: ${({ isActiveStep, completedStep, theme, circleSize }) =>
completedStep
? `url(${CheckCircle}) no-repeat center/${circleSize ? `${circleSize}px` : 'var(--circleSize)'}`
: isActiveStep
? theme.primary1
: theme.blueShade3};

> small {
font-size: inherit;
color: inherit;
display: ${({ completedStep }) => (completedStep ? 'none' : 'block')};
}
}

> b {
color: ${({ isActiveStep, completedStep, theme }) =>
completedStep ? theme.text1 : isActiveStep ? theme.text1 : transparentize(0.4, theme.text1)};
font-weight: ${({ isActiveStep, completedStep }) => (completedStep ? '300' : isActiveStep ? 'bold' : '300')};
}

> i {
font-style: normal;
color: ${({ isActiveStep, completedStep, theme }) =>
completedStep
? transparentize(0.2, theme.text1)
: isActiveStep
? transparentize(0.2, theme.text1)
: transparentize(0.4, theme.text1)};
font-size: 12px;
margin: 6px 0 0;
padding: 0 24px;
text-align: center;
}
`

interface StepperProps {
steps: {
id: number
title: string
subtitle?: string
}[]
activeStep: number
}

export function Stepper({ steps, activeStep }: StepperProps) {
return (
<Wrapper>
{steps.map(({ id, title, subtitle }) => {
const completedStep = activeStep > id
const isActiveStep = activeStep === id
return (
<Step key={id} totalSteps={steps.length} isActiveStep={isActiveStep} completedStep={completedStep}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit overkill as there are only 3 steps but cool anyhow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just keeping in mind, this was setup as a re-usable component (outside Claim purposes).

<span>
<small>{id + 1}</small>
</span>
<b>{title}</b>
<i>{subtitle}</i>
</Step>
)
})}
</Wrapper>
)
}
2 changes: 1 addition & 1 deletion src/custom/pages/Claim/FooterNavButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function FooterNavButtons({
<>
{investFlowStep === 0 ? (
<ButtonPrimary onClick={() => setInvestFlowStep(1)}>
<Trans>Approve tokens</Trans>
<Trans>Continue</Trans>
</ButtonPrimary>
) : investFlowStep === 1 ? (
<ButtonPrimary onClick={() => setInvestFlowStep(2)}>
Expand Down
73 changes: 56 additions & 17 deletions src/custom/pages/Claim/InvestmentFlow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
InvestContent,
InvestFlowValidation,
InvestSummaryTable,
StepIndicator,
Steps,
ClaimTable,
AccountClaimSummary,
} from 'pages/Claim/styled'
import { InvestSummaryRow } from 'pages/Claim/InvestmentFlow/InvestSummaryRow'

import { Stepper } from 'components/Stepper'

import { ClaimType, useClaimState, useUserEnhancedClaimData, useClaimDispatchers } from 'state/claim/hooks'
import { ClaimStatus } from 'state/claim/actions'
import { InvestClaim } from 'state/claim/reducer'
Expand All @@ -23,6 +23,23 @@ import { useActiveWeb3React } from 'hooks/web3'
import InvestOption from './InvestOption'
import { ClaimCommonTypes, ClaimWithInvestmentData, EnhancedUserClaimData } from '../types'

const STEPS_DATA = [
{
id: 0,
title: 'Start',
},
{
id: 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need an id if the index/order is deterministic? it's an array

Copy link
Contributor Author

@fairlighteth fairlighteth Jan 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@W3stside My thinking was that this would be a more fool proof approach. Order of an Array in the context of this small Array of objects const indeed is deterministic.

In context of a general purpose Stepper component, my understanding is that in scenarios where you might have missing (object) values in the Array or externally imported data, one could argue setting explicit step numbers might be a defensive way of setting it up, given the order/index number might be affected in those scenarios. But correct me if I'm wrong.

Happy to do it another way, but just to highlight.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to review post merge. For now I reverted logic back to use index for key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then make id optional as a prop and use the index as a fallback

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure Michel, makes sense if you plan to feed this with dynamic data which doesn't have order guarantees.
My thinking was, it's just using a static deterministic array, and in practical terms it will always be the case. I don't see the stepper consuming a dynamic list from an untrusted REST API :P

Anyways, it's all good, i consider the comment a ubernit :)
Fine to leave it, fine to remove it, fine to make it optional

Copy link
Contributor Author

@fairlighteth fairlighteth Jan 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, could also be an option. A bit messy potentially in the end where index and id are thrown together and mixed up? I think I leave it as per your first suggestion and use only index for now. Feel free to modify post merge otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anxolin Yeah, probably I'm over thinking it at this moment :)

title: 'Set allowances',
subtitle: 'Approve all tokens to be used for investment.',
},
{
id: 2,
title: 'Submit claim',
subtitle: 'Submit and confirm the transaction to claim vCOW.',
},
]

export type InvestOptionProps = {
claim: EnhancedUserClaimData
optionIndex: number
Expand Down Expand Up @@ -109,7 +126,7 @@ export default function InvestmentFlow({ hasClaims, isAirdropOnly, ...tokenAppro
if (
!activeClaimAccount || // no connected account
!hasClaims || // no claims
!isInvestFlowActive || // not on correct step (account change in mid step)
!isInvestFlowActive || // not on correct step (account change in mid-step)
claimStatus !== ClaimStatus.DEFAULT || // not in default claim state
isAirdropOnly // is only for airdrop
) {
Expand All @@ -118,19 +135,42 @@ export default function InvestmentFlow({ hasClaims, isAirdropOnly, ...tokenAppro

return (
<InvestFlow>
<StepIndicator>
<Steps step={investFlowStep}>
<li>Allowances: Approve all tokens to be used for investment.</li>
<li>Submit and confirm the transaction to claim vCOW</li>
</Steps>
<h1>
{investFlowStep === 0
? 'Claiming vCOW is a two step process'
: investFlowStep === 1
? 'Set allowance to Buy vCOW'
: 'Confirm transaction to claim all vCOW'}
</h1>
</StepIndicator>
<Stepper steps={STEPS_DATA} activeStep={investFlowStep} />

<h1>
{investFlowStep === 0
? 'Claim and invest'
: investFlowStep === 1
? 'Set allowance to Buy vCOW'
: 'Confirm transaction to claim all vCOW'}
</h1>

{investFlowStep === 0 && (
<p>
You have chosen to exercise one or more investment opportunities alongside claiming your airdrop. Exercising
your investment options will give you the chance to acquire vCOW tokens at at fixed price. This process
consists of two steps.
<br />
<br />
The first step allows you to define the investment amounts and set the required allowances for the tokens you
will use to invest with.
<br />
<br />
The last step executes all claiming opportunities on-chain. Additionally, it sends the tokens you will use to
invest with, to the smart contract. In return, the smart contract will send the vCOW tokens for the Airdrop
and your 4 years linear vesting of the investment amount will start.
<br />
<br />
For more details around the token, please read{' '}
<a href="https://cow.fi" target="_blank" rel="noreferrer">
the blog post
</a>
<br />. For more details about the claiming process, please read the{' '}
fairlighteth marked this conversation as resolved.
Show resolved Hide resolved
<a href="https://cow.fi" target="_blank" rel="noreferrer">
step by step guide
</a>
</p>
)}

{/* Invest flow: Step 1 > Set allowances and investment amounts */}
{investFlowStep === 1 ? (
Expand All @@ -152,7 +192,6 @@ export default function InvestmentFlow({ hasClaims, isAirdropOnly, ...tokenAppro
<InvestFlowValidation>Approve all investment tokens before continuing</InvestFlowValidation>
</InvestContent>
) : null}

{/* Invest flow: Step 2 > Review summary */}
{investFlowStep === 2 ? (
<InvestContent>
Expand Down
Loading