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 { 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 { FilterAuthor } from './filters/filter-author'
|
||||
import { FilterWithCheckboxes, Tabs } from './filters'
|
||||
|
||||
import type { FilterWithCheckboxesProps } from './filters/filter-with-checkboxes'
|
||||
import type { TextInput } from 'react-native'
|
||||
|
||||
const issues = [
|
||||
|
@ -51,25 +52,7 @@ const issues = [
|
|||
},
|
||||
]
|
||||
|
||||
const authors = [
|
||||
{
|
||||
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',
|
||||
},
|
||||
const authors: FilterWithCheckboxesProps['data'] = [
|
||||
{
|
||||
id: 4,
|
||||
name: 'marcelines',
|
||||
|
@ -92,9 +75,53 @@ const authors = [
|
|||
},
|
||||
]
|
||||
|
||||
export const TableIssues = () => {
|
||||
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open')
|
||||
const epics: FilterWithCheckboxesProps['data'] = [
|
||||
{
|
||||
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 inputRef = useRef<TextInput>(null)
|
||||
const [isMinimized, setIsMinimized] = useState(true)
|
||||
|
@ -102,30 +129,10 @@ export const TableIssues = () => {
|
|||
return (
|
||||
<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="flex">
|
||||
<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>
|
||||
<Tabs />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex px-1">
|
||||
<div className="pr-2">
|
||||
<Input
|
||||
placeholder="Find Author"
|
||||
icon={<SearchIcon size={20} />}
|
||||
|
@ -138,17 +145,22 @@ export const TableIssues = () => {
|
|||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
<FilterAuthor authors={authors} />
|
||||
<div className="pr-3">
|
||||
<Button size={32} variant="ghost">
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pr-3">
|
||||
<Button size={32} variant="ghost">
|
||||
Sort
|
||||
</Button>
|
||||
</div>
|
||||
<FilterWithCheckboxes
|
||||
data={authors}
|
||||
label="Author"
|
||||
noResultsText="No author found"
|
||||
/>
|
||||
<FilterWithCheckboxes
|
||||
data={epics}
|
||||
label="Epics"
|
||||
noResultsText="No epics found"
|
||||
/>
|
||||
<FilterWithCheckboxes
|
||||
data={labels}
|
||||
label="Label"
|
||||
noResultsText="No labels found"
|
||||
noPadding
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,15 +9,14 @@ const EpicsDetailPage: Page = () => {
|
|||
<div className="border-neutral-10 border-b px-5 py-3">
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<div className="px-10 py-6">
|
||||
<div className="border-neutral-10 border-b px-10 py-6">
|
||||
<EpicOverview
|
||||
title="Communities protocol"
|
||||
description="Detecting keycard reader removal for the beginning of each flow"
|
||||
fullscreen
|
||||
/>
|
||||
|
||||
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" />
|
||||
|
||||
</div>
|
||||
<div className="border-neutral-10 border-b px-10 py-6">
|
||||
<TableIssues />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -79,7 +79,7 @@ const DropdownMenuCheckboxItem = forwardRef<
|
|||
return (
|
||||
<ItemBaseCheckbox {...props} ref={forwardedRef} onSelect={handleSelect}>
|
||||
<Stack flexDirection="row" gap={8} alignItems="center">
|
||||
{cloneElement(icon, { color: '$neutral-50' })}
|
||||
{cloneElement(icon)}
|
||||
<Text size={15} weight="medium" color="$neutral-100">
|
||||
{label}
|
||||
</Text>
|
||||
|
|
|
@ -42,7 +42,7 @@ export const Default: Story = {
|
|||
<Tag label="New tag" size={24} disabled selected />
|
||||
<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
|
||||
|
|
Loading…
Reference in New Issue