Skip to content

Commit

Permalink
feat: author details sidebar (#1403)
Browse files Browse the repository at this point in the history
* feat: add author details sidebar

* feat: add chevron in author details stats container

* feat: improve date in mobile

* feat: add view more grants button

* feat: improve typing

* feat: clickable only if it has projects

* feat: update project status pill colors

* fix: proposal card props

* feat: hide date on mobile
  • Loading branch information
andyesp authored Nov 16, 2023
1 parent 4e73381 commit 8d8dc0b
Show file tree
Hide file tree
Showing 31 changed files with 535 additions and 272 deletions.
2 changes: 1 addition & 1 deletion src/components/Common/ArticleSectionHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Props {

export default function ArticleSectionHeading({ children }: Props) {
return (
<Heading className="ArticleSectionHeading" size="2xs" weight="semi-bold">
<Heading className="ArticleSectionHeading" size="3xs" weight="semi-bold">
{children}
</Heading>
)
Expand Down
11 changes: 10 additions & 1 deletion src/components/Common/Pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ export enum PillColor {
Transparent = 'transparent',
}

export enum PillStyle {
Shiny = 'shiny',
Medium = 'medium',
Light = 'light',
Outline = 'outline',
}

export type PillStyleType = PillStyle | `${PillStyle}`

export type Props = {
children: React.ReactText
color?: PillColor | `${PillColor}`
size?: 'sm' | 'md'
style?: 'shiny' | 'medium' | 'light' | 'outline'
style?: PillStyleType
className?: string
icon?: React.ReactNode
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
padding: 16px;
justify-content: space-between;
align-items: center;
transition: box-shadow 0.1s ease, transform 0.1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
transition: var(--card-transition);
}

.ProposalPreviewCard--vote {
Expand Down
5 changes: 5 additions & 0 deletions src/components/Common/Typography/Heading.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
}

.Heading--2xs {
font-size: 15px;
line-height: 24px;
}

.Heading--3xs {
font-size: 13px;
line-height: 18px;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Common/Typography/Heading.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import classNames from 'classnames'

import './Heading.css'
import { FontSize, FontWeight } from './Text'
import { FontWeight } from './Text'

type HeadingTypes = 'h1' | 'h3'

type Props = React.HTMLAttributes<HTMLHeadingElement> & {
className?: string
as?: HeadingTypes
size?: FontSize | '2xs'
size?: '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
weight?: FontWeight
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/Modal/BidVotingModal/BidVotingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ function BidVotingModal({ onCastVote, castingVote, linkedTenderId, proposalPageS
proposal={proposal}
showBudget
isDisabled={proposal.snapshot_id === currentProposal}
hideUser
showUser={false}
showLeadingVP
hideEndDate
showEndDate={false}
/>
))}
</Modal.Content>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Projects/ProjectCard/ProjectCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
justify-content: space-between;
cursor: pointer;
background: var(--white-900);
transition: box-shadow 0.1s ease, transform 0.1s ease;
transition: var(--card-transition);
min-width: 0;
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/Projects/ProjectPill.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NewGrantCategory, OldGrantCategory, ProposalGrantCategory } from '../../entities/Grant/types'
import { ProposalType } from '../../entities/Proposal/types'
import Pill, { PillColor } from '../Common/Pill'
import Pill, { PillColor, PillStyle, PillStyleType } from '../Common/Pill'

interface Props {
type: ProposalGrantCategory
style?: PillStyleType
}

const PROJECT_CATEGORY_COLORS: Record<ProposalGrantCategory | ProposalType.Tender, PillColor> = {
Expand Down Expand Up @@ -33,11 +34,11 @@ function getProjectCategory(
return category
}

export default function ProjectPill({ type }: Props) {
export default function ProjectPill({ type, style = PillStyle.Shiny }: Props) {
const categoryType = getProjectCategory(type)

return (
<Pill size="sm" color={PROJECT_CATEGORY_COLORS[categoryType]}>
<Pill size="sm" color={PROJECT_CATEGORY_COLORS[categoryType]} style={style}>
{categoryType.split(' ')[0]}
</Pill>
)
Expand Down
26 changes: 26 additions & 0 deletions src/components/Projects/ProjectStatusPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ProjectStatus } from '../../entities/Grant/types'
import { getEnumDisplayName } from '../../helpers'
import Pill, { PillColor } from '../Common/Pill'

interface Props {
status: ProjectStatus
}

const STATUS_COLORS: Record<ProjectStatus, PillColor> = {
[ProjectStatus.InProgress]: PillColor.Green,
[ProjectStatus.Finished]: PillColor.Green,
[ProjectStatus.Revoked]: PillColor.Red,
[ProjectStatus.Paused]: PillColor.Gray,
[ProjectStatus.Pending]: PillColor.Gray,
}

export default function ProjectStatusPill({ status }: Props) {
const displayedStatus = getEnumDisplayName(status)
const style = status === ProjectStatus.InProgress ? 'outline' : 'shiny'

return (
<Pill size="sm" color={STATUS_COLORS[status]} style={style}>
{displayedStatus}
</Pill>
)
}
34 changes: 34 additions & 0 deletions src/components/Proposal/View/AuthorDetails.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
}

.AuthorDetails__UserContainer {
gap: 8px;
margin-bottom: 21px;
}

Expand Down Expand Up @@ -52,10 +53,43 @@
border-radius: 8px;
border: 1px solid var(--alpha-black-300);
background: var(--white-900);
position: relative;
}

.AuthorDetails__Chevron {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
right: 14px;
}

@media (min-width: 768px) {
.AuthorDetails__StatsContainer {
grid-template-columns: repeat(2, 1fr);
}
}

.AuthorDetails__StatsContainer--clickable {
cursor: pointer;
transition: var(--card-transition);
}

.AuthorDetails__StatsContainer--clickable:hover {
cursor: pointer;
box-shadow: var(--shadow-2);
transform: translateY(-4px);
}

.AuthorDetails__SidebarList {
display: flex;
flex-direction: column;
gap: 26px;
margin-bottom: 8px;
}

.AuthorDetails__SidebarSubtitle {
color: var(--black-800);
font-size: 15px;
font-weight: 600;
line-height: 24px;
}
100 changes: 75 additions & 25 deletions src/components/Proposal/View/AuthorDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { useIntl } from 'react-intl'

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

import { ProjectStatus } from '../../../entities/Grant/types'
import { ProposalType } from '../../../entities/Proposal/types'
import { Project, ProposalType } from '../../../entities/Proposal/types'
import { CURRENCY_FORMAT_OPTIONS } from '../../../helpers'
import useAddressVotes from '../../../hooks/useAddressVotes'
import useFormatMessage from '../../../hooks/useFormatMessage'
import useGovernanceProfile from '../../../hooks/useGovernanceProfile'
import useProfile from '../../../hooks/useProfile'
import useProposals from '../../../hooks/useProposals'
import useVestings from '../../../hooks/useVestings'
import useVotingStats from '../../../hooks/useVotingStats'
import Time from '../../../utils/date/Time'
import locations from '../../../utils/locations'
import { createProject } from '../../../utils/projects'
import FullWidthButton from '../../Common/FullWidthButton'
import InvertedButton from '../../Common/InvertedButton'
import Heading from '../../Common/Typography/Heading'
import Link from '../../Common/Typography/Link'
import Username from '../../Common/Username'
import ChevronRight from '../../Icon/ChevronRight'
import GovernanceSidebar from '../../Sidebar/GovernanceSidebar'
import ValidatedProfileCheck from '../../User/ValidatedProfileCheck'

import './AuthorDetails.css'
import AuthorDetailsStat from './AuthorDetailsStat'
import ProjectCardList from './ProjectCardList'
import Section from './Section'

interface Props {
Expand All @@ -36,53 +45,54 @@ export default function AuthorDetails({ address }: Props) {
const { proposals: grants } = useProposals({ user: address, type: ProposalType.Grant })
const intl = useIntl()
const hasPreviouslySubmittedGrants = !!grants && grants?.total > 1
const [isSidebarVisible, setIsSidebarVisible] = useState(false)
const { displayableAddress } = useProfile(address)

const { data: vestings } = useVestings(hasPreviouslySubmittedGrants)
const grantsWithVesting = useMemo(
const projects = useMemo(
() =>
grants?.data.map((grant) => {
const vesting = vestings?.find((item) => grant.id === item.proposal_id)
const releaseable = vesting?.vesting_releasable || 0
const released = vesting?.vesting_released || 0

return {
...grant,
vested: releaseable + released,
vesting_status: vesting?.vesting_status,
}
}),
grants?.data.map((grant) =>
createProject(
grant,
vestings?.find((item) => grant.id === item.proposal_id)
)
) || [],
[vestings, grants?.data]
)
const fundsVested = useMemo(
() => grantsWithVesting?.reduce((total, grant) => total + grant.vested, 0),
[grantsWithVesting]
() => projects?.reduce((total, grant) => total + (grant?.contract?.vestedAmount || 0), 0),
[projects]
)

const projectPerformanceTotals = useMemo(
const projectsByStatus = useMemo(
() =>
SHOWN_PERFORMANCE_STATUSES.reduce((acc, cur) => {
const total = grantsWithVesting?.filter((item) => item.vesting_status === cur).length || 0
return total > 0 ? { ...acc, [cur]: total } : acc
}, {} as Record<string, number>),
[grantsWithVesting]
const items = projects?.filter((item) => item.status === cur)
const total = items?.length || 0
return total > 0 ? { ...acc, [cur]: { items, total } } : acc
}, {} as Record<string, { items: Project[]; total: number }>),
[projects]
)

const projectPerformanceText = useMemo(
() =>
Object.keys(projectPerformanceTotals)
Object.keys(projectsByStatus)
.map((item) =>
t(`page.proposal_detail.author_details.project_performance_${snakeCase(item)}`, {
total: projectPerformanceTotals[item],
total: projectsByStatus[item].total,
})
)
.join(', '),
[projectPerformanceTotals, t]
[projectsByStatus, t]
)

const { votes } = useAddressVotes(address)
const hasVoted = votes && votes.length > 0
const activeSinceFormattedDate = hasVoted ? Time.unix(votes[0].created).format('MMMM, YYYY') : ''

const hasProjects = !isEmpty(projectsByStatus)
const handleClose = () => setIsSidebarVisible(false)

return (
<Section title={t('page.proposal_detail.author_details.title')} isNew>
<div className="AuthorDetails__UserContainer">
Expand All @@ -106,7 +116,13 @@ export default function AuthorDetails({ address }: Props) {
<InvertedButton>{t('page.proposal_detail.author_details.view_profile')}</InvertedButton>
</Link>
</div>
<div className="AuthorDetails__StatsContainer">
<div
className={classNames(
'AuthorDetails__StatsContainer',
hasProjects && 'AuthorDetails__StatsContainer--clickable'
)}
onClick={() => hasProjects && setIsSidebarVisible(true)}
>
{!hasPreviouslySubmittedGrants && (
<AuthorDetailsStat
label={t('page.proposal_detail.author_details.grant_stats_label')}
Expand Down Expand Up @@ -139,7 +155,41 @@ export default function AuthorDetails({ address }: Props) {
total: participationTotal,
})}
/>
{hasProjects && (
<div className="AuthorDetails__Chevron">
<ChevronRight color="var(--black-400)" />
</div>
)}
</div>
<GovernanceSidebar
title={t('page.proposal_detail.author_details.sidebar.title', { username: displayableAddress })}
visible={isSidebarVisible}
onClose={handleClose}
>
<div>
<div className="AuthorDetails__SidebarList">
{projectsByStatus &&
Object.keys(projectsByStatus).map((item) => {
const projects = projectsByStatus[item].items

return (
<div key={item}>
<Heading size="2xs" weight="semi-bold">
{t('page.proposal_detail.author_details.sidebar.subtitle', {
total: projects.length,
status: item,
})}
</Heading>
<ProjectCardList projects={projects} />
</div>
)
})}
</div>
<FullWidthButton href={locations.profile({ address })}>
{t('page.proposal_detail.author_details.sidebar.view_profile')}
</FullWidthButton>
</div>
</GovernanceSidebar>
</Section>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
margin-bottom: 11px;
}

.CompetingProposalsSidebar__TitleContainer > .dcl.close {
width: 20px;
}

.CompetingProposalsSidebar__Title {
font-weight: var(--weight-semi-bold);
font-size: 17px;
Expand Down
Loading

0 comments on commit 8d8dc0b

Please sign in to comment.