Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Filter improvements #1345

Merged
merged 11 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
1emu marked this conversation as resolved.
Show resolved Hide resolved
)

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
Loading