feat: add remaining filters and some polishment with breakpoint table issues component

This commit is contained in:
marcelines 2023-06-07 13:54:54 +01:00
parent dc138f8715
commit ddcddbac35
No known key found for this signature in database
GPG Key ID: 56B1E53E2A3F43C7
11 changed files with 306 additions and 60 deletions

View File

@ -37,7 +37,7 @@ const SideBar = (props: Props) => {
return ( return (
<div className="border-neutral-10 border-r p-5"> <div className="border-neutral-10 border-r p-5">
<aside className=" sticky top-5 min-w-[320px]"> <aside className=" sticky top-5 w-[280px]">
<Accordion.Root <Accordion.Root
type="single" type="single"
collapsible collapsible

View File

@ -11,8 +11,12 @@ function isColorTokens(
const ColorCircle = ({ const ColorCircle = ({
color: colorFromProps, color: colorFromProps,
opacity = 40,
size = 16,
}: { }: {
color?: ColorTokens | `#${string}` color: ColorTokens | `#${string}`
opacity?: number
size?: number
}) => { }) => {
if (!colorFromProps) { if (!colorFromProps) {
return null return null
@ -32,9 +36,9 @@ const ColorCircle = ({
<div <div
className="rounded-full" className="rounded-full"
style={{ style={{
width: 16, width: size,
height: 16, height: size,
backgroundColor: `color-mix(in srgb, ${color} ${40}%, transparent)`, backgroundColor: `color-mix(in srgb, ${color} ${opacity}%, transparent)`,
border: `1px solid ${color}`, border: `1px solid ${color}`,
}} }}
/> />

View File

@ -1,4 +1,4 @@
import { useState } from 'react' import { cloneElement, useState } from 'react'
import { Avatar, Button, Input, Text } from '@status-im/components' import { Avatar, Button, Input, Text } from '@status-im/components'
import { DropdownMenu } from '@status-im/components/src/dropdown-menu' import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
@ -8,16 +8,39 @@ import { ColorCircle } from './components/color-circle'
import type { ColorTokens } from '@tamagui/core' import type { ColorTokens } from '@tamagui/core'
type Props = { type Data = {
data: {
id: number id: number
name: string name: string
avatar?: string avatar?: string | React.ReactElement
color?: ColorTokens | `#${string}` color?: ColorTokens | `#${string}`
}[] }
type Props = {
data: Data[]
label: string label: string
noPadding?: boolean noPadding?: boolean
} }
const isAvatar = (value: unknown): value is string => {
return typeof value === 'string' && value !== null
}
const RenderIcon = (props: Data) => {
if (props.color) {
return <ColorCircle color={props.color} />
}
if (!props.avatar) {
return <></>
}
if (isAvatar(props.avatar)) {
return <Avatar src={props.avatar} size={16} name={props.name} type="user" />
}
return cloneElement(props.avatar) || <></>
}
const FilterWithCheckboxes = (props: Props) => { const FilterWithCheckboxes = (props: Props) => {
const { data, label, noPadding } = props const { data, label, noPadding } = props
@ -32,7 +55,7 @@ const FilterWithCheckboxes = (props: Props) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
return ( return (
<div className={noPadding ? '' : 'pr-3'}> <div className={noPadding ? '' : 'pr-2'}>
<DropdownMenu onOpenChange={() => setIsOpen(!isOpen)}> <DropdownMenu onOpenChange={() => setIsOpen(!isOpen)}>
<Button <Button
size={32} size={32}
@ -63,18 +86,7 @@ const FilterWithCheckboxes = (props: Props) => {
return ( return (
<DropdownMenu.CheckboxItem <DropdownMenu.CheckboxItem
key={filtered.id} key={filtered.id}
icon={ icon={<RenderIcon {...filtered} />}
filtered.color ? (
<ColorCircle color={filtered.color} />
) : (
<Avatar
name={filtered.name}
src={filtered.avatar}
size={16}
type="user"
/>
)
}
label={filtered.name} label={filtered.name}
checked={selectedValues.includes(filtered.id)} checked={selectedValues.includes(filtered.id)}
onSelect={() => { onSelect={() => {

View File

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

View File

@ -0,0 +1,73 @@
import { useState } from 'react'
import { IconButton, Text } from '@status-im/components'
import { DropdownMenu } from '@status-im/components/src/dropdown-menu'
import { SortIcon } from '@status-im/icons'
type Data = {
id: number
name: string
}
type Props = {
data: Data[]
noPadding?: boolean
}
const Sort = (props: Props) => {
const { data, noPadding } = props
const [selectedValue, setSelectedValue] = useState<number>()
return (
<div className={noPadding ? '' : 'pr-2'}>
<DropdownMenu>
<div className="relative">
<IconButton icon={<SortIcon size={20} />} variant="outline" />
</div>
<DropdownMenu.Content sideOffset={10} align="end">
<div className="p-2">
<Text size={13} color="$neutral-80">
Sort by
</Text>
</div>
{data.map(option => {
return (
<DropdownMenu.Item
key={option.id}
label={option.name}
onSelect={() => {
setSelectedValue(option.id)
}}
selected={selectedValue === option.id}
/>
)
})}
{data.length === 0 && (
<div className="flex flex-col items-center justify-center p-2 py-1">
<img
className="pb-3 invert"
alt="No results"
src={'/assets/filters/empty.png'}
width={80}
height={80}
/>
<div className="pb-[2px]">
<Text size={15} weight="semibold">
No options found
</Text>
</div>
<div className="text-center">
<Text size={13}>We didn&apos;t find any results</Text>
</div>
</div>
)}
</DropdownMenu.Content>
</DropdownMenu>
</div>
)
}
export { Sort }
export type { Props as SortProps }

View File

@ -9,14 +9,14 @@ const Tabs = (): JSX.Element => {
return ( return (
<div className="flex"> <div className="flex">
<div className="flex items-center pr-3"> <div className="flex items-center pr-4">
<button <button
className={`flex cursor-pointer flex-row items-center transition-colors ${ className={`flex cursor-pointer flex-row items-center transition-colors ${
isOpen ? 'text-neutral-100' : 'text-neutral-50' isOpen ? 'text-neutral-100' : 'text-neutral-50'
}`} }`}
onClick={() => setActiveTab('open')} onClick={() => setActiveTab('open')}
> >
<OpenIcon size={16} color={isOpen ? '$neutral-100' : '$neutral-50'} /> <OpenIcon size={20} color={isOpen ? '$neutral-100' : '$neutral-50'} />
<span className="pl-1 text-[15px]">784 Open</span> <span className="pl-1 text-[15px]">784 Open</span>
</button> </button>
</div> </div>
@ -28,7 +28,7 @@ const Tabs = (): JSX.Element => {
onClick={() => setActiveTab('closed')} onClick={() => setActiveTab('closed')}
> >
<DoneIcon <DoneIcon
size={16} size={20}
color={!isOpen ? '$neutral-100' : '$neutral-50'} color={!isOpen ? '$neutral-100' : '$neutral-50'}
/> />
<span className="pl-1 text-[15px]">1012 Closed</span> <span className="pl-1 text-[15px]">1012 Closed</span>

View File

@ -1,12 +1,15 @@
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 { SearchIcon } from '@status-im/icons' import { ProfileIcon, SearchIcon } from '@status-im/icons'
import Link from 'next/link' import Link from 'next/link'
import { FilterWithCheckboxes, Tabs } from './filters' import { useCurrentBreakpoint } from '@/hooks/use-current-breakpoint'
import { FilterWithCheckboxes, Sort, Tabs } from './filters'
import type { FilterWithCheckboxesProps } from './filters/filter-with-checkboxes' import type { FilterWithCheckboxesProps } from './filters/filter-with-checkboxes'
import type { SortProps } from './filters/sort'
import type { TextInput } from 'react-native' import type { TextInput } from 'react-native'
const issues = [ const issues = [
@ -121,18 +124,113 @@ const labels: FilterWithCheckboxesProps['data'] = [
}, },
] ]
export const TableIssues = () => { const assignees: FilterWithCheckboxesProps['data'] = [
{
id: 1,
name: 'Unassigned',
avatar: <ProfileIcon size={16} />,
},
{
id: 4,
name: 'marcelines',
avatar: 'https://avatars.githubusercontent.com/u/29401404?v=4',
},
{
id: 5,
name: 'prichodko',
avatar: 'https://avatars.githubusercontent.com/u/14926950?v=4',
},
{
id: 6,
name: 'felicio',
avatar: 'https://avatars.githubusercontent.com/u/13265126?v=4',
},
{
id: 7,
name: 'jkbktl',
avatar: 'https://avatars.githubusercontent.com/u/520927?v=4',
},
]
const repositories: FilterWithCheckboxesProps['data'] = [
{
id: 1,
name: 'status-mobile',
},
{
id: 2,
name: 'status-desktop',
},
{
id: 3,
name: 'status-web',
},
{
id: 4,
name: 'status-go',
},
{
id: 5,
name: 'nwaku',
},
{
id: 6,
name: 'go-waku',
},
{
id: 7,
name: 'js-waku',
},
{
id: 8,
name: 'nimbus-eth2',
},
{
id: 9,
name: 'help.status.im',
},
]
const sortOptions: SortProps['data'] = [
{
id: 1,
name: 'Default',
},
{
id: 2,
name: 'Alphabetical',
},
{
id: 3,
name: 'Creation date',
},
{
id: 4,
name: 'Updated',
},
{
id: 5,
name: 'Completion',
},
]
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)
const currentBreakpoint = useCurrentBreakpoint()
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 w-full flex-col 2xl:flex-row 2xl:justify-between">
<Tabs /> <Tabs />
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-end"> <div className="flex items-center 2xl:justify-end">
<div className="pr-2"> <div className="flex w-full justify-between pr-2 pt-4 2xl:justify-end 2xl:pt-0">
<div className="flex">
<div className="pr-2 transition-all">
<Input <Input
placeholder="Find Author" placeholder="Find Author"
icon={<SearchIcon size={20} />} icon={<SearchIcon size={20} />}
@ -141,13 +239,20 @@ export const TableIssues = () => {
onChangeText={setIssuesSearchText} onChangeText={setIssuesSearchText}
onHandleMinimized={() => setIsMinimized(!isMinimized)} onHandleMinimized={() => setIsMinimized(!isMinimized)}
minimized={isMinimized} minimized={isMinimized}
direction="rtl" direction={currentBreakpoint === '2xl' ? 'rtl' : 'ltr'}
ref={inputRef} ref={inputRef}
/> />
</div> </div>
<FilterWithCheckboxes data={authors} label="Author" /> <FilterWithCheckboxes data={authors} label="Author" />
<FilterWithCheckboxes data={epics} label="Epics" /> <FilterWithCheckboxes data={epics} label="Epics" />
<FilterWithCheckboxes data={labels} label="Label" noPadding /> <FilterWithCheckboxes data={labels} label="Labels" />
<FilterWithCheckboxes data={assignees} label="Assignee" />
<FilterWithCheckboxes data={repositories} label="Repos" />
</div>
<Sort data={sortOptions} noPadding />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -197,3 +302,5 @@ export const TableIssues = () => {
</div> </div>
) )
} }
export { TableIssues }

View File

@ -0,0 +1,43 @@
import { useEffect, useState } from 'react'
import defaultTheme from 'tailwindcss/defaultTheme'
// If we had custom breakpoints, we could use this to get the current breakpoint but we will use the default breakpoints for now
// import resolveConfig from 'tailwindcss/resolveConfig'
// import tailwindConfig from '../../tailwind.config'
// const fullConfig = resolveConfig(tailwindConfig)
export function useCurrentBreakpoint() {
const [currentBreakpoint, setCurrentBreakpoint] = useState('')
useEffect(() => {
const handleResize = () => {
const screenWidth = window.innerWidth
const breakpoints = defaultTheme.screens
? Object.entries(defaultTheme.screens)
: []
for (let i = breakpoints.length - 1; i >= 0; i--) {
const [breakpoint, minWidth] = breakpoints[i]
const convertedMinWidth = parseInt(minWidth, 10)
if (screenWidth >= convertedMinWidth) {
setCurrentBreakpoint(breakpoint)
break
}
}
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return currentBreakpoint || 'sm' // Default breakpoint if none matches or during SSR
}

View File

@ -9,6 +9,7 @@ import {
Root, Root,
Trigger, Trigger,
} from '@radix-ui/react-dropdown-menu' } from '@radix-ui/react-dropdown-menu'
import { CheckIcon } from '@status-im/icons'
import { Stack, styled } from '@tamagui/core' import { Stack, styled } from '@tamagui/core'
import { Checkbox } from '../selectors' import { Checkbox } from '../selectors'
@ -34,7 +35,7 @@ const DropdownMenu = (props: Props) => {
} }
interface DropdownMenuItemProps { interface DropdownMenuItemProps {
icon: React.ReactElement icon?: React.ReactElement
label: string label: string
onSelect: () => void onSelect: () => void
selected?: boolean selected?: boolean
@ -42,23 +43,26 @@ interface DropdownMenuItemProps {
} }
const MenuItem = (props: DropdownMenuItemProps) => { const MenuItem = (props: DropdownMenuItemProps) => {
const { icon, label, onSelect, danger } = props const { icon, label, onSelect, danger, selected } = props
const iconColor = danger ? '$danger-50' : '$neutral-50' const iconColor = danger ? '$danger-50' : '$neutral-50'
const textColor = danger ? '$danger-50' : '$neutral-100' const textColor = danger ? '$danger-50' : '$neutral-100'
return ( return (
<ItemBase onSelect={onSelect}> <ItemBase onSelect={onSelect}>
{cloneElement(icon, { color: iconColor })} <Stack flexDirection="row" gap={8} alignItems="center">
{icon && cloneElement(icon, { color: iconColor })}
<Text size={15} weight="medium" color={textColor}> <Text size={15} weight="medium" color={textColor}>
{label} {label}
</Text> </Text>
</Stack>
{selected && <CheckIcon size={20} color={iconColor} />}
</ItemBase> </ItemBase>
) )
} }
interface DropdownMenuCheckboxItemProps { interface DropdownMenuCheckboxItemProps {
icon: React.ReactElement icon?: React.ReactElement
label: string label: string
onSelect: () => void onSelect: () => void
checked?: boolean checked?: boolean
@ -79,7 +83,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)} {icon && cloneElement(icon)}
<Text size={15} weight="medium" color="$neutral-100"> <Text size={15} weight="medium" color="$neutral-100">
{label} {label}
</Text> </Text>
@ -109,6 +113,8 @@ const ItemBase = styled(DropdownMenuItem, {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
height: 32, height: 32,
paddingHorizontal: 8, paddingHorizontal: 8,
borderRadius: '$10', borderRadius: '$10',

View File

@ -93,8 +93,8 @@ const Base = styled(Stack, {
outline: { outline: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderColor: '$neutral-20', borderColor: '$neutral-30',
hoverStyle: { borderColor: '$neutral-30' }, hoverStyle: { borderColor: '$neutral-40' },
pressStyle: { pressStyle: {
borderColor: '$neutral-20', borderColor: '$neutral-20',
backgroundColor: '$neutral-10', backgroundColor: '$neutral-10',

View File

@ -171,7 +171,7 @@ const InputContainer = styled(Stack, {
paddingHorizontal: 12, paddingHorizontal: 12,
animation: 'medium', animation: 'fast',
width: '100%', width: '100%',
hoverStyle: { hoverStyle: {