diff --git a/src/components/Banner/Banner.css b/src/components/Banner/Banner.css index 5ae28d5..bdf973b 100644 --- a/src/components/Banner/Banner.css +++ b/src/components/Banner/Banner.css @@ -13,6 +13,10 @@ background-color: var(--blue-200); } +.Banner--red { + background-color: var(--red-200); +} + .Banner--purple { background: var(--alpha-purple-200); } @@ -49,6 +53,10 @@ background-color: var(--blue-800) !important; } +.Banner__Button--red { + background-color: var(--red-800) !important; +} + .Banner__Button--purple { background-color: var(--iris-100) !important; } diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index fd82bbc..f6b290f 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -9,18 +9,19 @@ import Text from '../Common/Typography/Text' import './Banner.css' -type BannerColor = 'blue' | 'purple' +type BannerColor = 'blue' | 'purple' | 'red' export type BannerProps = { isVisible: boolean title: string description: string bannerHideKey: string - icon: React.ReactNode + icon?: React.ReactNode buttonLabel: string onButtonClick?: (event: React.MouseEvent) => void buttonHref?: string color?: BannerColor + isClosable?: boolean } function Banner({ @@ -33,6 +34,7 @@ function Banner({ onButtonClick, buttonHref, color = 'blue', + isClosable = true, }: BannerProps) { const [show, setShow] = useState(isVisible) @@ -54,7 +56,7 @@ function Banner({ <> {show && (
-
{icon}
+ {icon &&
{icon}
}
@@ -76,7 +78,7 @@ function Banner({
- + {isClosable && }
)} diff --git a/src/components/Icon/LateClock.tsx b/src/components/Icon/LateClock.tsx new file mode 100644 index 0000000..5342182 --- /dev/null +++ b/src/components/Icon/LateClock.tsx @@ -0,0 +1,27 @@ +function LateClock({ className, size = '12' }: { size?: string; className?: string }) { + return ( + + + + + + + + + + + + ) +} + +export default LateClock diff --git a/src/components/Icon/ThumbDownCircle.tsx b/src/components/Icon/ThumbDownCircle.tsx new file mode 100644 index 0000000..222f61e --- /dev/null +++ b/src/components/Icon/ThumbDownCircle.tsx @@ -0,0 +1,20 @@ +function ThumbDownCircle({ className, size = '32' }: { size?: string; className?: string }) { + return ( + + + + + ) +} + +export default ThumbDownCircle diff --git a/src/components/Icon/ThumbUpCircle.tsx b/src/components/Icon/ThumbUpCircle.tsx new file mode 100644 index 0000000..a1ef8cd --- /dev/null +++ b/src/components/Icon/ThumbUpCircle.tsx @@ -0,0 +1,20 @@ +function ThumbUpCircle({ className, size = '32' }: { size?: string; className?: string }) { + return ( + + + + + ) +} + +export default ThumbUpCircle diff --git a/src/components/Icon/Warning.tsx b/src/components/Icon/Warning.tsx index e93665d..eaf030f 100644 --- a/src/components/Icon/Warning.tsx +++ b/src/components/Icon/Warning.tsx @@ -1,21 +1,18 @@ function Warning({ className, size = '32' }: { size?: string; className?: string }) { return ( ) } diff --git a/src/components/Projects/ProjectCard/ProjectCard.tsx b/src/components/Projects/ProjectCard/ProjectCard.tsx index 52a2f7d..672fd02 100644 --- a/src/components/Projects/ProjectCard/ProjectCard.tsx +++ b/src/components/Projects/ProjectCard/ProjectCard.tsx @@ -6,7 +6,7 @@ import { ProposalProjectWithUpdate } from '../../../types/proposals' import locations from '../../../utils/locations' import { isProposalInCliffPeriod } from '../../../utils/proposal' import Link from '../../Common/Typography/Link' -import ProposalUpdate from '../../Proposal/Update/ProposalUpdate' +import ProjectUpdateCard from '../../Proposal/Update/ProjectUpdateCard' import CliffProgress from './CliffProgress' import './ProjectCard.css' @@ -37,7 +37,7 @@ const ProjectCard = ({ project, hoverable = false }: Props) => { {proposalInCliffPeriod ? : }
- +
) diff --git a/src/components/Projects/ProjectSidebar.tsx b/src/components/Projects/ProjectSidebar.tsx index 15d8efc..02d1956 100644 --- a/src/components/Projects/ProjectSidebar.tsx +++ b/src/components/Projects/ProjectSidebar.tsx @@ -1,8 +1,12 @@ +import React, { useMemo, useState } from 'react' + import useFormatMessage from '../../hooks/useFormatMessage.ts' import useProject from '../../hooks/useProject.ts' import BoxTabs from '../Common/BoxTabs' import GovernanceSidebar from '../Sidebar/GovernanceSidebar' +import UpdatesTabView from './Updates/UpdatesTabView.tsx' + import ProjectGeneralInfo from './ProjectGeneralInfo.tsx' import ProjectSheetTitle from './ProjectSheetTitle.tsx' import './ProjectSidebar.css' @@ -17,6 +21,29 @@ function ProjectSidebar({ projectId, isSidebarVisible, onClose }: Props) { const { project, isLoadingProject } = useProject(projectId) const t = useFormatMessage() + const [viewIdx, setViewIdx] = useState(0) + + const MENU_ITEMS: { labelKey: string; view: React.ReactNode }[] = useMemo( + () => [ + { + labelKey: 'page.project_sidebar.general_info.title', + view: project && , + }, + { labelKey: 'page.project_sidebar.milestones.title', view: <> }, + { + labelKey: 'page.project_sidebar.updates.title', + view: ( + + ), + }, + { labelKey: 'page.project_sidebar.activity.title', view: <> }, + ], + [project] + ) + return ( - {t('project_sheet.general_info.title')} - {t('project_sheet.milestones.title')} - {t('project_sheet.updates.title')} - {t('project_sheet.activity.title')} + {MENU_ITEMS.map((item, idx) => ( + setViewIdx(idx)}> + {t(item.labelKey)} + + ))} - {project && } +
{MENU_ITEMS[viewIdx].view}
) } diff --git a/src/components/Projects/Updates/PostUpdateBanner.tsx b/src/components/Projects/Updates/PostUpdateBanner.tsx new file mode 100644 index 0000000..b6421b8 --- /dev/null +++ b/src/components/Projects/Updates/PostUpdateBanner.tsx @@ -0,0 +1,27 @@ +import useFormatMessage from '../../../hooks/useFormatMessage' +import Banner from '../../Banner/Banner' + +interface Props { + updateNumber: number + dueDays: number + onClick: () => void +} + +function PostUpdateBanner({ updateNumber, dueDays, onClick }: Props) { + const t = useFormatMessage() + + return ( + + ) +} + +export default PostUpdateBanner diff --git a/src/components/Projects/Updates/UpdatesTabView.tsx b/src/components/Projects/Updates/UpdatesTabView.tsx new file mode 100644 index 0000000..82f6ccf --- /dev/null +++ b/src/components/Projects/Updates/UpdatesTabView.tsx @@ -0,0 +1,117 @@ +import { useCallback, useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import { useAuthContext } from '../../../context/AuthProvider' +import useFormatMessage from '../../../hooks/useFormatMessage' +import useProposalUpdates from '../../../hooks/useProposalUpdates' +import { UpdateStatus } from '../../../types/updates' +import Time from '../../../utils/date/Time' +import locations from '../../../utils/locations' +import { isBetweenLateThresholdDate } from '../../../utils/updates' +import Empty from '../../Common/Empty' +import ConfirmationModal from '../../Modal/ConfirmationModal' +import ProjectUpdateCard from '../../Proposal/Update/ProjectUpdateCard' + +import PostUpdateBanner from './PostUpdateBanner' + +interface Props { + proposalId: string + allowedAddresses: Set +} + +function UpdatesTabView({ allowedAddresses, proposalId }: Props) { + const t = useFormatMessage() + const navigate = useNavigate() + const [account] = useAuthContext() + const [isLateUpdateModalOpen, setIsLateUpdateModalOpen] = useState(false) + const { publicUpdates, nextUpdate, currentUpdate, pendingUpdates, refetchUpdates } = useProposalUpdates(proposalId) + + const updates = publicUpdates || [] + const hasUpdates = updates.length > 0 + const hasSubmittedUpdate = !!currentUpdate?.completion_date + const isAllowedToPostUpdate = !!account && allowedAddresses.has(account) + const nextDueDateRemainingDays = Time(nextUpdate?.due_date).diff(new Date(), 'days') + + const latePendingUpdate = useMemo( + () => + pendingUpdates?.find( + (update) => + update.id !== nextUpdate?.id && Time().isAfter(update.due_date) && isBetweenLateThresholdDate(update.due_date) + ), + [nextUpdate?.id, pendingUpdates] + ) + const navigateToNextUpdateSubmit = useCallback(() => { + const hasUpcomingPendingUpdate = currentUpdate?.id && currentUpdate?.status === UpdateStatus.Pending + navigate( + locations.submitUpdate({ + ...(hasUpcomingPendingUpdate && { id: currentUpdate?.id }), + proposalId, + }) + ) + }, [currentUpdate?.id, currentUpdate?.status, navigate, proposalId]) + const handlePendingModalPrimaryClick = () => { + navigate( + locations.submitUpdate({ + id: latePendingUpdate?.id, + proposalId, + }) + ) + } + const handlePendingModalSecondaryClick = () => { + navigateToNextUpdateSubmit() + } + const handlePostUpdateClick = useCallback(() => { + if (latePendingUpdate) { + setIsLateUpdateModalOpen(true) + + return + } + + navigateToNextUpdateSubmit() + }, [latePendingUpdate, navigateToNextUpdateSubmit]) + + return ( + <> + {isAllowedToPostUpdate && !hasSubmittedUpdate && ( + + )} + {hasUpdates ? ( + updates.map((update, idx) => ( + + )) + ) : ( + + )} + setIsLateUpdateModalOpen(false)} + title={t('page.proposal_detail.grant.pending_update_modal.title')} + description={ + nextUpdate + ? t('page.proposal_detail.grant.pending_update_modal.description') + : t('page.proposal_detail.grant.pending_update_modal.description_last') + } + primaryButtonText={t('page.proposal_detail.grant.pending_update_modal.primary_button')} + secondaryButtonText={ + nextUpdate + ? t('page.proposal_detail.grant.pending_update_modal.secondary_button') + : t('page.proposal_detail.grant.pending_update_modal.secondary_button_additional') + } + /> + + ) +} + +export default UpdatesTabView diff --git a/src/components/Proposal/ProposalSidebar.tsx b/src/components/Proposal/ProposalSidebar.tsx index 5027ae6..e9e67ce 100644 --- a/src/components/Proposal/ProposalSidebar.tsx +++ b/src/components/Proposal/ProposalSidebar.tsx @@ -10,10 +10,7 @@ import { ProjectStatus } from '../../types/grants.ts' import { ProposalAttributes, ProposalStatus } from '../../types/proposals' import { SubscriptionAttributes } from '../../types/subscriptions' import { Survey } from '../../types/surveyTopics' -import { UpdateAttributes } from '../../types/updates' import { SelectedVoteChoice, VoteByAddress } from '../../types/votes' -import { isProjectProposal } from '../../utils/proposal' -import { isProposalStatusWithUpdates } from '../../utils/updates' import { calculateResult } from '../../utils/votes/utils' import { NotDesktop1200 } from '../Layout/Desktop1200' import CalendarAlertModal from '../Modal/CalendarAlertModal' @@ -26,7 +23,6 @@ import ProposalCoAuthorStatus from './View/ProposalCoAuthorStatus' import ProposalDetailSection from './View/ProposalDetailSection' import ProposalGovernanceSection from './View/ProposalGovernanceSection' import ProposalThresholdsSummary from './View/ProposalThresholdsSummary' -import ProposalUpdatesActions from './View/ProposalUpdatesActions' import SubscribeButton from './View/SubscribeButton' import VestingContract from './View/VestingContract' @@ -38,9 +34,6 @@ interface Props { proposalLoading: boolean proposalPageState: ProposalPageState updatePageState: React.Dispatch> - pendingUpdates?: UpdateAttributes[] - nextUpdate?: UpdateAttributes - currentUpdate?: UpdateAttributes | null castingVote: boolean castVote: (selectedChoice: SelectedVoteChoice, survey?: Survey | undefined) => void voteWithSurvey: boolean @@ -63,9 +56,6 @@ export default function ProposalSidebar({ proposalLoading, proposalPageState, updatePageState, - pendingUpdates, - nextUpdate, - currentUpdate, castingVote, castVote, voteWithSurvey, @@ -123,11 +113,6 @@ export default function ProposalSidebar({ setIsVotesListModalOpen(true) } - const showProposalUpdatesActions = - proposal && - isProposalStatusWithUpdates(proposal?.status) && - isProjectProposal(proposal?.type) && - (isOwner || isCoauthor) const showProposalThresholdsSummary = !!( proposal && proposal?.required_to_pass !== null && @@ -161,14 +146,6 @@ export default function ProposalSidebar({ {showVestingContract && } {proposal && }
- {showProposalUpdatesActions && ( - - )}
void onDeleteUpdateClick: () => void } -const getHealthTextKey = (health: UpdateAttributes['health']) => { +const getStatusIcon = (health: UpdateAttributes['health'], completion_date: UpdateAttributes['completion_date']) => { + if (!completion_date) { + return Warning + } + switch (health) { case ProjectHealth.OnTrack: - return 'page.proposal_update.on_track_label' + return ThumbUpCircle case ProjectHealth.AtRisk: - return 'page.proposal_update.at_risk_label' + return Warning case ProjectHealth.OffTrack: - return 'page.proposal_update.off_track_label' + default: + return ThumbDownCircle } } -const CollapsedProposalUpdate = ({ - proposal, +const CollapsedProjectUpdateCard = ({ update, + isAllowedToPostUpdate, index, - isCoauthor, isLinkable, - showHealth, onEditClick, onDeleteUpdateClick, }: Props) => { const t = useFormatMessage() - const [account] = useAuthContext() const navigate = useNavigate() const { status, health, completion_date, author } = update @@ -60,7 +61,6 @@ const CollapsedProposalUpdate = ({ const Component = isLinkable && completion_date ? Link : 'div' const UpdateIcon = getStatusIcon(health, completion_date) - const isAllowedToPostUpdate = account && (proposal.user === account || isCoauthor) const formattedCompletionDate = completion_date ? formatDate(completion_date) : '' const handleUpdateClick = useCallback( @@ -79,41 +79,49 @@ const CollapsedProposalUpdate = ({ href={completion_date ? updateLocation : undefined} onClick={isLinkable ? undefined : handleUpdateClick} className={classNames( - 'ProposalUpdate', - status === UpdateStatus.Pending && 'ProposalUpdate--pending', - !completion_date && 'ProposalUpdate--missed' + 'ProjectUpdateCard', + status === UpdateStatus.Pending && 'ProjectUpdateCard--pending', + !completion_date && 'ProjectUpdateCard--missed' )} > -
-
- +
+
+ + {status === UpdateStatus.Late && }
-
- - {showHealth ? ( +
+ + {t('page.proposal_detail.grant.update_index', { index })} + +
+ {completion_date ? ( <> - {t('page.proposal_update.health_label')}: {t(getHealthTextKey(health))} + + + {t( + `page.update_detail.${status === UpdateStatus.Late ? 'late_completion_date' : 'completion_date'}`, + { date: formattedCompletionDate } + )} + + + <>{author && } ) : ( - t('page.proposal_detail.grant.update_index', { index }) - )} - - {completion_date && ( -
- - - {t('page.update_detail.completion_date', { date: formattedCompletionDate })} - + + {t('page.update_detail.failed_update')} - {author && } -
- )} + )} +
{completion_date && ( -
+
{isAllowedToPostUpdate && ( -
+
)} @@ -124,4 +132,4 @@ const CollapsedProposalUpdate = ({ ) } -export default CollapsedProposalUpdate +export default CollapsedProjectUpdateCard diff --git a/src/components/Proposal/Update/EmptyProjectUpdate.tsx b/src/components/Proposal/Update/EmptyProjectUpdate.tsx new file mode 100644 index 0000000..74affab --- /dev/null +++ b/src/components/Proposal/Update/EmptyProjectUpdate.tsx @@ -0,0 +1,25 @@ +import classNames from 'classnames' + +import useFormatMessage from '../../../hooks/useFormatMessage' +import QuestionCircleIcon from '../../Icon/QuestionCircle' + +import './ProjectUpdateCard.css' + +const EmptyProjectUpdate = () => { + const t = useFormatMessage() + + return ( +
+
+
+ +
+
+ {t('page.grants.empty_update')} +
+
+
+ ) +} + +export default EmptyProjectUpdate diff --git a/src/components/Proposal/Update/EmptyProposalUpdate.tsx b/src/components/Proposal/Update/EmptyProposalUpdate.tsx deleted file mode 100644 index 33f391d..0000000 --- a/src/components/Proposal/Update/EmptyProposalUpdate.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import classNames from 'classnames' - -import useFormatMessage from '../../../hooks/useFormatMessage' -import QuestionCircleIcon from '../../Icon/QuestionCircle' - -import './ProposalUpdate.css' - -const EmptyProposalUpdate = () => { - const t = useFormatMessage() - - return ( -
-
-
- -
-
- {t('page.grants.empty_update')} -
-
-
- ) -} - -export default EmptyProposalUpdate diff --git a/src/components/Proposal/Update/ExpandedProposalUpdate.tsx b/src/components/Proposal/Update/ExpandedProposalUpdate.tsx deleted file mode 100644 index 5946c3f..0000000 --- a/src/components/Proposal/Update/ExpandedProposalUpdate.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useCallback } from 'react' -import { useNavigate } from 'react-router-dom' - -import classNames from 'classnames' - -import useFormatMessage from '../../../hooks/useFormatMessage' -import { ProjectHealth, UpdateAttributes, UpdateStatus } from '../../../types/updates' -import { formatDate } from '../../../utils/date/Time' -import locations from '../../../utils/locations' -import DateTooltip from '../../Common/DateTooltip' -import Link from '../../Common/Typography/Link' -import ChevronRight from '../../Icon/ChevronRight' - -import { getStatusIcon } from './ProposalUpdate' -import './ProposalUpdate.css' -import UpdateMenu from './UpdateMenu' - -interface Props { - update: UpdateAttributes - index?: number - onEditClick: () => void - onDeleteUpdateClick: () => void - showMenu?: boolean -} - -function getIconColor(health: ProjectHealth) { - switch (health) { - case ProjectHealth.OnTrack: - return 'var(--green-800)' - case ProjectHealth.AtRisk: - return 'var(--yellow-800)' - case ProjectHealth.OffTrack: - return 'var(--red-800)' - } -} - -const ExpandedProposalUpdate = ({ update, index, onEditClick, onDeleteUpdateClick, showMenu }: Props) => { - const t = useFormatMessage() - const { introduction, status, health, completion_date } = update - const UpdateIcon = getStatusIcon(health, completion_date) - const navigate = useNavigate() - - const handleUpdateClick = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (e: React.MouseEvent) => { - if (update.completion_date) { - e.stopPropagation() - e.preventDefault() - navigate(locations.update(update.id)) - } - }, - [update, navigate] - ) - - if (!completion_date) { - return null - } - - return ( - -
-
-
- -
- - {t('page.proposal_detail.grant.update_index', { index })} - -
-
- - {formatDate(completion_date)} - - {showMenu && ( -
- -
- )} - {status === UpdateStatus.Late && ( - {t('page.proposal_detail.grant.update_late')} - )} -
-
-
- {introduction} -
-
- {t('page.proposal_detail.grant.update_keep_reading')} - {update?.health && } -
- - ) -} - -export default ExpandedProposalUpdate diff --git a/src/components/Proposal/Update/ProjectUpdateCard.css b/src/components/Proposal/Update/ProjectUpdateCard.css new file mode 100644 index 0000000..fa2f1b5 --- /dev/null +++ b/src/components/Proposal/Update/ProjectUpdateCard.css @@ -0,0 +1,103 @@ +.ProjectUpdateCard { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + border: 1px solid var(--black-300); + border-radius: 8px; + padding: 16px; + min-height: 44px; + margin-bottom: 8px; + cursor: pointer; + box-shadow: 0px 1px 8px 0px var(--alpha-black-200); +} + +.ProjectUpdateCard--pending { + cursor: default; +} + +.ProjectUpdateCard--missed { + cursor: default !important; + box-shadow: unset; + border: none; + background-color: var(--alpha-black-200); +} + +.ProjectUpdateCard__Icon--missed circle { + fill: var(--black-300); +} + +.ProjectUpdateCard__Icon--missed path { + fill: var(--black-600); +} + +.ProjectUpdateCard:last-child { + margin-bottom: 36px; +} + +.ProjectUpdateCard__Left { + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + overflow: hidden; +} + +.ProjectUpdateCard__IconContainer { + margin-right: 12px; + display: flex; + position: relative; +} + +.ProjectUpdateCard__LateClock { + position: absolute; + bottom: 0; + right: 0; +} + +.ProjectUpdateCard__Description { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--black-600); + max-width: 570px; + padding-right: 10px; +} + +.ProjectUpdateCard__Index { + color: inherit !important; +} + +.ProjectUpdateCard__Date { + display: flex; + flex-direction: row; + align-items: center; + font-size: 12px; + line-height: 24px; + color: var(--black-400); + text-transform: uppercase; + gap: 8px; +} + +.ProjectUpdateCard__Details { + display: flex; + gap: 5px; +} + +.ProjectUpdateCard__DateText { + color: var(--black-400); + opacity: 0.5; +} + +.ProjectUpdateCard .ui.basic.button { + padding-right: 0; +} + +.EmptyProjectUpdateCard__Icon path { + fill: var(--black-600); + opacity: 0.8; +} + +.ProjectUpdateCard__Menu { + padding: 0 5px 0 10px; +} diff --git a/src/components/Proposal/Update/ProjectUpdateCard.tsx b/src/components/Proposal/Update/ProjectUpdateCard.tsx new file mode 100644 index 0000000..7e08df4 --- /dev/null +++ b/src/components/Proposal/Update/ProjectUpdateCard.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import { Governance } from '../../../clients/Governance' +import { UpdateAttributes } from '../../../types/updates' +import locations from '../../../utils/locations' +import { DeleteUpdateModal } from '../../Modal/DeleteUpdateModal/DeleteUpdateModal' + +import CollapsedProjectUpdateCard from './CollapsedProjectUpdateCard' +import EmptyProjectUpdate from './EmptyProjectUpdate' + +interface Props { + update?: UpdateAttributes | null + index?: number + onUpdateDeleted?: () => void + isLinkable?: boolean + showHealth?: boolean + isAllowedToPostUpdate: boolean +} + +const ProjectUpdateCard = ({ update, index, onUpdateDeleted, isAllowedToPostUpdate, isLinkable = true }: Props) => { + const [isDeletingUpdate, setIsDeletingUpdate] = useState(false) + const [isDeleteUpdateModalOpen, setIsDeleteUpdateModalOpen] = useState(false) + const navigate = useNavigate() + + if (!update) { + return + } + + const handleEditClick = () => navigate(locations.edit.update(update.id)) + + const handleDeleteUpdateClick = async () => { + try { + setIsDeletingUpdate(true) + await Governance.get().deleteProposalUpdate(update.id) + setIsDeleteUpdateModalOpen(false) + if (onUpdateDeleted) { + onUpdateDeleted() + } + } catch (error) { + console.log('Update delete failed', error) + setIsDeletingUpdate(false) + } + } + + return ( + <> + setIsDeleteUpdateModalOpen(true)} + isAllowedToPostUpdate={isAllowedToPostUpdate} + update={update} + index={index} + isLinkable={isLinkable} + /> + setIsDeleteUpdateModalOpen(false)} + /> + + ) +} + +export default ProjectUpdateCard diff --git a/src/components/Proposal/Update/ProposalUpdate.css b/src/components/Proposal/Update/ProposalUpdate.css deleted file mode 100644 index 7733dbe..0000000 --- a/src/components/Proposal/Update/ProposalUpdate.css +++ /dev/null @@ -1,154 +0,0 @@ -.ProposalUpdate { - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: row; - border: 1px solid var(--black-300); - border-radius: 8px; - padding: 16px; - min-height: 44px; - margin-bottom: 8px; - cursor: pointer; -} - -.ProposalUpdate--expanded { - justify-content: flex-start; - flex-direction: column; - align-items: flex-start; - padding: 12px 16px; - border: 1px solid var(--black-400); -} - -.ProposalUpdate--pending { - cursor: default; -} - -.ProposalUpdate--missed { - cursor: default !important; - border: 1px solid #f3b3be; -} - -.ProposalUpdate:last-child { - margin-bottom: 36px; -} - -.ProposalUpdate__Heading { - display: flex; - flex-direction: row; - width: 100%; - align-items: center; - justify-content: space-between; - margin-bottom: 5px; -} - -.ProposalUpdate__Left { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - overflow: hidden; -} - -.ProposalUpdate__IconContainer { - margin-right: 12px; - display: flex; -} - -.ProposalUpdate__Description { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: var(--black-600); - max-width: 570px; - padding-right: 10px; -} - -.ProposalUpdate__Description--expanded { - word-break: break-word; - text-overflow: ellipsis; - overflow: hidden; - display: -webkit-box !important; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; - color: var(--black-800); - font-size: 15px; - line-height: 24px; - padding-right: 0; - margin-bottom: 8px; -} - -.ProposalUpdate__Index--expanded { - font-size: 13px; - font-style: normal; - font-weight: var(--weight-semi-bold); - line-height: 28px; - letter-spacing: 0.3px; - text-transform: uppercase; - color: var(--black-800); - margin-right: 0; -} - -.ProposalUpdate__Date { - display: flex; - flex-direction: row; - align-items: center; - font-size: 12px; - line-height: 24px; - color: var(--black-400); - text-transform: uppercase; - gap: 8px; -} - -.ProposalUpdate__Details { - display: flex; - gap: 5px; -} - -.ProposalUpdate__DateText { - color: var(--black-400); - opacity: 0.5; -} - -.ProposalUpdate__KeepReading { - display: flex; - align-items: center; - font-weight: var(--weight-semi-bold); - text-transform: uppercase; - gap: 4px; -} - -.ProposalUpdate__KeepReading--onTrack { - color: var(--green-800); -} - -.ProposalUpdate__KeepReading--atRisk { - color: var(--yellow-800); -} - -.ProposalUpdate__KeepReading--offTrack { - color: var(--red-800); -} - -.ProposalUpdate .ui.basic.button { - padding-right: 0; -} - -.ProposalUpdate__Late { - padding: 3px 8px; - border-radius: 12px; - background: var(--black-200); - color: var(--black-800); - text-transform: uppercase; - font-size: 12px; - font-weight: var(--weight-semi-bold); - line-height: 18px; -} - -.EmptyProposalUpdate__Icon path { - fill: var(--black-600); - opacity: 0.8; -} - -.ProposalUpdate__Menu { - padding: 0 5px 0 10px; -} diff --git a/src/components/Proposal/Update/ProposalUpdate.tsx b/src/components/Proposal/Update/ProposalUpdate.tsx deleted file mode 100644 index 6ac0ad4..0000000 --- a/src/components/Proposal/Update/ProposalUpdate.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' - -import { Governance } from '../../../clients/Governance' -import { useAuthContext } from '../../../context/AuthProvider' -import { ProposalAttributes, ProposalProject } from '../../../types/proposals' -import { ProjectHealth, UpdateAttributes } from '../../../types/updates' -import locations from '../../../utils/locations' -import { isSameAddress } from '../../../utils/snapshot' -import CancelIcon from '../../Icon/Cancel' -import CheckCircleIcon from '../../Icon/CheckCircle' -import QuestionCircleIcon from '../../Icon/QuestionCircle' -import WarningIcon from '../../Icon/Warning' -import { DeleteUpdateModal } from '../../Modal/DeleteUpdateModal/DeleteUpdateModal' - -import CollapsedProposalUpdate from './CollapsedProposalUpdate' -import EmptyProposalUpdate from './EmptyProposalUpdate' -import ExpandedProposalUpdate from './ExpandedProposalUpdate' -import './ProposalUpdate.css' - -interface Props { - proposal: ProposalAttributes | ProposalProject - update?: UpdateAttributes | null - expanded: boolean - index?: number - onUpdateDeleted?: () => void - isCoauthor?: boolean - isLinkable?: boolean - showHealth?: boolean -} - -export const getStatusIcon = ( - health: UpdateAttributes['health'], - completion_date: UpdateAttributes['completion_date'] -) => { - if (!completion_date) { - return QuestionCircleIcon - } - - switch (health) { - case ProjectHealth.OnTrack: - return CheckCircleIcon - case ProjectHealth.AtRisk: - return WarningIcon - case ProjectHealth.OffTrack: - default: - return CancelIcon - } -} - -const ProposalUpdate = ({ - proposal, - update, - expanded, - index, - onUpdateDeleted, - isCoauthor, - isLinkable = true, - showHealth, -}: Props) => { - const [isDeletingUpdate, setIsDeletingUpdate] = useState(false) - const [isDeleteUpdateModalOpen, setIsDeleteUpdateModalOpen] = useState(false) - const [account] = useAuthContext() - const navigate = useNavigate() - - if (!update) { - return - } - - const handleEditClick = () => navigate(locations.edit.update(update.id)) - - const handleDeleteUpdateClick = async () => { - try { - setIsDeletingUpdate(true) - await Governance.get().deleteProposalUpdate(update.id) - setIsDeleteUpdateModalOpen(false) - if (onUpdateDeleted) { - onUpdateDeleted() - } - } catch (error) { - console.log('Update delete failed', error) - setIsDeletingUpdate(false) - } - } - - return ( - <> - {expanded && update?.completion_date ? ( - setIsDeleteUpdateModalOpen(true)} - /> - ) : ( - setIsDeleteUpdateModalOpen(true)} - proposal={proposal} - update={update} - index={index} - isCoauthor={isCoauthor} - isLinkable={isLinkable} - showHealth={showHealth} - /> - )} - setIsDeleteUpdateModalOpen(false)} - /> - - ) -} - -export default ProposalUpdate diff --git a/src/components/Proposal/Update/ProposalUpdates.css b/src/components/Proposal/Update/ProposalUpdates.css deleted file mode 100644 index a3eca86..0000000 --- a/src/components/Proposal/Update/ProposalUpdates.css +++ /dev/null @@ -1,8 +0,0 @@ -.ProposalUpdates__EmptyContainer { - margin-top: 36px; - margin-bottom: 53px; -} - -.ProposalUpdates__EmptyIcon { - opacity: 0.32; -} diff --git a/src/components/Proposal/Update/ProposalUpdates.tsx b/src/components/Proposal/Update/ProposalUpdates.tsx deleted file mode 100644 index a705c95..0000000 --- a/src/components/Proposal/Update/ProposalUpdates.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import useFormatMessage from '../../../hooks/useFormatMessage' -import { ProposalAttributes } from '../../../types/proposals' -import { UpdateAttributes } from '../../../types/updates' -import Empty from '../../Common/Empty' -import Megaphone from '../../Icon/Megaphone' -import Section from '../View/Section' - -import ProposalUpdate from './ProposalUpdate' -import './ProposalUpdates.css' - -interface Props { - proposal: ProposalAttributes | null - updates?: UpdateAttributes[] | null - isCoauthor: boolean - onUpdateDeleted: () => void -} - -export default function ProposalUpdates({ proposal, updates, isCoauthor, onUpdateDeleted }: Props) { - const t = useFormatMessage() - - if (!updates || !proposal) { - return null - } - - const hasUpdates = updates.length > 0 - - return ( -
- {!hasUpdates && ( - } - description={t('page.proposal_detail.grant.update_empty')} - /> - )} - {hasUpdates && - updates.map((item, index) => ( - - ))} -
- ) -} diff --git a/src/components/Proposal/View/ProposalUpdatesActions.css b/src/components/Proposal/View/ProposalUpdatesActions.css deleted file mode 100644 index dd31fa2..0000000 --- a/src/components/Proposal/View/ProposalUpdatesActions.css +++ /dev/null @@ -1,23 +0,0 @@ -.ProposalUpdatesActions__UpdateDescription { - margin-bottom: 8px; -} -.ProposalUpdatesActions__UpdateDescriptionBold { - font-weight: var(--weight-medium) !important; -} - -.ProposalUpdatesActions__UpdateButton { - height: 36px; - padding: 0 !important; -} - -.ProposalUpdatesActions__DueDate { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 10px; -} - -.ProposalUpdatesActions__DueDate .ProposalUpdatesActions__InfoIconContainer { - display: flex; - align-items: center; -} diff --git a/src/components/Proposal/View/ProposalUpdatesActions.tsx b/src/components/Proposal/View/ProposalUpdatesActions.tsx deleted file mode 100644 index 05b98d7..0000000 --- a/src/components/Proposal/View/ProposalUpdatesActions.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import { useNavigate } from 'react-router-dom' - -import { Button } from 'decentraland-ui/dist/components/Button/Button' - -import useFormatMessage from '../../../hooks/useFormatMessage' -import { ProposalAttributes } from '../../../types/proposals' -import { UpdateAttributes, UpdateStatus } from '../../../types/updates' -import Time from '../../../utils/date/Time' -import locations from '../../../utils/locations' -import { isBetweenLateThresholdDate } from '../../../utils/updates' -import DateTooltip from '../../Common/DateTooltip' -import Markdown from '../../Common/Typography/Markdown' -import Helper from '../../Helper/Helper' -import ConfirmationModal from '../../Modal/ConfirmationModal' - -import './ProposalUpdatesActions.css' - -type ProposalUpdatesActionsProps = { - nextUpdate?: UpdateAttributes | null - currentUpdate?: UpdateAttributes | null - pendingUpdates?: UpdateAttributes[] - proposal: ProposalAttributes -} - -export default function ProposalUpdatesActions({ - nextUpdate, - currentUpdate, - proposal, - pendingUpdates, -}: ProposalUpdatesActionsProps) { - const t = useFormatMessage() - const hasSubmittedUpdate = !!currentUpdate?.completion_date - const [isLateUpdateModalOpen, setIsLateUpdateModalOpen] = useState(false) - const latePendingUpdate = useMemo( - () => - pendingUpdates?.find( - (update) => - update.id !== nextUpdate?.id && Time().isAfter(update.due_date) && isBetweenLateThresholdDate(update.due_date) - ), - [nextUpdate?.id, pendingUpdates] - ) - const navigate = useNavigate() - - const navigateToNextUpdateSubmit = useCallback(() => { - const hasUpcomingPendingUpdate = currentUpdate?.id && currentUpdate?.status === UpdateStatus.Pending - navigate( - locations.submitUpdate({ - ...(hasUpcomingPendingUpdate && { id: currentUpdate?.id }), - proposalId: proposal.id, - }) - ) - }, [currentUpdate?.id, currentUpdate?.status, navigate, proposal.id]) - - const onPostUpdateClick = useCallback(() => { - if (latePendingUpdate) { - setIsLateUpdateModalOpen(true) - - return - } - - navigateToNextUpdateSubmit() - }, [latePendingUpdate, navigateToNextUpdateSubmit]) - - const handlePendingModalPrimaryClick = () => { - navigate( - locations.submitUpdate({ - id: latePendingUpdate?.id, - proposalId: proposal.id, - }) - ) - } - - const handlePendingModalSecondaryClick = () => { - navigateToNextUpdateSubmit() - } - - return ( -
-
- - {t('page.proposal_detail.grant.update_description')} - - - {!hasSubmittedUpdate && nextUpdate?.due_date && currentUpdate?.due_date && ( - - - - {t('page.proposal_detail.grant.current_update_due_date', { - date: Time(currentUpdate.due_date).fromNow(true), - })} - - - - - )} - {hasSubmittedUpdate && nextUpdate?.due_date && currentUpdate?.due_date && ( - - - - {t('page.proposal_detail.grant.next_update_due_date', { - date: Time(nextUpdate.due_date).fromNow(true), - })} - - - - )} -
- setIsLateUpdateModalOpen(false)} - title={t('page.proposal_detail.grant.pending_update_modal.title')} - description={ - nextUpdate - ? t('page.proposal_detail.grant.pending_update_modal.description') - : t('page.proposal_detail.grant.pending_update_modal.description_last') - } - primaryButtonText={t('page.proposal_detail.grant.pending_update_modal.primary_button')} - secondaryButtonText={ - nextUpdate - ? t('page.proposal_detail.grant.pending_update_modal.secondary_button') - : t('page.proposal_detail.grant.pending_update_modal.secondary_button_additional') - } - /> -
- ) -} diff --git a/src/intl/en.json b/src/intl/en.json index 270bdc4..3962d0d 100644 --- a/src/intl/en.json +++ b/src/intl/en.json @@ -1125,6 +1125,25 @@ "rejected_description": "You've rejected being involved in this initiative.", "revert_label": "Revert" }, + "project_sidebar": { + "general_info": { + "title": "General Info" + }, + "milestones": { + "title": "Milestones" + }, + "updates": { + "title": "Updates", + "banner": { + "title": "Update #{number} due in {days} {days, plural, one {day} other {days}}", + "description": "Improve your reputation by posting timely updates", + "button": "Post Update" + } + }, + "activity": { + "title": "Activity" + } + }, "proposal_detail": { "title": "Decentraland DAO", "description": "The governance hub for Decentraland. Create and vote on proposals that help shape the future of the metaverse.", @@ -1704,9 +1723,11 @@ "financial_details": "Financial details", "additional_notes": "Additional notes and links", "completion_date": "Posted {date} by", + "late_completion_date": "Late update, {date} by", "edit_date": "Update last edited **{date}**", "due_date": "Update shared **{date} late**, after it was due.", - "delete_update": "Delete update" + "delete_update": "Delete update", + "failed_update": "Team failed to report progress" }, "submit": { "title": "Submit Proposal", diff --git a/src/pages/proposal.tsx b/src/pages/proposal.tsx index 7a19373..61debd0 100644 --- a/src/pages/proposal.tsx +++ b/src/pages/proposal.tsx @@ -33,7 +33,6 @@ import ProposalHero from '../components/Proposal/ProposalHero' import ProposalSidebar from '../components/Proposal/ProposalSidebar' import VotingRationaleSection from '../components/Proposal/Rationale/VotingRationaleSection' import SurveyResults from '../components/Proposal/SentimentSurvey/SurveyResults' -import ProposalUpdates from '../components/Proposal/Update/ProposalUpdates' import AuthorDetails from '../components/Proposal/View/AuthorDetails' import BiddingAndTendering from '../components/Proposal/View/BiddingAndTendering' import ProposalBudget from '../components/Proposal/View/Budget/ProposalBudget' @@ -66,9 +65,8 @@ import { SelectedVoteChoice } from '../types/votes' import { ErrorCategory } from '../utils/errorCategories' import locations from '../utils/locations' import { isUnderMaintenance } from '../utils/maintenance' -import { isBiddingAndTenderingProposal, isGovernanceProcessProposal, isProjectProposal } from '../utils/proposal' +import { isBiddingAndTenderingProposal, isGovernanceProcessProposal } from '../utils/proposal' import { SurveyEncoder } from '../utils/surveyTopics' -import { isProposalStatusWithUpdates } from '../utils/updates' import './proposal.css' @@ -159,10 +157,7 @@ export default function ProposalPage() { }) const { budgetWithContestants, isLoadingBudgetWithContestants } = useBudgetWithContestants(proposal?.id) - const { publicUpdates, pendingUpdates, nextUpdate, currentUpdate, refetchUpdates } = useProposalUpdates(proposal?.id) - const showProposalUpdates = - publicUpdates && isProposalStatusWithUpdates(proposal?.status) && isProjectProposal(proposal?.type) - + const { publicUpdates, refetchUpdates } = useProposalUpdates(proposal?.id) const { surveyTopics, isLoadingSurveyTopics, voteWithSurvey, showSurveyResults } = useSurvey( proposal, votes, @@ -384,14 +379,6 @@ export default function ProposalPage() { {proposal?.type === ProposalType.POI && } {proposal && isBiddingAndTenderingProposal(proposal?.type) && } {showAuthorDetails && } - {showProposalUpdates && ( - - )} {showVotesChart && (