feature: stepper component (#91)

This commit is contained in:
Camila Sosa Morales 2023-01-30 20:46:13 -05:00 committed by GitHub
parent 77b20d527d
commit 325fdb8361
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 475 additions and 21 deletions

View File

@ -2,6 +2,7 @@ import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import { initializeWallet } from './store';
import { themeGlobals } from '@/theme/globals';
import { Home } from './views';
import { Mint } from './views/mint';
import { SVGTestScreen } from './views/svg-test'; // TODO: remove when done
initializeWallet();
@ -13,6 +14,7 @@ export const App = () => {
<BrowserRouter>
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/mint" element={<Mint />} />
<Route path="/svg" element={<SVGTestScreen />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>

View File

@ -1,4 +1,4 @@
import { dripStitches } from '../../theme/stitches'; //TODO replace with absolute path
import { dripStitches } from '../../theme'; //TODO replace with absolute path
import React from 'react';
const { styled } = dripStitches;

View File

@ -0,0 +1 @@
export * from './stepper';

View File

@ -0,0 +1,48 @@
import { Button } from '../../core';
import { Stepper } from './stepper';
import { Flex } from '../flex.styled';
export default {
title: 'Components/Layout/Stepper',
component: Stepper,
};
const StepperButton: React.FC = () => {
const { nextStep, prevStep, setStep } = Stepper.useContext();
return (
<Flex css={{ gap: '$md' }}>
<Button onClick={prevStep}>Prev</Button>
<Button onClick={nextStep}>Next</Button>
<Button onClick={() => setStep(4)}>Set final step</Button>
</Flex>
);
};
export const Default = () => {
return (
<>
<Stepper.Root initialStep={1} totalSteps={4}>
<Stepper.Container>
<Stepper.Step>
{/* Step 1 */}
<Stepper.Indicator />
</Stepper.Step>
<Stepper.Step>
{/* Step 2*/}
<Stepper.Indicator />
</Stepper.Step>
<Stepper.Step>
{/* Step 3 */}
<Stepper.Indicator />
</Stepper.Step>
<Stepper.Step>
{/* Step 4 */}
<Stepper.Indicator />
</Stepper.Step>
</Stepper.Container>
<StepperButton />
</Stepper.Root>
</>
);
};

View File

@ -0,0 +1,39 @@
import { dripStitches } from '../../../theme';
const { styled } = dripStitches;
export abstract class StepperStyles {
static readonly Rail = styled('div', {
position: 'relative',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '0.8rem',
width: '100%',
'&:before': {
content: '""',
position: 'absolute',
bottom: 0,
height: '1px',
width: '100%',
backgroundColor: '$border-default',
zIndex: -1,
},
});
static readonly RailDivision = styled('div', {
height: '$2h',
width: '$13',
borderRadius: '$full',
backgroundColor: '$slate6',
'&[data-active="true"]': {
backgroundColor: '$blue10',
},
});
static readonly RailDivisionLabel = styled('span', {
color: '$blue10',
mt: '$3',
});
}

View File

@ -0,0 +1,147 @@
import React, { useMemo, useState } from 'react';
import { createContext } from '../../..//utils';
import { Flex } from '../flex.styled';
import { StepperStyles } from './stepper.styles';
export type SelectContext = {
totalSteps: number;
currentStep: number;
nextStep: () => void;
prevStep: () => void;
setStep: (step: number) => void;
};
const [Provider, useContext] = createContext<SelectContext>({
name: 'Stepper.Context',
hookName: 'Stepper.useContext',
providerName: 'Stepper.Provider',
});
const getStepsLength = (node: React.ReactNode): number => {
let length = 0;
React.Children.forEach(node, (child) => {
if (React.isValidElement(child)) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (child.type === Stepper.Step) length += 1;
else length += getStepsLength(child.props.children);
}
});
return length;
};
export abstract class Stepper {
static readonly useContext = useContext;
static readonly Root: React.FC<Stepper.RootProps> = ({
children,
initialStep = 0,
}) => {
const [currentStep, setCurrentStep] = useState(initialStep - 1);
const totalSteps = useMemo(() => getStepsLength(children), [children]);
const nextStep = (): void => {
if (currentStep < totalSteps - 1) {
setCurrentStep(currentStep + 1);
}
};
const prevStep = (): void => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const setStep = (step: number): void => {
if (step > 0 && step <= totalSteps) {
setCurrentStep(step - 1);
}
};
return (
<Provider
value={{ totalSteps, currentStep, nextStep, prevStep, setStep }}
>
{children}
</Provider>
);
};
static readonly Container = (props: Stepper.ContainerProps): JSX.Element => {
const { children } = props;
const { currentStep } = this.useContext();
const filteredChildren = useMemo(
() =>
React.Children.toArray(children).map((child, index) => {
if (!React.isValidElement(child)) {
throw new Error(
'Stepper.Container children must be a valid React element'
);
}
if (child.type !== Stepper.Step) {
throw new Error(
'Stepper.Container children must be a Stepper.Step component'
);
}
if (index === currentStep) {
return child;
}
return null;
}),
[children, currentStep]
);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{filteredChildren}</>;
};
static readonly Step = ({
children,
}: // eslint-disable-next-line react/jsx-no-useless-fragment
Stepper.StepProps): JSX.Element => <>{children}</>;
static readonly Indicator: React.FC<Stepper.IndicatorProps> = (props) => {
const { currentStep, totalSteps } = this.useContext();
const steps = Array.from(Array(totalSteps).keys());
return (
<Flex css={{ flexDirection: 'column' }}>
<StepperStyles.Rail {...props}>
{steps.map((step) => (
<StepperStyles.RailDivision
key={step}
data-active={step <= currentStep}
/>
))}
</StepperStyles.Rail>
<StepperStyles.RailDivisionLabel>
Step {currentStep + 1}
</StepperStyles.RailDivisionLabel>
</Flex>
);
};
}
export namespace Stepper {
export type RootProps = {
children: React.ReactNode;
initialStep?: number;
};
export type StepIndex = string | number;
export type ContainerProps = {
children: React.ReactNode | React.ReactNode[];
};
export type StepProps = {
children: React.ReactNode;
};
export type IndicatorProps = React.ComponentProps<typeof StepperStyles.Rail>;
}

View File

@ -16,6 +16,7 @@ export const spacing = {
10: '2.5rem',
11: '2.75rem',
12: '3rem',
13: '3.25rem', // 52px
14: '3.5rem',
16: '4rem',
20: '5rem', // 80px
@ -23,6 +24,7 @@ export const spacing = {
24: '6rem',
28: '7rem',
32: '8rem',
34: '8.5rem',
36: '9rem',
40: '10rem',
44: '11rem',
@ -34,4 +36,9 @@ export const spacing = {
72: '18rem',
80: '20rem',
96: '24rem',
100: '25rem',
102: '25.5rem', // 408px
104: '26rem', // 416px
106: '26.5rem', // 424px
128: '32rem', // 512px
};

44
ui/src/utils/context.ts Normal file
View File

@ -0,0 +1,44 @@
import {
createContext as createReactContext,
useContext as useReactContext,
} from 'react';
export interface CreateContextOptions {
hookName: string;
providerName: string;
name: string;
}
export type CreateContextReturn<T> = [
React.Provider<T>,
() => T,
React.Context<T>
];
const getErrorMessage = (hook: string, provider: string): string =>
`${hook} returned \`undefined\`. Seems you forgot to wrap component within ${provider}`;
export const createContext = <T>({
name,
hookName,
providerName,
}: CreateContextOptions): CreateContextReturn<T> => {
const Context = createReactContext<T | undefined>(undefined);
Context.displayName = name;
const useContext = (): T => {
const context = useReactContext(Context);
if (!context) {
const error = new Error(getErrorMessage(hookName, providerName));
error.name = 'ContextError';
Error.captureStackTrace?.(error, useContext);
throw error;
}
return context;
};
return [Context.Provider, useContext, Context] as CreateContextReturn<T>;
};

View File

@ -1,3 +1,4 @@
export * from './format';
export * from './validation';
export * from './object';
export * from './context';

View File

@ -1,22 +1,3 @@
import { Flex } from '@/components';
import { Form } from '@/components';
import React, { useState } from 'react';
export const Home = () => {
const [file, setFile] = useState<File | null>(null);
return (
<Flex
css={{
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<h1>Home</h1>
<Form.Field css={{ width: '$24' }}>
<Form.Label>Label</Form.Label>
<Form.LogoFileInput value={file} onChange={(file) => setFile(file)} />
</Form.Field>
</Flex>
);
return <h1>Home</h1>;
};

View File

@ -0,0 +1 @@
export * from './mint';

View File

@ -0,0 +1,168 @@
import { Button, Flex } from '@/components';
import { IconButton } from '@/components/core/button/icon-button';
import { Icon } from '@/components/core/icon';
import React from 'react';
import { Stepper } from '@/components/layout/stepper/stepper';
// TODO remove after flow integration
const StepperButton: React.FC = () => {
const { nextStep, prevStep, setStep } = Stepper.useContext();
return (
<Flex css={{ gap: '$md' }}>
<Button onClick={prevStep} variant="outline">
Prev
</Button>
<Button onClick={nextStep} variant="outline">
Next
</Button>
<Button onClick={() => setStep(4)} variant="outline">
Set final step
</Button>
</Flex>
);
};
const CardHeading = ({ title }: { title: string }) => {
const { currentStep, prevStep } = Stepper.useContext();
return (
<Flex css={{ justifyContent: 'space-between' }}>
<Flex>
{currentStep > 0 && (
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
onClick={prevStep}
css={{ mr: '$2' }}
/>
)}
<h3 style={{ fontSize: '20px', fontWeight: '700' }}>{title}</h3>
</Flex>
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
</Flex>
);
};
type CardProps = {
children: React.ReactNode;
title: string;
};
// TODO create card component for all the project and then remove this
const Card = ({ children, title }: CardProps) => (
// TODO style with stitches
<div
style={{
width: '424px',
backgroundColor: '#1A1D1E',
borderStyle: 'solid',
borderColor: '#313538',
borderRadius: '20px',
padding: '28px',
minHeight: '378px',
}}
>
<CardHeading title={title} />
{children}
</div>
);
const Heading = ({ children }: { children: React.ReactNode }) => (
// TODO style with stitches or we can use tailwind
<h2 className="text-4xl">{children}</h2>
);
type StepperIndicatorContainerProps = {
children: React.ReactNode;
};
const StepperIndicatorContainer = ({
children,
}: StepperIndicatorContainerProps) => {
return (
<Flex
css={{
flexDirection: 'column',
justifyContent: 'center',
mr: '$34',
width: '$106',
}}
>
{children}
</Flex>
);
};
type MintStepContainerProps = {
children: React.ReactNode;
};
const MintStepContainer = ({ children }: MintStepContainerProps) => (
<Flex css={{ flexDirection: 'row', justifyContent: 'center' }}>
{children}
</Flex>
);
export const MintStepper = () => (
<Stepper.Root initialStep={1}>
<Stepper.Container>
<Stepper.Step>
<MintStepContainer>
<StepperIndicatorContainer>
<Stepper.Indicator />
<Heading>Connect your Ethereum Wallet to mint an NFA</Heading>
</StepperIndicatorContainer>
{/* TODO create component to handle the wallet connection */}
<Card title="Get Started">
<span>Step 1</span>
</Card>
</MintStepContainer>
</Stepper.Step>
<Stepper.Step>
<MintStepContainer>
<StepperIndicatorContainer>
<Stepper.Indicator />
<Heading>Connect GitHub and select repository</Heading>
</StepperIndicatorContainer>
{/* TODO create component to handle the github connection */}
<Card title="Connect GitHub">
<span>Step 2</span>
</Card>
</MintStepContainer>
</Stepper.Step>
<Stepper.Step>
<MintStepContainer>
<StepperIndicatorContainer>
<Stepper.Indicator />
<Heading>Finalize a few key things for your DyDx NFA</Heading>
</StepperIndicatorContainer>
{/* TODO create component to handle the NFA details */}
<Card title="NFA Details">
<span>Step 3</span>
</Card>
</MintStepContainer>
</Stepper.Step>
<Stepper.Step>
<MintStepContainer>
<StepperIndicatorContainer>
<Stepper.Indicator />
<Heading>Review your DyDx NFA and mint it on Polygon</Heading>
</StepperIndicatorContainer>
{/* TODO create component to handle the NFA mint */}
<Card title="Mint NFA">
<span>Step 4</span>
</Card>
</MintStepContainer>
</Stepper.Step>
</Stepper.Container>
{/* TODO remove buttons when finish to integrate all the flow */}
<StepperButton />
</Stepper.Root>
);

View File

@ -0,0 +1,15 @@
import { Flex } from '@/components';
import { MintStepper } from './mint-stepper';
export const Mint = () => (
<Flex css={{ height: 'inherit', justifyContent: 'center' }}>
<Flex
css={{
flexDirection: 'column',
justifyContent: 'center',
}}
>
<MintStepper />
</Flex>
</Flex>
);