feature: stepper component (#91)
This commit is contained in:
parent
77b20d527d
commit
325fdb8361
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './stepper';
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './format';
|
||||
export * from './validation';
|
||||
export * from './object';
|
||||
export * from './context';
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './mint';
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
Loading…
Reference in New Issue