mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-30 04:24:26 +01:00
feat: create reusable select component
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
`}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user