feat: add filters with checkboxes
This commit is contained in:
parent
e40ed199d8
commit
c6488be581
|
@ -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 }
|
|
@ -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 }
|
|
|
@ -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 }
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { FilterWithCheckboxes } from './filter-with-checkboxes'
|
||||||
|
export { Tabs } from './tabs'
|
|
@ -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 }
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue