Skip to content

Commit

Permalink
feat: Filter improvements (#1345)
Browse files Browse the repository at this point in the history
* proposal types refactor

* grant multi-category filter

* grouped categories started

* filters finished

* type refactor

* refactor: CategoryOption

* minor fixes

* requested changes

* minor fix

* requested changes
  • Loading branch information
ncomerci authored Oct 24, 2023
1 parent f81b620 commit 3768788
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 192 deletions.
38 changes: 0 additions & 38 deletions src/components/Category/CategoryList.tsx

This file was deleted.

23 changes: 21 additions & 2 deletions src/components/Category/CategoryOption.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,17 @@
.CategoryOption.CategoryOption--draft:hover,
.CategoryOption.CategoryOption--draft.CategoryOption--active,
.CategoryOption.CategoryOption--governance:hover,
.CategoryOption.CategoryOption--governance.CategoryOption--active {
.CategoryOption.CategoryOption--governance.CategoryOption--active,
.CategoryOption.CategoryOption--governance_process:hover,
.CategoryOption.CategoryOption--governance_process.CategoryOption--active {
background-color: var(--background-poll);
}

.CategoryOption.CategoryOption--bidding_and_tendering:hover,
.CategoryOption.CategoryOption--bidding_and_tendering.CategoryOption--active {
background-color: var(--background-pitch);
}

.CategoryOption__Counter {
font-weight: var(--weight-semi-bold);
font-size: 10px;
Expand Down Expand Up @@ -156,10 +163,22 @@
transform: translateY(-50%);
width: var(--size);
height: var(--size);
background-color: var(--purple-800);
border-radius: 50%;
}

.CategoryOption__Subcategories--grant .CategoryOption__SubcategoryItem--active::after,
.CategoryOption__Subcategories--grants .CategoryOption__SubcategoryItem--active::after {
background-color: var(--purple-800);
}

.CategoryOption__Subcategories--governance_process .CategoryOption__SubcategoryItem--active::after {
background-color: var(--orange-800);
}

.CategoryOption__Subcategories--bidding_and_tendering .CategoryOption__SubcategoryItem--active::after {
background-color: var(--red-800);
}

.CategoryOption__SubcategoryItem svg {
margin-right: 8px;
}
149 changes: 49 additions & 100 deletions src/components/Category/CategoryOption.tsx
Original file line number Diff line number Diff line change
@@ -1,139 +1,87 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'

import classNames from 'classnames'
import isNumber from 'lodash/isNumber'
import toSnakeCase from 'lodash/snakeCase'

import { NewGrantCategory, SubtypeAlternativeOptions, SubtypeOptions, toGrantSubtype } from '../../entities/Grant/types'
import { ProposalType } from '../../entities/Proposal/types'
import { CategoryIconVariant } from '../../helpers/styles'
import { SubtypeOptions } from '../../entities/Grant/types'
import { BiddingProcessType, GovernanceProcessType, ProposalType } from '../../entities/Proposal/types'
import useFormatMessage from '../../hooks/useFormatMessage'
import useURLSearchParams from '../../hooks/useURLSearchParams'
import Link from '../Common/Typography/Link'
import Text from '../Common/Typography/Text'
import Arrow from '../Icon/Arrow'
import { getNewGrantsCategoryIcon } from '../Icon/NewGrantsCategoryIcons'
import All from '../Icon/ProposalCategories/All'
import Grant from '../Icon/ProposalCategories/Grant'
import Tender from '../Icon/ProposalCategories/Tender'
import SubItem from '../Icon/SubItem'
import { ProjectTypeFilter } from '../Search/CategoryFilter'

import { categoryIcons } from './CategoryBanner'
import './CategoryOption.css'

type Props = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
type Props = {
active?: boolean
type: string
type: ProposalType | 'all_proposals' | ProjectTypeFilter | 'governance_process'
count?: number
subtypes?: SubtypeOptions[]
onClick?: () => void
href?: string
className?: string
subcategories?: SubtypeOptions[] | GovernanceProcessType[] | BiddingProcessType[]
isSubcategoryActive?: (subcategory: string) => boolean
subcategoryHref?: (href: string | undefined, subcategory: string) => string
icon: React.ReactElement
title: string
}

const icons: Record<string, any> = {
all_proposals: All,
all_projects: All,
community: Grant,
content_creator: Grant,
gaming: Grant,
platform_contributor: Grant,
grants: Grant,
bidding_and_tendering: Tender,
legacy: Grant,
...categoryIcons,
}

export const getCategoryIcon = (type: string, variant?: CategoryIconVariant, size?: number) => {
const newGrants = Object.values(NewGrantCategory)
const newGrantIndex = newGrants.map(toSnakeCase).indexOf(type)
const isNewGrant = newGrantIndex !== -1
if (isNewGrant) {
const icon = getNewGrantsCategoryIcon(newGrants[newGrantIndex])
return icon({ variant: variant || CategoryIconVariant.Filled, size: size })
}

const Icon = icons[type]

return (
<div className="CategoryOption__IconContainer">
<Icon size="24" />
</div>
)
}

const getHref = (href: string | undefined, subtype: SubtypeOptions) => {
const url = new URL(href || '/', 'http://localhost') // Create a URL object using a dummy URL
if (subtype === SubtypeAlternativeOptions.All) {
url.searchParams.delete('subtype')
} else {
url.searchParams.set('subtype', subtype)
}
const newHref = url.pathname + '?' + url.searchParams.toString()
return newHref
}

export default function CategoryOption({ active, type, className, count, subtypes, onClick, ...props }: Props) {
export default function CategoryOption({
href,
active,
type,
className,
count,
onClick,
icon,
title,
subcategories,
subcategoryHref,
isSubcategoryActive,
}: Props) {
const t = useFormatMessage()
const params = useURLSearchParams()
const currentType = useMemo(() => params.get('type'), [params])
const currentSubtype = useMemo(() => toGrantSubtype(params.get('subtype')), [params])

const handleClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
if (onClick) {
onClick(e)
}
},
[onClick]
const isGroupSelected = useMemo(
() => !!subcategories?.includes(currentType as never) || currentType === type,
[subcategories, currentType, type]
)

const isGrant = currentType === ProposalType.Grant || currentType === ProjectTypeFilter.Grants
const hasSubtypes = !!subtypes && subtypes.length > 0
const [isSubtypesOpen, setIsSubtypesOpen] = useState(isGrant)
const [isGroupExpanded, setIsGroupExpanded] = useState(isGroupSelected)

useEffect(() => {
setIsSubtypesOpen(isGrant)
}, [isGrant, currentType])

const isSubtypeActive = useCallback(
(subtype: SubtypeOptions) => {
if (subtype === SubtypeAlternativeOptions.All && !currentSubtype) {
return true
}

return toSnakeCase(subtype) === toSnakeCase(currentSubtype)
},
[currentSubtype]
)
setIsGroupExpanded(isGroupSelected)
}, [isGroupSelected, currentType])

return (
<>
<Link
{...props}
href={props.href || undefined}
onClick={handleClick}
href={href || undefined}
className={classNames(
'CategoryOption',
`CategoryOption--${type}`,
active && 'CategoryOption--active',
className
)}
onClick={(e) => {
e.preventDefault()
setIsGroupExpanded((prev) => !prev)
onClick?.()
}}
>
<span className="CategoryOption__TitleContainer">
<span>
{getCategoryIcon(type, CategoryIconVariant.Circled)}
<div className="CategoryOption__IconContainer">{icon}</div>
<Text weight={active ? 'medium' : 'normal'} className="CategoryOption__Title">
{t(`category.${type}_title`)}
{title}
</Text>
</span>
{hasSubtypes && (
<span
className={classNames('CategoryOption__Arrow', isSubtypesOpen && 'CategoryOption__Arrow--active')}
onClick={(e) => {
e.preventDefault()
setIsSubtypesOpen((prev) => !prev)
}}
>
{subcategories && (
<span className={classNames('CategoryOption__Arrow', isGroupExpanded && 'CategoryOption__Arrow--active')}>
<Arrow filled={false} color="var(--black-700)" />
</span>
)}
Expand All @@ -144,25 +92,26 @@ export default function CategoryOption({ active, type, className, count, subtype
</span>
)}
</Link>
{hasSubtypes && (
{subcategories && (
<div
className={classNames(
'CategoryOption__Subcategories',
isSubtypesOpen && 'CategoryOption__Subcategories--active'
`CategoryOption__Subcategories--${type}`,
isGroupExpanded && 'CategoryOption__Subcategories--active'
)}
>
{subtypes.map((subtype, index) => {
{subcategories.map((item, index) => {
return (
<Link
className={classNames(
'CategoryOption__SubcategoryItem',
isSubtypeActive(subtype) && 'CategoryOption__SubcategoryItem--active'
isSubcategoryActive?.(item) && 'CategoryOption__SubcategoryItem--active'
)}
key={subtype + `-${index}`}
href={getHref(props.href, subtype)}
key={item + `-${index}`}
href={subcategoryHref?.(href, item)}
>
<SubItem />
{t(`category.${toSnakeCase(subtype)}_title`)}
{t(`category.${toSnakeCase(item)}_title`)}
</Link>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import CategoryBanner from '../../Category/CategoryBanner'
import Text from '../../Common/Typography/Text'
import '../ProposalModal.css'

type AddRemoveProposalTypes = ProposalType.POI | ProposalType.Hiring | ProposalType.Catalyst

export type AddRemoveProposalModalProps = ModalProps & {
title: 'poi' | 'hiring' | 'catalyst'
proposalType: ProposalType.POI | ProposalType.Hiring | ProposalType.Catalyst
proposalType: AddRemoveProposalTypes
addType: PoiType | HiringType | CatalystType
isAddDisabled?: boolean
removeType: PoiType | HiringType | CatalystType
isRemoveDisabled?: boolean
}

export function AddRemoveProposalModal({
title,
proposalType,
addType,
isAddDisabled,
Expand All @@ -35,7 +35,7 @@ export function AddRemoveProposalModal({
<Modal {...props} size="tiny" className="GovernanceContentModal ProposalModal" closeIcon={<Close />}>
<Modal.Content>
<div className="ProposalModal__Title">
<Header>{t(`category.${title}_title`)}</Header>
<Header>{t(`category.${proposalType}_title`)}</Header>
<Text size="lg">{t('modal.poi_proposal.description')}</Text>
</div>
</Modal.Content>
Expand Down
26 changes: 22 additions & 4 deletions src/components/Projects/Current/BudgetBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { useIntl } from 'react-intl'

import classNames from 'classnames'
import snakeCase from 'lodash/snakeCase'
import toSnakeCase from 'lodash/snakeCase'

import { ProjectStatus, SubtypeAlternativeOptions, SubtypeOptions, isGrantSubtype } from '../../../entities/Grant/types'
import {
NewGrantCategory,
ProjectStatus,
SubtypeAlternativeOptions,
SubtypeOptions,
isGrantSubtype,
} from '../../../entities/Grant/types'
import { CURRENCY_FORMAT_OPTIONS } from '../../../helpers'
import { CategoryIconVariant } from '../../../helpers/styles'
import useBudgetByCategory from '../../../hooks/useBudgetByCategory'
import useFormatMessage from '../../../hooks/useFormatMessage'
import { getCategoryIcon } from '../../Category/CategoryOption'
import ProgressBar from '../../Common/ProgressBar'
import { getNewGrantsCategoryIcon } from '../../Icon/NewGrantsCategoryIcons'

import './BudgetBanner.css'
import BudgetBannerItem from './BudgetBannerItem'
Expand All @@ -20,6 +26,18 @@ interface Props {
initiativesCount?: number
}

export const getNewGrantIcon = (type: string, variant?: CategoryIconVariant, size?: number) => {
const newGrants = Object.values(NewGrantCategory)
const newGrantIndex = newGrants.map(toSnakeCase).indexOf(type)
const isNewGrant = newGrantIndex !== -1
if (isNewGrant) {
const icon = getNewGrantsCategoryIcon(newGrants[newGrantIndex])
return icon({ variant: variant || CategoryIconVariant.Filled, size: size })
}

return <></>
}

export default function BudgetBanner({ category, status, initiativesCount }: Props) {
const t = useFormatMessage()
const intl = useIntl()
Expand All @@ -35,7 +53,7 @@ export default function BudgetBanner({ category, status, initiativesCount }: Pro
<div className={classNames('BudgetBanner', !showProgress && 'BudgetBanner--start')}>
<div className="BudgetBanner__LabelWithIcon">
{category && (isGrantSubtype(category) || category === SubtypeAlternativeOptions.Legacy) && (
<span>{getCategoryIcon(snakeCase(category), CategoryIconVariant.Circled, 48)}</span>
<span>{getNewGrantIcon(toSnakeCase(category), CategoryIconVariant.Circled, 48)}</span>
)}
<BudgetBannerItem
value={intl.formatNumber(totalBudget, CURRENCY_FORMAT_OPTIONS as any)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Proposal/View/Budget/CategoryTotalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { ProposalAttributes, ProposalType } from '../../../../entities/Proposal/
import { CategoryIconVariant } from '../../../../helpers/styles'
import useFormatMessage from '../../../../hooks/useFormatMessage'
import locations from '../../../../utils/locations'
import { getCategoryIcon } from '../../../Category/CategoryOption'
import { GrantRequestSectionCard } from '../../../GrantRequest/GrantRequestSectionCard'
import { getNewGrantIcon } from '../../../Projects/Current/BudgetBanner'

import './CategoryTotalCard.css'

Expand All @@ -23,7 +23,7 @@ export default function CategoryTotalCard({ proposal, budget }: Props) {
<GrantRequestSectionCard
title={
<>
{getCategoryIcon(snakeCase(grantCategory), CategoryIconVariant.Circled)}
{getNewGrantIcon(snakeCase(grantCategory), CategoryIconVariant.Circled)}
<span className="CategoryTotalCard__Title">
{t('page.proposal_detail.grant.category_budget.title', { category: grantCategory })}
</span>
Expand Down
Loading

0 comments on commit 3768788

Please sign in to comment.