feat: add filters with checkboxes

This commit is contained in:
marcelines 2023-06-06 17:49:23 +01:00
parent e40ed199d8
commit c6488be581
No known key found for this signature in database
GPG Key ID: 56B1E53E2A3F43C7
9 changed files with 265 additions and 141 deletions

View File

@ -0,0 +1,44 @@
import { tokens } from '@status-im/components/src/tokens'
import type { ColorTokens } from '@tamagui/core'
// TypeGuard for ColorTokens
function isColorTokens(
value: `#${string}` | ColorTokens
): value is ColorTokens {
return typeof value === 'string' && value.startsWith('$')
}
const ColorCircle = ({
color: colorFromProps,
}: {
color?: ColorTokens | `#${string}`
}) => {
if (!colorFromProps) {
return null
}
let color: ColorTokens | string = colorFromProps
if (isColorTokens(colorFromProps)) {
const colorToken = colorFromProps.replace(
'$',
''
) as keyof typeof tokens.color
color = tokens.color[colorToken]?.val || colorFromProps
}
return (
<div
className="rounded-full"
style={{
width: 16,
height: 16,
backgroundColor: `color-mix(in srgb, ${color} ${40}%, transparent)`,
border: `1px solid ${color}`,
}}
/>
)
}
export { ColorCircle }

View File

@ -1,79 +0,0 @@
import { useState } from 'react'
import { Avatar, Button, Input, Text } from '@status-im/components'
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
import { DropdownIcon, SearchIcon } from '@status-im/icons'
type Props = {
authors: {
id: number
name: string
avatar?: string
}[]
}
const FilterAuthor = (props: Props) => {
const { authors } = props
const [authorFilterText, setAuthorFilterText] = useState('')
const filteredAuthors = authors.filter(author =>
author.name.toLowerCase().includes(authorFilterText.toLowerCase())
)
const [selectedAuthors, setSelectedAuthors] = useState<number[]>([])
return (
<div className="pr-3">
<DropdownMenu>
<Button
size={32}
variant="outline"
iconAfter={<DropdownIcon size={20} />}
>
Author
</Button>
<DropdownMenu.Content sideOffset={10} align="end">
<div className="p-2 px-1">
<Input
placeholder="Find Author"
icon={<SearchIcon size={20} />}
size={32}
value={authorFilterText}
onChangeText={setAuthorFilterText}
/>
</div>
{filteredAuthors.map(author => (
<DropdownMenu.CheckboxItem
key={author.id}
icon={
<Avatar
name={author.name}
src={author.avatar}
size={16}
type="user"
/>
}
label={author.name}
checked={selectedAuthors.includes(author.id)}
onSelect={() => {
if (selectedAuthors.includes(author.id)) {
setSelectedAuthors(
selectedAuthors.filter(id => id !== author.id)
)
} else {
setSelectedAuthors([...selectedAuthors, author.id])
}
}}
/>
))}
{filteredAuthors.length === 0 && (
<div className="p-2 py-1">
<Text size={13}> No authors found</Text>
</div>
)}
</DropdownMenu.Content>
</DropdownMenu>
</div>
)
}
export { FilterAuthor }

View File

@ -0,0 +1,105 @@
import { useState } from 'react'
import { Avatar, Button, Input, Text } from '@status-im/components'
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
import { DropdownIcon, SearchIcon } from '@status-im/icons'
import { ColorCircle } from './components/color-circle'
import type { ColorTokens } from '@tamagui/core'
type Props = {
data: {
id: number
name: string
avatar?: string
color?: ColorTokens | `#${string}`
}[]
label: string
noResultsText?: string
noPadding?: boolean
}
const FilterWithCheckboxes = (props: Props) => {
const { data, label, noResultsText, noPadding } = props
const [filterText, setFilterText] = useState('')
// TODO - this will be improved by having a debounced search and use memoization
const filteredData = data.filter(label =>
label.name.toLowerCase().includes(filterText.toLowerCase())
)
const [selectedValues, setSelectedValues] = useState<number[]>([])
const [isOpen, setIsOpen] = useState(false)
return (
<div className={noPadding ? '' : 'pr-3'}>
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)}>
<Button
size={32}
variant="outline"
iconAfter={
<div
className={`transition-transform ${
isOpen ? 'rotate-180' : 'rotate-0'
}`}
>
<DropdownIcon size={20} />
</div>
}
>
{label}
</Button>
<DropdownMenu.Content sideOffset={10} align="end">
<div className="p-2 px-1">
<Input
placeholder="Find epics"
icon={<SearchIcon size={20} />}
size={32}
value={filterText}
onChangeText={setFilterText}
/>
</div>
{filteredData.map(filtered => {
return (
<DropdownMenu.CheckboxItem
key={filtered.id}
icon={
filtered.color ? (
<ColorCircle color={filtered.color} />
) : (
<Avatar
name={filtered.name}
src={filtered.avatar}
size={16}
type="user"
/>
)
}
label={filtered.name}
checked={selectedValues.includes(filtered.id)}
onSelect={() => {
if (selectedValues.includes(filtered.id)) {
setSelectedValues(
selectedValues.filter(id => id !== filtered.id)
)
} else {
setSelectedValues([...selectedValues, filtered.id])
}
}}
/>
)
})}
{filteredData.length === 0 && (
<div className="p-2 py-1">
<Text size={13}>{noResultsText}</Text>
</div>
)}
</DropdownMenu.Content>
</DropdownMenu>
</div>
)
}
export { FilterWithCheckboxes }
export type { Props as FilterWithCheckboxesProps }

View File

@ -0,0 +1,2 @@
export { FilterWithCheckboxes } from './filter-with-checkboxes'
export { Tabs } from './tabs'

View File

@ -0,0 +1,41 @@
import { useState } from 'react'
import { DoneIcon, OpenIcon } from '@status-im/icons'
const Tabs = (): JSX.Element => {
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
const isOpen = activeTab === 'open'
return (
<div className="flex">
<div className="flex items-center pr-3">
<button
className={`flex cursor-pointer flex-row items-center transition-colors ${
isOpen ? 'text-neutral-100' : 'text-neutral-50'
}`}
onClick={() => setActiveTab('open')}
>
<OpenIcon size={16} color={isOpen ? '$neutral-100' : '$neutral-50'} />
<span className="pl-1 text-[15px]">784 Open</span>
</button>
</div>
<div className="flex items-center pr-3">
<button
className={`flex cursor-pointer flex-row items-center transition-colors ${
!isOpen ? 'text-neutral-100' : 'text-neutral-50'
}`}
onClick={() => setActiveTab('closed')}
>
<DoneIcon
size={16}
color={!isOpen ? '$neutral-100' : '$neutral-50'}
/>
<span className="pl-1 text-[15px]">1012 Closed</span>
</button>
</div>
</div>
)
}
export { Tabs }

View File

@ -1,11 +1,12 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { Avatar, Button, Input, Tag, Text } from '@status-im/components' import { Avatar, Button, Input, Tag, Text } from '@status-im/components'
import { OpenIcon, SearchIcon } from '@status-im/icons' import { SearchIcon } from '@status-im/icons'
import Link from 'next/link' import Link from 'next/link'
import { FilterAuthor } from './filters/filter-author' import { FilterWithCheckboxes, Tabs } from './filters'
import type { FilterWithCheckboxesProps } from './filters/filter-with-checkboxes'
import type { TextInput } from 'react-native' import type { TextInput } from 'react-native'
const issues = [ const issues = [
@ -51,25 +52,7 @@ const issues = [
}, },
] ]
const authors = [ const authors: FilterWithCheckboxesProps['data'] = [
{
id: 1,
name: 'Tobias',
avatar:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&h=500&q=80',
},
{
id: 2,
name: 'Arnold',
avatar:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&h=500&q=80',
},
{
id: 3,
name: 'Alisher',
avatar:
'https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=500&h=500&q=80',
},
{ {
id: 4, id: 4,
name: 'marcelines', name: 'marcelines',
@ -92,9 +75,53 @@ const authors = [
}, },
] ]
export const TableIssues = () => { const epics: FilterWithCheckboxesProps['data'] = [
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open') {
id: 4,
name: 'E:ActivityCenter',
color: '$orange-60',
},
{
id: 5,
name: 'E:Keycard',
color: '$purple-60',
},
{
id: 6,
name: 'E:Wallet',
color: '$pink-60',
},
{
id: 7,
name: 'E:Chat',
color: '$beige-60',
},
]
const labels: FilterWithCheckboxesProps['data'] = [
{
id: 4,
name: 'Mobile',
color: '$blue-60',
},
{
id: 5,
name: 'Frontend',
color: '$brown-50',
},
{
id: 6,
name: 'Backend',
color: '$red-60',
},
{
id: 7,
name: 'Desktop',
color: '$green-60',
},
]
export const TableIssues = () => {
const [issuesSearchText, setIssuesSearchText] = useState('') const [issuesSearchText, setIssuesSearchText] = useState('')
const inputRef = useRef<TextInput>(null) const inputRef = useRef<TextInput>(null)
const [isMinimized, setIsMinimized] = useState(true) const [isMinimized, setIsMinimized] = useState(true)
@ -102,30 +129,10 @@ export const TableIssues = () => {
return ( return (
<div className="border-neutral-10 overflow-hidden rounded-2xl border"> <div className="border-neutral-10 overflow-hidden rounded-2xl border">
<div className="bg-neutral-5 border-neutral-10 flex border-b p-3"> <div className="bg-neutral-5 border-neutral-10 flex border-b p-3">
<div className="flex"> <Tabs />
<div className="pr-3">
<Button
size={32}
variant={activeTab === 'open' ? 'darkGrey' : 'grey'}
onPress={() => setActiveTab('open')}
icon={<OpenIcon size={20} />}
>
784 Open
</Button>
</div>
<div className="pr-3">
<Button
size={32}
variant={activeTab === 'closed' ? 'darkGrey' : 'grey'}
onPress={() => setActiveTab('closed')}
>
1012 closed
</Button>
</div>
</div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<div className="flex px-1"> <div className="pr-2">
<Input <Input
placeholder="Find Author" placeholder="Find Author"
icon={<SearchIcon size={20} />} icon={<SearchIcon size={20} />}
@ -138,17 +145,22 @@ export const TableIssues = () => {
ref={inputRef} ref={inputRef}
/> />
</div> </div>
<FilterAuthor authors={authors} /> <FilterWithCheckboxes
<div className="pr-3"> data={authors}
<Button size={32} variant="ghost"> label="Author"
Filter noResultsText="No author found"
</Button> />
</div> <FilterWithCheckboxes
<div className="pr-3"> data={epics}
<Button size={32} variant="ghost"> label="Epics"
Sort noResultsText="No epics found"
</Button> />
</div> <FilterWithCheckboxes
data={labels}
label="Label"
noResultsText="No labels found"
noPadding
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,15 +9,14 @@ const EpicsDetailPage: Page = () => {
<div className="border-neutral-10 border-b px-5 py-3"> <div className="border-neutral-10 border-b px-5 py-3">
<Breadcrumbs /> <Breadcrumbs />
</div> </div>
<div className="px-10 py-6"> <div className="border-neutral-10 border-b px-10 py-6">
<EpicOverview <EpicOverview
title="Communities protocol" title="Communities protocol"
description="Detecting keycard reader removal for the beginning of each flow" description="Detecting keycard reader removal for the beginning of each flow"
fullscreen fullscreen
/> />
</div>
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" /> <div className="border-neutral-10 border-b px-10 py-6">
<TableIssues /> <TableIssues />
</div> </div>
</div> </div>

View File

@ -79,7 +79,7 @@ const DropdownMenuCheckboxItem = forwardRef<
return ( return (
<ItemBaseCheckbox {...props} ref={forwardedRef} onSelect={handleSelect}> <ItemBaseCheckbox {...props} ref={forwardedRef} onSelect={handleSelect}>
<Stack flexDirection="row" gap={8} alignItems="center"> <Stack flexDirection="row" gap={8} alignItems="center">
{cloneElement(icon, { color: '$neutral-50' })} {cloneElement(icon)}
<Text size={15} weight="medium" color="$neutral-100"> <Text size={15} weight="medium" color="$neutral-100">
{label} {label}
</Text> </Text>

View File

@ -42,7 +42,7 @@ export const Default: Story = {
<Tag label="New tag" size={24} disabled selected /> <Tag label="New tag" size={24} disabled selected />
<Tag label="New tag" size={24} color="#FF7D46" /> <Tag label="New tag" size={24} color="#FF7D46" />
<Tag label="New tag #7140FD" size={24} color="#BA34F5" /> <Tag label="New tag #BA34F5" size={24} color="#BA34F5" />
<Tag label="New tag #7140FD" size={24} color="#7140FD" icon="🧙‍♂️" /> <Tag label="New tag #7140FD" size={24} color="#7140FD" icon="🧙‍♂️" />
<Tag <Tag