forked from Joystream/atlas
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🌳 CRT progress widget (Joystream#4569)
* Initial setup * Finishing touches for UI * Introduce data into widget * Create story * Lint fix * CR fixes
- Loading branch information
Showing
5 changed files
with
369 additions
and
0 deletions.
There are no files selected for viewing
48 changes: 48 additions & 0 deletions
48
packages/atlas/src/components/ProgressWidget/ProgressWidget.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Meta, StoryFn } from '@storybook/react' | ||
|
||
import { ProgressWidget, ProgressWidgetProps } from '@/components/ProgressWidget/ProgressWidget' | ||
import { Text } from '@/components/Text' | ||
|
||
import { TextButton } from '../_buttons/Button' | ||
|
||
export default { | ||
title: 'CRT/ProgressWidget', | ||
component: ProgressWidget, | ||
} as Meta<ProgressWidgetProps> | ||
|
||
const steps: ProgressWidgetProps['steps'] = [ | ||
{ | ||
title: 'Create token', | ||
description: 'Create own token and share it with your viewers!', | ||
primaryButton: { | ||
text: 'Create token', | ||
}, | ||
}, | ||
{ | ||
title: 'Share the good news', | ||
description: 'Share base information about your token with your viewers', | ||
primaryButton: { | ||
text: 'Share now', | ||
}, | ||
}, | ||
{ | ||
title: 'Take your profits', | ||
description: 'Visit dashboard and withdraw first revenue from your token', | ||
primaryButton: { | ||
text: 'To the moon!', | ||
}, | ||
}, | ||
] | ||
|
||
const Template: StoryFn<ProgressWidgetProps> = (args) => <ProgressWidget {...args} /> | ||
|
||
export const Default = Template.bind({}) | ||
Default.args = { | ||
activeStep: 2, | ||
steps, | ||
goalComponent: ( | ||
<Text variant="t200" as="p"> | ||
Complete 1 more step to achieve <TextButton>Token master</TextButton> | ||
</Text> | ||
), | ||
} |
113 changes: 113 additions & 0 deletions
113
packages/atlas/src/components/ProgressWidget/ProgressWidget.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import styled from '@emotion/styled' | ||
|
||
import { cVar, sizes, transitions } from '@/styles' | ||
|
||
export const Header = styled.div<{ progressWidth: string }>` | ||
background-color: ${cVar('colorBackgroundMuted')}; | ||
display: flex; | ||
width: 100%; | ||
justify-content: space-between; | ||
align-items: center; | ||
padding: ${sizes(9)} ${sizes(8)}; | ||
position: relative; | ||
overflow: hidden; | ||
::after { | ||
content: ' '; | ||
display: block; | ||
background-color: ${cVar('colorBackgroundPrimary')}; | ||
position: absolute; | ||
bottom: 0; | ||
height: 4px; | ||
left: 0; | ||
width: ${(props) => props.progressWidth}; | ||
} | ||
` | ||
|
||
export const RowBox = styled.div<{ gap: number; wrap?: boolean }>` | ||
display: flex; | ||
gap: ${(props) => sizes(props.gap)}; | ||
align-items: center; | ||
flex-wrap: ${(props) => (props.wrap ? 'wrap' : 'none')}; | ||
` | ||
|
||
export const ColumnBox = styled.div<{ gap: number }>` | ||
display: flex; | ||
flex-direction: column; | ||
gap: ${(props) => sizes(props.gap)}; | ||
width: 100%; | ||
` | ||
|
||
type DrawerProps = { | ||
maxHeight?: number | ||
isActive?: boolean | ||
} | ||
|
||
export const DetailsDrawer = styled.div<DrawerProps>` | ||
position: relative; | ||
display: flex; | ||
flex-direction: column; | ||
top: 0; | ||
width: 100%; | ||
max-height: ${({ isActive, maxHeight }) => (isActive ? `${maxHeight}px` : '0px')}; | ||
overflow: hidden; | ||
transition: max-height ${transitions.timings.loading} ${transitions.easing}; | ||
background-color: ${cVar('colorBackgroundMuted')}; | ||
` | ||
|
||
export const DropdownContainer = styled.div` | ||
display: flex; | ||
width: 100%; | ||
flex-direction: column; | ||
gap: ${sizes(10)}; | ||
padding: ${sizes(4)}; | ||
` | ||
|
||
export const ProgressBar = styled.div<{ progress: number }>` | ||
position: relative; | ||
height: 12px; | ||
width: 100%; | ||
background-color: ${cVar('colorBackground')}; | ||
border-radius: ${sizes(8)}; | ||
overflow: hidden; | ||
::after { | ||
content: ' '; | ||
position: absolute; | ||
left: 0; | ||
height: 100%; | ||
background-color: ${cVar('colorBackgroundPrimary')}; | ||
width: ${({ progress }) => `${progress}%`}; | ||
transition: width 1s linear; | ||
border-radius: ${sizes(8)}; | ||
min-width: 25px; | ||
} | ||
` | ||
|
||
export const StepCardContainer = styled.div<{ isActive: boolean }>` | ||
display: flex; | ||
flex-direction: column; | ||
gap: ${sizes(6)}; | ||
background-color: ${cVar('colorBackgroundMutedAlpha')}; | ||
border-left: 4px solid ${(props) => (props.isActive ? cVar('colorBackgroundPrimary') : 'transparent')}; | ||
padding: 16px; | ||
.step-number { | ||
background-color: ${(props) => | ||
props.isActive ? cVar('colorBackgroundPrimary') : cVar('colorBackgroundStrongAlpha')}; | ||
} | ||
` | ||
|
||
export const StepNumber = styled.div` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
background-color: ${cVar('colorBackgroundPrimary')}; | ||
border-radius: 50%; | ||
width: 28px; | ||
height: 28px; | ||
` | ||
|
||
export const MainWrapper = styled.div` | ||
position: relative; | ||
` |
179 changes: 179 additions & 0 deletions
179
packages/atlas/src/components/ProgressWidget/ProgressWidget.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { ReactNode, useRef, useState } from 'react' | ||
|
||
import { | ||
SvgActionCheck, | ||
SvgActionChevronB, | ||
SvgActionChevronL, | ||
SvgActionChevronR, | ||
SvgActionChevronT, | ||
SvgActionPlay, | ||
} from '@/assets/icons' | ||
import { Carousel, SwiperInstance } from '@/components/Carousel' | ||
import { | ||
ColumnBox, | ||
DetailsDrawer, | ||
DropdownContainer, | ||
Header, | ||
MainWrapper, | ||
ProgressBar, | ||
RowBox, | ||
StepCardContainer, | ||
StepNumber, | ||
} from '@/components/ProgressWidget/ProgressWidget.styles' | ||
import { Text } from '@/components/Text' | ||
import { Button, ButtonProps } from '@/components/_buttons/Button' | ||
import { useMediaMatch } from '@/hooks/useMediaMatch' | ||
|
||
import { getProgressPercentage, responsive } from './ProgressWidget.utils' | ||
|
||
type StepButtonProps = { | ||
text: string | ||
} & Omit<ButtonProps, 'children'> | ||
|
||
type StepProps = { | ||
title: string | ||
description: string | ||
primaryButton: StepButtonProps | ||
} | ||
|
||
export type ProgressWidgetProps = { | ||
steps: StepProps[] | ||
activeStep: number | ||
goalComponent: ReactNode | ||
} | ||
|
||
export const ProgressWidget = ({ steps, activeStep, goalComponent }: ProgressWidgetProps) => { | ||
const [isVisible, setIsVisible] = useState(false) | ||
const [glider, setGlider] = useState<SwiperInstance | null>(null) | ||
const drawer = useRef<HTMLDivElement>(null) | ||
const xsMatch = useMediaMatch('xs') | ||
const smMatch = useMediaMatch('sm') | ||
const isDone = activeStep + 1 > steps.length | ||
return ( | ||
<MainWrapper> | ||
<Header progressWidth={isVisible ? '0%' : `${getProgressPercentage(activeStep, steps.length)}%`}> | ||
<RowBox gap={4}> | ||
<Text variant="h500" as="h5"> | ||
Your progress | ||
</Text> | ||
{!isVisible && ( | ||
<Text variant="t200-strong" as="p"> | ||
({isDone ? steps.length : activeStep}/{steps.length}) | ||
</Text> | ||
)} | ||
</RowBox> | ||
{xsMatch && ( | ||
<Button | ||
onClick={() => setIsVisible((prev) => !prev)} | ||
icon={isVisible ? <SvgActionChevronT /> : <SvgActionChevronB />} | ||
iconPlacement="right" | ||
variant="tertiary" | ||
> | ||
{isVisible ? 'Show less' : 'Show more'} | ||
</Button> | ||
)} | ||
</Header> | ||
<DetailsDrawer isActive={isVisible} ref={drawer} maxHeight={drawer?.current?.scrollHeight}> | ||
<DropdownContainer> | ||
<RowBox gap={12}> | ||
<ExtendedProgressBar | ||
activeStep={steps[isDone ? steps.length - 1 : activeStep]} | ||
activeStepNumber={activeStep} | ||
totalSteps={steps.length} | ||
goalComponent={goalComponent} | ||
/> | ||
{smMatch && ( | ||
<RowBox gap={4}> | ||
<Button icon={<SvgActionChevronL />} onClick={() => glider?.slidePrev()} variant="secondary" /> | ||
<Button icon={<SvgActionChevronR />} onClick={() => glider?.slideNext()} variant="secondary" /> | ||
</RowBox> | ||
)} | ||
</RowBox> | ||
<Carousel | ||
initialSlide={activeStep + 1} | ||
spaceBetween={12} | ||
navigation | ||
dotsVisible | ||
breakpoints={responsive} | ||
onSwiper={(swiper) => setGlider(swiper)} | ||
> | ||
{steps.map((step, idx) => ( | ||
<StepCard | ||
key={step.title} | ||
step={step} | ||
status={idx === activeStep ? 'active' : idx < activeStep ? 'done' : 'next'} | ||
stepNumber={idx + 1} | ||
/> | ||
))} | ||
</Carousel> | ||
</DropdownContainer> | ||
</DetailsDrawer> | ||
</MainWrapper> | ||
) | ||
} | ||
type ExtendedProgressBarProps = { | ||
activeStep: StepProps | ||
activeStepNumber: number | ||
totalSteps: number | ||
goalComponent: ReactNode | ||
} | ||
const ExtendedProgressBar = ({ activeStep, activeStepNumber, totalSteps, goalComponent }: ExtendedProgressBarProps) => { | ||
const isDone = activeStepNumber + 1 > totalSteps | ||
|
||
return ( | ||
<ColumnBox gap={2}> | ||
<Text variant="t300-strong" as="p"> | ||
{activeStep.title} | ||
</Text> | ||
<RowBox gap={4}> | ||
<ProgressBar progress={getProgressPercentage(activeStepNumber, totalSteps)} /> | ||
<Text variant="t200-strong" as="p"> | ||
{isDone ? totalSteps : activeStepNumber}/{totalSteps} | ||
</Text> | ||
</RowBox> | ||
{goalComponent} | ||
</ColumnBox> | ||
) | ||
} | ||
|
||
type StepCardProps = { | ||
step: StepProps | ||
status: 'active' | 'done' | 'next' | ||
stepNumber: number | ||
} | ||
|
||
const StepCard = ({ step, status, stepNumber }: StepCardProps) => { | ||
const smMatch = useMediaMatch('xs') | ||
|
||
return ( | ||
<StepCardContainer isActive={status === 'active'}> | ||
<StepNumber className="step-number"> | ||
<Text variant="t200-strong" as="p"> | ||
{status === 'done' ? <SvgActionCheck /> : stepNumber} | ||
</Text> | ||
</StepNumber> | ||
<ColumnBox gap={2}> | ||
<Text variant="h300" as="h3"> | ||
{step.title} | ||
</Text> | ||
<Text variant="t200" as="p"> | ||
{step.description} | ||
</Text> | ||
</ColumnBox> | ||
|
||
<RowBox gap={4} wrap> | ||
<Button | ||
{...step.primaryButton} | ||
variant={status === 'active' ? 'primary' : 'secondary'} | ||
disabled={status === 'done'} | ||
fullWidth={!smMatch} | ||
> | ||
{step.primaryButton.text} | ||
</Button> | ||
<Button variant="tertiary" fullWidth={!smMatch} _textOnly icon={<SvgActionPlay />} iconPlacement="right"> | ||
Learn more | ||
</Button> | ||
</RowBox> | ||
</StepCardContainer> | ||
) | ||
} |
28 changes: 28 additions & 0 deletions
28
packages/atlas/src/components/ProgressWidget/ProgressWidget.utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { CarouselProps } from '@/components/Carousel' | ||
import { breakpoints } from '@/styles' | ||
|
||
export const responsive: CarouselProps['breakpoints'] = { | ||
[parseInt(breakpoints.xs)]: { | ||
slidesPerView: 1, | ||
slidesPerGroup: 1, | ||
}, | ||
[parseInt(breakpoints.sm)]: { | ||
slidesPerView: 2, | ||
slidesPerGroup: 2, | ||
}, | ||
[parseInt(breakpoints.lg)]: { | ||
slidesPerView: 3, | ||
slidesPerGroup: 3, | ||
}, | ||
[parseInt(breakpoints.xl)]: { | ||
slidesPerView: 4, | ||
slidesPerGroup: 4, | ||
}, | ||
[parseInt(breakpoints.xxl)]: { | ||
slidesPerView: 5, | ||
slidesPerGroup: 5, | ||
}, | ||
} | ||
|
||
export const getProgressPercentage = (activeStep: number, totalStepCount: number) => | ||
Math.round((activeStep / totalStepCount) * 100) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './ProgressWidget' |