diff --git a/ui/src/components/core/icon/icon-library.tsx b/ui/src/components/core/icon/icon-library.tsx index ed7a545..5b219c2 100644 --- a/ui/src/components/core/icon/icon-library.tsx +++ b/ui/src/components/core/icon/icon-library.tsx @@ -1,13 +1,17 @@ import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub'; import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp'; import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp'; +import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp'; +import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck'; import { MetamaskIcon, EthereumIcon } from './custom'; export const IconLibrary = Object.freeze({ back: IoArrowBackCircleSharp, + check: AiOutlineCheck, ethereum: EthereumIcon, github: IoLogoGithub, info: IoInformationCircleSharp, + upload: IoCloudUploadSharp, metamask: MetamaskIcon, //remove if not used }); diff --git a/ui/src/components/core/input/index.ts b/ui/src/components/core/input/index.ts new file mode 100644 index 0000000..fa78574 --- /dev/null +++ b/ui/src/components/core/input/index.ts @@ -0,0 +1 @@ +export * from './input'; diff --git a/ui/src/components/core/input/input-file.tsx b/ui/src/components/core/input/input-file.tsx new file mode 100644 index 0000000..8dff12d --- /dev/null +++ b/ui/src/components/core/input/input-file.tsx @@ -0,0 +1,94 @@ +import { Flex } from '../../layout'; +import { dripStitches } from '../../../theme'; +import { forwardRef, useRef, useState } from 'react'; +import { Icon } from '../icon'; +import { Form } from '../../../components/form/form'; + +const { styled } = dripStitches; + +const BorderInput = styled('div', { + borderStyle: 'solid', + borderColor: '$gray7', + width: '$22', + height: '$22', + transition: 'border-color 0.2s ease-in-out', + borderWidth: '$default', + borderRadius: '$lg', + zIndex: '$docked', + my: '$1h', + + '&:hover': { + borderColor: '$gray8', + }, +}); + +const DEFAULT_MAX_FILE_SIZE = 10; // in KB + +// The file size must be capped to a size that the contract can handle +const validateFileSize = ( + file: File, + maxSize = DEFAULT_MAX_FILE_SIZE +): boolean => { + return file.size <= 1024 * maxSize; +}; + +type InputFileProps = { + value: File | null; + onChange: (file: File | null) => void; +} & React.ComponentProps; + +export const StyledInputFile = forwardRef( + ({ value: file, onChange, css, ...props }, ref) => { + const inputFileRef = useRef(null); + const [errorMessage, setErrorMessage] = useState(null); + + const handleFileChange = (e: React.ChangeEvent) => { + e.preventDefault(); + setErrorMessage(null); + + if (e.target.files && e.target.files.length > 0) { + if (validateFileSize(e.target.files[0])) onChange(e.target.files[0]); + else { + onChange(null); + setErrorMessage('File size is too big'); + } + } + }; + + return ( + <> + inputFileRef.current?.click()} + > + {file ? ( + logo + ) : ( + + )} + + + + + {errorMessage && {errorMessage}} + + ); + } +); diff --git a/ui/src/components/core/input/input.stories.tsx b/ui/src/components/core/input/input.stories.tsx new file mode 100644 index 0000000..af44d22 --- /dev/null +++ b/ui/src/components/core/input/input.stories.tsx @@ -0,0 +1,20 @@ +import { Input } from './'; + +export default { + title: 'Components/Input', + component: Input, +}; + +export const Variants = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/ui/src/components/core/input/input.ts b/ui/src/components/core/input/input.ts new file mode 100644 index 0000000..3ff8e4d --- /dev/null +++ b/ui/src/components/core/input/input.ts @@ -0,0 +1,66 @@ +import { dripStitches } from '../../../theme'; +import { StyledInputFile } from './input-file'; +const { styled } = dripStitches; + +const styles = { + all: 'unset', + width: '100%', + boxSizing: 'border-box', + borderStyle: 'solid', + minWidth: '$0', + color: '$slate12', + my: '$1h', + + transition: 'border-color 0.2s ease-in-out', + borderWidth: '$default', + borderColor: '$slate7', + backgroundColor: 'transparent', + '&:hover': { + borderColor: '$gray8', + }, + '&:focus': { + outline: 'none', + borderColor: '$blue9', + }, + '&[aria-invalid=true], &[data-invalid]': { + borderColor: '$red9', + }, + '&:disabled': { + color: '$slate8', + borderColor: '$slate6', + backgroundColor: '$slate2', + '&::placeholder': { + color: '$slate8', + }, + }, + + variants: { + size: { + sm: { + borderRadius: '$md', + fontSize: '$xs', + lineHeight: '$4', + p: '$1h', + }, + md: { + borderRadius: '$lg', + fontSize: '$sm', + p: '$3 $3h', + }, + lg: { + borderRadius: '$xl', + fontSize: '$md', + p: '$4 $5', + }, + }, + }, + defaultVariants: { + size: 'md', + }, +}; + +export const Input = styled('input', styles); + +export const Textarea = styled('textarea', styles); + +export const LogoFileInput = StyledInputFile; diff --git a/ui/src/components/form/form.stories.tsx b/ui/src/components/form/form.stories.tsx new file mode 100644 index 0000000..6fcf372 --- /dev/null +++ b/ui/src/components/form/form.stories.tsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import { Form } from './form'; + +export default { + title: 'Components/Form', + component: Form, +}; + +export const Fields = () => { + const [file, setFile] = useState(null); + return ( + <> + + Label + + Input error + + + Label + + Textarea error + + + Label + setFile(file)} /> + + + ); +}; diff --git a/ui/src/components/form/form.styles.ts b/ui/src/components/form/form.styles.ts new file mode 100644 index 0000000..f7fb11b --- /dev/null +++ b/ui/src/components/form/form.styles.ts @@ -0,0 +1,46 @@ +import { dripStitches } from '../../theme'; +import { Flex } from '../layout'; + +const { styled } = dripStitches; +export abstract class FormStyles { + static readonly Field = styled(Flex, { + flexDirection: 'column', + }); + + static readonly Label = styled('label', { + color: '$slate11', + + '&:disabled': { + color: '$slate8', + }, + variants: { + size: { + sm: { + fontSize: '$xs', //TODO check with royce font size + }, + md: { + fontSize: '$xs', + }, + lg: { + fontSize: '$md', + }, + }, + }, + defaultVariants: { + size: 'md', + }, + }); + + static readonly ErrorMessage = styled('span', { + color: '$red11', + fontSize: '0.625rem', + + variants: { + size: { + lg: { + fontSize: '$sm', + }, + }, + }, + }); +} diff --git a/ui/src/components/form/form.tsx b/ui/src/components/form/form.tsx new file mode 100644 index 0000000..09fefbc --- /dev/null +++ b/ui/src/components/form/form.tsx @@ -0,0 +1,67 @@ +import React, { forwardRef } from 'react'; +import { Input, LogoFileInput, Textarea } from '../core/input'; +import { FormStyles } from './form.styles'; + +export abstract class Form { + static readonly Field = forwardRef( + ({ children, ...props }, ref) => { + return ( + + {children} + + ); + } + ); + + static readonly Label = forwardRef( + ({ children, ...props }, ref) => ( + + {children} + + ) + ); + + static readonly Error = forwardRef( + ({ children, ...props }, ref) => ( + + {children} + + ) + ); + + static readonly Input = forwardRef( + (props, ref) => { + return ; + } + ); + + static readonly Textarea = forwardRef< + HTMLTextAreaElement, + Form.TextareaProps + >((props, ref) => { + return