feat: create reusable select component

This commit is contained in:
MTG2000
2022-09-04 10:57:01 +03:00
parent ad5e5ac948
commit 3bebc91d6e
10 changed files with 155 additions and 304 deletions

14
package-lock.json generated
View File

@@ -19,7 +19,7 @@
"@remirror/react": "^1.0.34",
"@shopify/react-web-worker": "^5.0.1",
"@szhsin/react-menu": "^3.0.2",
"@tailwindcss/line-clamp": "^0.4.0",
"@tailwindcss/line-clamp": "^0.4.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
@@ -15324,9 +15324,9 @@
}
},
"node_modules/@tailwindcss/line-clamp": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.0.tgz",
"integrity": "sha512-HQZo6gfx1D0+DU3nWlNLD5iA6Ef4JAXh0LeD8lOGrJwEDBwwJNKQza6WoXhhY1uQrxOuU8ROxV7CqiQV4CoiLw==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz",
"integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==",
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
@@ -82381,9 +82381,9 @@
}
},
"@tailwindcss/line-clamp": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.0.tgz",
"integrity": "sha512-HQZo6gfx1D0+DU3nWlNLD5iA6Ef4JAXh0LeD8lOGrJwEDBwwJNKQza6WoXhhY1uQrxOuU8ROxV7CqiQV4CoiLw==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz",
"integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==",
"requires": {}
},
"@testing-library/dom": {

View File

@@ -1,5 +1,5 @@
{
"name": "my-app",
"name": "makers-bolt-fun",
"version": "0.1.0",
"private": true,
"dependencies": {
@@ -14,7 +14,7 @@
"@remirror/react": "^1.0.34",
"@shopify/react-web-worker": "^5.0.1",
"@szhsin/react-menu": "^3.0.2",
"@tailwindcss/line-clamp": "^0.4.0",
"@tailwindcss/line-clamp": "^0.4.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",

View File

@@ -21,8 +21,8 @@ const Card = React.forwardRef<HTMLDivElement, PropsWithChildren<Props>>(({
ref={ref}
className={`
${onlyMd ?
`md:bg-white md:rounded-16 md:border-2 border-gray-200 ${defaultPadding && "md:p-24"}` :
`bg-white rounded-12 md:rounded-16 border-2 border-gray-200 ${defaultPadding && "p-16 md:p-24"}`
`md:bg-white md:rounded-16 md:outline outline-2 outline-gray-200 ${defaultPadding && "md:p-24"}` :
`bg-white rounded-12 md:rounded-16 outline outline-2 outline-gray-200 ${defaultPadding && "p-16 md:p-24"}`
}
${className}
`}

View File

@@ -1,171 +0,0 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { useWatch } from 'react-hook-form';
import { WrapForm } from 'src/utils/storybook/decorators';
import Autocomplete from './Autocomplete';
export default {
title: 'Shared/Inputs/AutoComplete',
component: Autocomplete,
decorators: [WrapForm({
defaultValues: {
autocomplete: null
}
})],
} as ComponentMeta<typeof Autocomplete>;
const options = [
{
"id": "20f0eb8d-c0cd-4e12-8a08-0d9846fc8704",
"name": "Nichole Bailey",
"username": "Cassie14",
"email": "Daisy_Auer50@hotmail.com",
"address": {
"street": "Anastasia Tunnel",
"suite": 95587,
"city": "Port Casperview",
"zipcode": "04167-6996",
"geo": {
"lat": "-73.4727",
"lng": "-142.9435"
}
},
"phone": "324-615-9195 x5902",
"website": "ron.net",
"company": {
"name": "Roberts, Tremblay and Christiansen",
"catchPhrase": "Vision-oriented actuating access",
"bs": "bricks-and-clicks strategize portals"
}
},
{
"id": "62b70f76-85ba-4241-9ffd-07582008c497",
"name": "Robert Blick",
"username": "Madilyn93",
"email": "Ronaldo82@gmail.com",
"address": {
"street": "Charlie Plain",
"suite": 83070,
"city": "Lake Bonitaland",
"zipcode": "01109",
"geo": {
"lat": "50.0971",
"lng": "-2.3057"
}
},
"phone": "1-541-367-2047 x9006",
"website": "jovani.com",
"company": {
"name": "Parisian - Kling",
"catchPhrase": "Multi-tiered tertiary toolset",
"bs": "plug-and-play benchmark content"
}
},
{
"id": "d02f74d9-bf99-4e41-b678-15e903abc1b3",
"name": "Eli O'Kon",
"username": "Rosario.Davis",
"email": "Mckayla59@hotmail.com",
"address": {
"street": "Wilford Drive",
"suite": 69742,
"city": "North Dianna",
"zipcode": "80620",
"geo": {
"lat": "-61.4191",
"lng": "126.7878"
}
},
"phone": "(339) 709-4080",
"website": "clay.name",
"company": {
"name": "Gerlach - Metz",
"catchPhrase": "Pre-emptive user-facing service-desk",
"bs": "frictionless monetize markets"
}
},
{
"id": "21077fa6-6a53-4b84-8407-6cd949718945",
"name": "Marilie Feil",
"username": "Antwon.Carter92",
"email": "Demario.Hyatt20@yahoo.com",
"address": {
"street": "Kenton Spurs",
"suite": 20079,
"city": "Beahanberg",
"zipcode": "79385",
"geo": {
"lat": "-70.7199",
"lng": "4.6977"
}
},
"phone": "608.750.4947",
"website": "jacynthe.org",
"company": {
"name": "Kuhn and Sons",
"catchPhrase": "Total eco-centric matrices",
"bs": "out-of-the-box target communities"
}
},
{
"id": "e07cf1b4-ff43-4c4a-a670-fd7417d6bbaf",
"name": "Ella Pagac",
"username": "Damien.Jaskolski",
"email": "Delmer1@gmail.com",
"address": {
"street": "VonRueden Shoals",
"suite": 14035,
"city": "Starkmouth",
"zipcode": "72448-1915",
"geo": {
"lat": "55.2157",
"lng": "98.0822"
}
},
"phone": "(165) 247-5332 x71067",
"website": "chad.info",
"company": {
"name": "Nicolas, Doyle and Rempel",
"catchPhrase": "Adaptive real-time strategy",
"bs": "innovative whiteboard supply-chains"
}
}
]
const Template: ComponentStory<typeof Autocomplete> = (args) => {
const value = useWatch({ name: 'autocomplete' })
console.log(value);
return <Autocomplete
options={options}
labelField='name'
valueField='name'
{...args as any}
/>
}
export const Default = Template.bind({});
Default.args = {
onChange: console.log
}
export const Lodaing = Template.bind({});
Lodaing.args = {
isLoading: true
}
export const Clearable = Template.bind({});
Clearable.args = {
isClearable: true
}
export const MultipleAllowed = Template.bind({});
MultipleAllowed.args = {
isMulti: true
}

View File

@@ -1,111 +0,0 @@
import { useMemo } from "react";
import Select, { StylesConfig } from "react-select";
import { ControlledStateHandler } from "src/utils/interfaces";
type Props<T extends object | string, IsMulti extends boolean = false> = {
options: T[];
labelField?: keyof T
valueField?: keyof T
placeholder?: string
disabled?: boolean
isLoading?: boolean;
isClearable?: boolean;
control?: any,
name?: string,
className?: string,
onBlur?: () => void;
size?: 'sm' | 'md' | 'lg'
} & ControlledStateHandler<T, IsMulti>
export default function AutoComplete<T extends object, IsMulti extends boolean>({
options,
labelField,
valueField,
placeholder = "Select Option...",
isMulti,
isClearable,
disabled,
className,
value,
onChange,
onBlur,
size = 'md',
...props
}: Props<T, IsMulti>) {
const colourStyles: StylesConfig = useMemo(() => ({
control: (styles, state) => ({
...styles,
padding: '5px 12px',
borderRadius: 12,
// border: 'none',
// boxShadow: 'none',
":hover": {
cursor: "pointer"
},
":focus-within": {
'--tw-border-opacity': '1',
borderColor: 'rgb(179 160 255 / var(--tw-border-opacity))',
outlineColor: '#9E88FF',
'--tw-ring-offset-shadow': 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
'--tw-ring-shadow': 'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
boxShadow: 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)',
'--tw-ring-color': 'rgb(179 160 255 / var(--tw-ring-opacity))',
'--tw-ring-opacity': '0.5'
}
}),
indicatorSeparator: (styles, state) => ({
...styles,
display: "none"
}),
input: (styles, state) => ({
...styles,
" input": {
boxShadow: 'none !important'
},
}),
}), [])
return (
<div className='w-full'>
<Select
options={options}
placeholder={placeholder}
className={className}
isMulti={isMulti}
isClearable={isClearable}
isLoading={props.isLoading}
getOptionLabel={o => o[labelField]}
getOptionValue={o => o[valueField]}
value={value as any}
onChange={v => onChange?.(v as any)}
onBlur={onBlur}
styles={colourStyles}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
</div>
);
}

View File

@@ -0,0 +1,136 @@
import { useCallback } from "react";
import Select, { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
import { ControlledStateHandler } from "src/utils/interfaces";
type Props<T extends Record<string, any>, IsMulti extends boolean = boolean> = {
options: T[];
labelField: keyof T
valueField: keyof T
placeholder?: string
disabled?: boolean
isLoading?: boolean;
isClearable?: boolean;
control?: any,
name?: string,
className?: string,
renderOption?: (option: OptionProps<T>) => JSX.Element
} & ControlledStateHandler<T, IsMulti>
export const selectCustomStyle: StylesConfig = ({
control: (styles, state) => ({
...styles,
padding: '5px 12px',
borderRadius: 12,
// border: 'none',
// boxShadow: 'none',
":hover": {
cursor: "pointer"
},
":focus-within": {
'--tw-border-opacity': '1',
borderColor: 'rgb(179 160 255 / var(--tw-border-opacity))',
outlineColor: '#9E88FF',
'--tw-ring-offset-shadow': 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
'--tw-ring-shadow': 'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
boxShadow: 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)',
'--tw-ring-color': 'rgb(179 160 255 / var(--tw-ring-opacity))',
'--tw-ring-opacity': '0.5'
}
}),
indicatorSeparator: (styles, state) => ({
...styles,
display: "none"
}),
input: (styles, state) => ({
...styles,
" input": {
boxShadow: 'none !important'
},
}),
menu: (styles, state) => ({
...styles,
padding: 8,
borderRadius: "16px !important"
}),
})
export default function BasicSelectInput<T extends Record<string, any>, IsMulti extends boolean>({
options,
labelField,
valueField,
placeholder = "Select Option...",
isMulti,
isClearable,
disabled,
className,
value,
onChange,
onBlur,
renderOption,
...props
}: Props<T, IsMulti>) {
return (
<div className='w-full'>
<Select
options={options}
placeholder={placeholder}
className={className}
isMulti={isMulti}
isClearable={isClearable}
isLoading={props.isLoading}
getOptionLabel={o => o[labelField]}
getOptionValue={o => o[valueField]}
value={value as any}
onChange={v => onChange?.(v as any)}
onBlur={onBlur}
components={{
Option: getOptionComponent(renderOption, labelField),
// ValueContainer: CustomValueContainer
}}
styles={selectCustomStyle as any}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
</div>
);
}
function getOptionComponent<T extends Record<string, any>>(renderOption: Props<T>['renderOption'], labelField: Props<T>['labelField']) {
const _render = renderOption ?? ((option) => <div className={`flex gap-16 my-4 px-16 py-12 rounded-12 text-gray-800 ${option.isSelected ? "bg-gray-100 text-gray-800" : "hover:bg-gray-50"} cursor-pointer`}>
{option.data[labelField]}
</div>)
return function OptionComponent(props: OptionProps<T>) {
return (
<components.Option {...props} className='!p-0 !bg-transparent hover:!bg-transparent'>
{_render(props)}
</components.Option>
);
};
}

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react'
import AutoComplete from 'src/Components/Inputs/Autocomplete/Autocomplete';
import { useMediaQuery } from 'src/utils/hooks';
import { MEDIA_QUERIES } from 'src/utils/theme';

View File

@@ -40,10 +40,10 @@ export default function EventCard({ event }: Props) {
return (
<div
role='button'
className="rounded-16 bg-white overflow-hidden border-2 flex flex-col group"
className="rounded-16 bg-white overflow-hidden outline outline-2 outline-gray-200 flex flex-col group"
onClick={openEventModal}
>
<img className="w-full h-[160px] object-cover" src={event.image} alt="" />
<img className="w-full h-[160px] object-cover rounded-t-16" src={event.image} alt="" />
<div className="p-16 grow flex flex-col">
<div className="flex flex-col gap-8">
<h3 className="text-body2 font-bold text-gray-900 group-hover:underline">

View File

@@ -1,5 +1,5 @@
import { FiSearch } from 'react-icons/fi'
import AutoComplete from 'src/Components/Inputs/Autocomplete/Autocomplete';
import BasicSelectInput from 'src/Components/Inputs/Selects/BasicSelectInput/BasicSelectInput';
import { TournamentEventTypeEnum } from 'src/graphql';
import { mapTypeToBadge } from '../EventCard/EventCard';
@@ -31,12 +31,12 @@ export default function EventsFilters(props: Props) {
onChange={e => props.onSearchChange(e.target.value)}
/>
</div>
<AutoComplete
<BasicSelectInput
isMulti={false}
labelField='label'
valueField='value'
isMulti={false}
size='lg'
placeholder='All events'
isClearable
value={props.eventValue ? { label: mapTypeToBadge[props.eventValue].text, value: props.eventValue } : null}
onChange={(v) => props.onEventChange(v ? v.value : null)}
options={options}

View File

@@ -13,12 +13,10 @@ export type ListComponentProps<T> = {
export type ControlledStateHandler<T, IsMulti extends boolean> = {
isMulti?: IsMulti;
value?:
| (true extends IsMulti ? T[] : never)
| (false extends IsMulti ? T : never)
| (true extends IsMulti ? T[] : T)
| null
onChange?: (
nv: | (true extends IsMulti ? T[] : never)
| (false extends IsMulti ? T : never)
nv: | (true extends IsMulti ? T[] : T)
| null
) => void
onBlur?: () => void