feat: mint step 1 UI layout connect with GitHub (#113)

* feat: add gh steps

* fix: fix import as change the component name

* chore: add context for mint view

* fix: fix styles for dropdown

* chore: add dropdown on github components

* styles: fix styles for github repository config card

* fix: apply PR review comments

* style: reuse no results component
This commit is contained in:
Camila Sosa Morales 2023-02-09 10:21:48 -05:00 committed by GitHub
parent 3e1373682f
commit 0af0da7477
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 602 additions and 179 deletions

View File

@ -14,7 +14,7 @@ const ComboboxInput = ({
handleInputChange,
handleInputClick,
}: ComboboxInputProps) => (
<div className="relative">
<div className="relative w-full cursor-default ">
<Icon
name="search"
size="sm"
@ -27,7 +27,7 @@ const ComboboxInput = ({
/>
<ComboboxLib.Input
placeholder="Search"
className={`w-full border-solid border border-slate7 py-3 pl-8 pr-10 text-sm bg-transparent leading-5 text-slate11 outline-none ${
className={`w-full border-solid border border-slate7 h-11 py-3 pl-8 pr-10 text-sm bg-transparent leading-5 text-slate11 outline-none ${
open ? 'border-b-0 rounded-t-xl bg-black border-slate6' : 'rounded-xl'
}`}
displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
@ -43,7 +43,6 @@ type ComboboxOptionProps = {
const ComboboxOption = ({ option }: ComboboxOptionProps) => (
<ComboboxLib.Option
key={option.value}
value={option}
className={({ active }) =>
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm ${
@ -65,8 +64,10 @@ const ComboboxOption = ({ option }: ComboboxOptionProps) => (
</ComboboxLib.Option>
);
const NoResults = () => (
<div className="relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11">
export const NoResults = ({ css }: { css?: string }) => (
<div
className={`relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11 ${css}`}
>
Nothing found.
</div>
);
@ -125,15 +126,14 @@ export const Combobox: React.FC<ComboboxProps> = ({
onChange={handleComboboxChange}
>
{({ open }) => (
<>
<div className="w-full cursor-default overflow-hidden bg-transparent text-left focus:outline-none sm:text-sm">
<ComboboxInput
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
open={open}
/>
<ComboboxLib.Button ref={buttonRef} className="hidden" />
</div>
<div className="relative">
<ComboboxInput
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
open={open}
/>
<ComboboxLib.Button ref={buttonRef} className="hidden" />
<Transition
show={open}
as={Fragment}
@ -143,17 +143,17 @@ export const Combobox: React.FC<ComboboxProps> = ({
leaveTo="opacity-0"
afterLeave={handleLeaveTransition}
>
<ComboboxLib.Options className="absolute max-h-60 w-inherit z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
<ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
{filteredItems.length === 0 && query !== '' ? (
<NoResults />
) : (
filteredItems.map((option) => {
return <ComboboxOption option={option} />;
filteredItems.map((option: ComboboxItem) => {
return <ComboboxOption key={option.value} option={option} />;
})
)}
</ComboboxLib.Options>
</Transition>
</>
</div>
)}
</ComboboxLib>
);

View File

@ -39,8 +39,12 @@ const DropdownButton = ({ selectedValue, open }: DropdownButtonProps) => (
open ? 'border-b-0 rounded-t-xl bg-black border-slate6' : 'rounded-xl'
}`}
>
<span className="block truncate text-slate11">
{selectedValue ? selectedValue.label : 'Select'}
<span
className={`block truncate ${
selectedValue && selectedValue.label ? 'text-slate12' : 'text-slate11'
}`}
>
{selectedValue && selectedValue.label ? selectedValue.label : 'Select'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
<Icon name="chevron-down" />
@ -67,6 +71,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
const handleDropdownChange = (option: DropdownItem) => {
onChange(option);
};
return (
<Listbox value={selectedValue} by="value" onChange={handleDropdownChange}>
{({ open }) => (
@ -80,7 +85,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
>
<Listbox.Options className="absolute max-h-fit w-full z-10 overflow-auto rounded-b-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm">
{items.map((option: DropdownItem) => (
<DropdownOption option={option} />
<DropdownOption key={option.value} option={option} />
))}
</Listbox.Options>
</Transition>

View File

@ -6,7 +6,6 @@ import { AiOutlineDown } from '@react-icons/all-files/ai/AiOutlineDown';
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
import { MetamaskIcon, EthereumIcon } from './custom';
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
export const IconLibrary = Object.freeze({
back: IoArrowBackCircleSharp,

View File

@ -0,0 +1,7 @@
import { dripStitches } from '@/theme';
const { styled } = dripStitches;
export const Separator = styled('hr', {
borderTop: '1px solid $slate6',
});

View File

@ -4,5 +4,7 @@ export const radii = {
md: '0.5rem', // 8px
lg: '0.75rem', // 12px
xl: '1rem', // 16px
xlh: '1.25rem', // 20px
'2xl': '1.5rem', // 24px
full: '9999px',
};

View File

@ -9,8 +9,8 @@ export const spacing = {
'3h': '0.875rem', // 14px
4: '1rem', // 16px
5: '1.25rem', // 20px
6: '1.5rem',
7: '1.75rem',
6: '1.5rem', // 24px
7: '1.75rem', // 28px
8: '2rem',
9: '2.25rem',
10: '2.5rem',
@ -28,18 +28,24 @@ export const spacing = {
36: '9rem',
40: '10rem',
44: '11rem',
46: '11.5rem', // 184px
'46h': '11.625rem', // 186px
48: '12rem',
52: '13rem',
56: '14rem',
59: '14.9375rem', // 239px
60: '15rem',
62: '15.5rem',
64: '16rem',
72: '18rem',
80: '20rem',
95: '23rem',
'95h': '23.375rem', // 374px
96: '24rem',
100: '25rem',
102: '25.5rem', // 408px
104: '26rem', // 416px
106: '26.5rem', // 424px
'107h': '26.625rem', // 426px
128: '32rem', // 512px
};

View File

@ -1 +1,3 @@
export * from './home';
export * from './mint';
export * from './svg-test';

View File

@ -0,0 +1,53 @@
import { Button, Card, Grid, Icon, IconButton } from '@/components';
import { Mint } from '../mint.context';
export const GithubConnect: React.FC = () => {
const { setGithubStep } = Mint.useContext();
const handleNextStep = () => {
//TODO when we integrate GH login, we'll need to set the step to 2 after login
setGithubStep(2);
};
return (
<Card.Container>
<Card.Heading
title="Connect GitHub"
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<Card.Body>
<Grid css={{ rowGap: '$6' }}>
<Button
iconSpacing="59"
size="lg"
variant="ghost"
css={{
backgroundColor: '$slate4',
color: '$slate12',
py: '$2h',
}}
onClick={handleNextStep}
rightIcon={
<Icon name="github" css={{ color: 'white', fontSize: '$4xl' }} />
}
>
GitHub
</Button>
<Card.Text
css={{ height: '$46h', width: '$95', fontSize: '$md', px: '$12' }}
>
<span>
After connecting your GitHub, your repositories will show here.
</span>
</Card.Text>
</Grid>
</Card.Body>
</Card.Container>
);
};

View File

@ -0,0 +1,130 @@
import {
Button,
Card,
Dropdown,
DropdownItem,
Form,
Grid,
Icon,
IconButton,
Stepper,
} from '@/components';
import React, { useState } from 'react';
import { Mint } from '../mint.context';
import { RepoRow } from './github-repository-selection';
//TODO remove once it's integrated with GH login
const branches: DropdownItem[] = [
{
label: 'master',
value: 'master',
},
{
label: 'develop',
value: 'develop',
},
{
label: 'feature/branch',
value: 'feature/branch',
},
];
export const GithubRepoConfiguration: React.FC = () => {
const {
repositoryName,
branchName,
commitHash,
setGithubStep,
setRepositoryConfig,
} = Mint.useContext();
const { nextStep } = Stepper.useContext();
const [branchSelected, setBranchSelected] = useState(branchName);
const [commitHashSelected, setCommitHashSelected] = useState(commitHash);
const handlePrevStepClick = () => {
setGithubStep(2);
setRepositoryConfig('', '');
};
const handleBranchChange = (dorpdownOption: DropdownItem) => {
//TODO we'll have to check the data that GH API returns
setBranchSelected(dorpdownOption.value);
};
const handleCommitHashChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCommitHashSelected(e.target.value);
};
const handleContinueClick = () => {
setRepositoryConfig(branchSelected, commitHashSelected);
nextStep();
};
return (
<Card.Container css={{ width: '$107h' }}>
<Card.Heading
title="Configure Repository"
leftIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={handlePrevStepClick}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<Card.Body css={{ pt: '$2' }}>
<Grid css={{ rowGap: '$6' }}>
<RepoRow
repo={repositoryName}
css={{ mb: '0' }}
button={
<Button
colorScheme="gray"
disabled
variant="outline"
css={{ py: '$1', height: '$5', borderRadius: '$md' }}
>
Use for NFA
</Button>
}
/>
<Form.Field>
<Form.Label>Git Branch</Form.Label>
<Dropdown
items={branches}
selectedValue={{ label: branchSelected, value: branchSelected }}
onChange={handleBranchChange}
/>
</Form.Field>
<Form.Field>
<Form.Label>Git Commit</Form.Label>
<Form.Input
placeholder="693f89763dbb7a6c9ce0711cc34591a4c8c77198"
value={commitHashSelected}
onChange={handleCommitHashChange}
/>
</Form.Field>
<Button
disabled={!branchSelected || !commitHashSelected}
colorScheme="blue"
variant="solid"
onClick={handleContinueClick}
>
Continue
</Button>
</Grid>
</Card.Body>
</Card.Container>
);
};

View File

@ -0,0 +1,166 @@
import {
Button,
Card,
Combobox,
ComboboxItem,
Flex,
Grid,
Icon,
IconButton,
NoResults,
} from '@/components';
import { Input } from '@/components/core/input';
import { Separator } from '@/components/core/separator.styles';
import React, { forwardRef, useRef, useState } from 'react';
import { Mint } from '../mint.context';
//TODO remove once it's integrated with GH login
const repos = [
'DyDx',
'Testing',
'Hello World',
'Portofolio',
'NFA',
'NFT',
'NFTs',
];
//TODO remove once it's integrated with GH login
const users: ComboboxItem[] = [
{ label: 'DyDx', value: 'DyDx', icon: 'github' },
{ label: 'Testing', value: 'Testing', icon: 'github' },
{ label: 'Hello World', value: 'Hello World', icon: 'github' },
{ label: 'Portofolio', value: 'Portofolio', icon: 'github' },
{ label: 'NFA', value: 'NFA', icon: 'github' },
{ label: 'NFT', value: 'NFT', icon: 'github' },
{ label: 'NFTs', value: 'NFTs', icon: 'github' },
];
type RepoRowProps = {
repo: string;
button: React.ReactNode;
} & React.ComponentProps<typeof Flex>;
export const RepoRow = forwardRef<HTMLDivElement, RepoRowProps>(
({ repo, button, ...props }, ref) => (
<Flex
{...props}
ref={ref}
css={{ justifyContent: 'space-between', my: '$4', ...props.css }}
>
<Flex css={{ alignItems: 'center' }}>
<Icon name="github" css={{ fontSize: '$2xl', mr: '$2' }} />
<span>{repo}</span>
</Flex>
{button}
</Flex>
)
);
export const GithubRepositoryConnection: React.FC = () => {
const [searchValue, setSearchValue] = useState('');
const [selectedUser, setSelectedUser] = useState<ComboboxItem | undefined>();
const { setGithubStep, setRepositoryName, setRepositoryConfig } =
Mint.useContext();
const timeOutRef = useRef<NodeJS.Timeout>();
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
timeOutRef.current && clearTimeout(timeOutRef.current);
timeOutRef.current = setTimeout(() => {
setSearchValue(event.target.value);
}, 500);
};
const handlePrevStepClick = () => {
setGithubStep(1);
};
const handleSelectRepo = (repo: string) => {
setRepositoryName(repo);
setGithubStep(3);
setRepositoryConfig('', '');
};
const filteredRepositories =
searchValue === ''
? repos
: repos.filter(
(item) => item.toUpperCase().indexOf(searchValue.toUpperCase()) != -1
);
return (
<Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pb: '$0h' }}>
<Card.Heading
title="Select Repository"
leftIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={handlePrevStepClick}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<Card.Body css={{ pt: '$4' }}>
<Grid css={{ rowGap: '$2' }}>
<Flex css={{ gap: '$4' }}>
<Combobox
items={users}
selectedValue={selectedUser}
onChange={setSelectedUser}
/>
<Input
leftIcon="search"
placeholder="Search"
onChange={handleSearchChange}
/>
</Flex>
<Flex
css={{
minHeight: '$40',
maxHeight: '$60',
overflowX: 'hidden',
overflowY: 'scroll',
flexDirection: 'column',
}}
>
{filteredRepositories.length > 0 ? (
filteredRepositories.map((repo, index, { length }) => (
<React.Fragment key={repo}>
<RepoRow
repo={repo}
button={
<Button
colorScheme="blue"
variant="outline"
css={{ py: '$1', height: '$5', borderRadius: '$md' }}
onClick={() => handleSelectRepo(repo)}
>
Use for NFA
</Button>
}
/>
{index < length - 1 && <Separator />}
</React.Fragment>
))
) : (
<NoResults css="text-center" />
)}
</Flex>
</Grid>
</Card.Body>
</Card.Container>
);
};

View File

@ -0,0 +1,20 @@
import { Stepper } from '@/components';
import { useState } from 'react';
import { Mint } from '../mint.context';
import { GithubConnect } from './github-connect-step';
import { GithubRepoConfiguration } from './github-repo-configuration';
import { GithubRepositoryConnection } from './github-repository-selection';
export const GithubStep = () => {
const { githubStep } = Mint.useContext();
switch (githubStep) {
case 1:
default:
return <GithubConnect />;
case 2:
return <GithubRepositoryConnection />;
case 3:
return <GithubRepoConfiguration />;
}
};

View File

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

View File

@ -0,0 +1,49 @@
import { Flex, Stepper } from '@/components';
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 Container = ({ children }: MintStepContainerProps) => (
<Flex css={{ flexDirection: 'row', justifyContent: 'center' }}>
{children}
</Flex>
);
type MintStepProps = {
children: React.ReactNode;
header: string;
};
export const MintStep: React.FC<MintStepProps> = ({ children, header }) => {
return (
<Container>
<StepperIndicatorContainer>
<Stepper.Indicator />
<h2 className="text-4xl">{header}</h2>
</StepperIndicatorContainer>
{children}
</Container>
);
};

View File

@ -1,164 +1,85 @@
import { Button, Flex, IconButton, Icon, Stepper } from '@/components';
import { IconButton, Icon, Stepper, Card } from '@/components';
import { GithubStep } from './github-step';
import { MintStep } from './mint-step';
import { Mint } from './mint.context';
//TODO remove after mint flow is done
const Heading = ({ title }: { title: string }) => {
const { prevStep } = Stepper.useContext();
// 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>
<Card.Heading
title={title}
leftIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={prevStep}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
);
};
const CardHeading = ({ title }: { title: string }) => {
const { currentStep, prevStep } = Stepper.useContext();
export const MintStepper = () => {
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' }}
/>
)}
<Stepper.Root initialStep={1}>
<Mint.Provider>
<Stepper.Container>
<Stepper.Step>
<MintStep header="Connect GitHub and select repository">
<GithubStep />
</MintStep>
</Stepper.Step>
<h3 style={{ fontSize: '20px', fontWeight: '700' }}>{title}</h3>
</Flex>
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
</Flex>
<Stepper.Step>
<MintStep header="Connect your Ethereum Wallet to mint an NFA">
<Card.Container>
<Heading title="Connect Wallet" />
<Card.Body>
<span>Step 2</span>
</Card.Body>
</Card.Container>
</MintStep>
</Stepper.Step>
<Stepper.Step>
<MintStep header="Finalize a few key things for your DyDx NFA">
<Card.Container>
<Heading title="NFA Detail" />
<Card.Body>
<span>Step 3</span>
</Card.Body>
</Card.Container>
</MintStep>
</Stepper.Step>
<Stepper.Step>
<MintStep header="Review your DyDx NFA and mint it on Polygon">
<Card.Container>
<Heading title="Mint NFA" />
<Card.Body>
<span>Step 4</span>
</Card.Body>
</Card.Container>
</MintStep>
</Stepper.Step>
</Stepper.Container>
</Mint.Provider>
{/* TODO remove buttons when finish to integrate all the flow */}
{/* <StepperButton /> */}
</Stepper.Root>
);
};
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,62 @@
import { createContext } from '@/utils';
import { useState } from 'react';
export type MintContext = {
repositoryName: string;
branchName: string;
commitHash: string;
githubStep: number;
setGithubStep: (step: number) => void;
setRepositoryName: (repo: string) => void;
setRepositoryConfig: (branch: string, hash: string) => void;
};
const [MintProvider, useContext] = createContext<MintContext>({
name: 'Mint.Context',
hookName: 'Mint.useContext',
providerName: 'Mint.Provider',
});
export abstract class Mint {
static readonly useContext = useContext;
static readonly Provider: React.FC<Mint.ProviderProps> = ({ children }) => {
const [repositoryName, setRepositoryName] = useState('');
const [branchName, setBranchName] = useState('');
const [commitHash, setCommitHash] = useState('');
const [githubStep, setGithubStepContext] = useState(1);
const setGithubStep = (step: number): void => {
if (step > 0 && step <= 3) {
setGithubStepContext(step);
}
};
const setRepositoryConfig = (branch: string, hash: string) => {
setBranchName(branch);
setCommitHash(hash);
};
return (
<MintProvider
value={{
repositoryName,
branchName,
commitHash,
githubStep,
setGithubStep,
setRepositoryConfig,
setRepositoryName,
}}
>
{children}
</MintProvider>
);
};
}
export namespace Mint {
export type ProviderProps = {
children: React.ReactNode;
};
}