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

New approval (proposal) card #1864 #1871

Merged
merged 12 commits into from
Jul 26, 2023
41 changes: 32 additions & 9 deletions src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CommonFeed,
Governance,
PredefinedTypes,
ResolutionType,
} from "@/shared/models";
import { TextEditorValue } from "@/shared/ui-kit";
import {
Expand All @@ -38,7 +39,9 @@ import {
ProposalFeedVotingInfo,
ProposalFeedButtonContainer,
UserVoteInfo,
ImmediateProposalInfo,
} from "./components";
import { ImmediateProposalVoteInfo } from "./components/ImmediateProposalVoteInfo";
import { useProposalSpecificData } from "./hooks";
import {
checkIsVotingAllowed,
Expand Down Expand Up @@ -314,21 +317,41 @@ const ProposalFeedCard: React.FC<ProposalFeedCardProps> = (props) => {
onHover(false);
}}
>
<ProposalFeedVotingInfo
proposal={proposal}
governanceCircles={governanceCircles}
/>
{proposal.resolutionType === ResolutionType.WAIT_FOR_EXPIRATION && (
<>
<ProposalFeedVotingInfo
proposal={proposal}
governanceCircles={governanceCircles}
/>
<UserVoteInfo
userVote={userVote}
userHasPermissionsToVote={userHasPermissionsToVote}
isCountdownState={isCountdownState}
/>
</>
)}

{proposal.resolutionType === ResolutionType.IMMEDIATE && (
<>
<ImmediateProposalInfo
proposal={proposal}
governanceCircles={governanceCircles}
proposerUserName={getUserName(feedItemUser)}
/>
<ImmediateProposalVoteInfo
proposal={proposal}
userVote={userVote}
/>
</>
)}

{isVotingAllowed && (
<ProposalFeedButtonContainer
proposalId={proposal.id}
onVoteCreate={setVote}
resolutionType={proposal.resolutionType}
/>
)}
<UserVoteInfo
userVote={userVote}
userHasPermissionsToVote={userHasPermissionsToVote}
isCountdownState={isCountdownState}
/>
</FeedCardContent>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import "../../../../../../constants";

.container {
display: flex;
flex-direction: column;
margin-bottom: 2rem;
}

.title {
font-size: $moderate-small-2;
}

.subtitle {
font-size: $mobile-title;
color: var(--gray-60);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import { getVotersString } from "@/pages/OldCommon/containers/ProposalContainer/helpers";
import { Governance, Proposal } from "@/shared/models";
import styles from "./ImmediateProposalInfo.module.scss";

interface ImmediateProposalInfoProps {
proposal: Proposal;
governanceCircles: Governance["circles"];
proposerUserName: string;
}

export const ImmediateProposalInfo = ({
proposal,
governanceCircles,
proposerUserName,
}: ImmediateProposalInfoProps) => {
const votersString = getVotersString(
proposal.global.weights,
governanceCircles,
);

return (
<div className={styles.container}>
<div className={styles.title}>
{`${proposerUserName} requests to join ${votersString} circle`}
</div>
{/* Show this only when the required number of voters is greater than 1. Logic for this will be added in the future, see detalis here https://github.com/daostack/common-backend/issues/1844 */}
{/* <div className={styles.subtitle}>
{`Approval by ${votersString} (1 from ${proposal.votes.totalMembersWithVotingRight} required)`}
</div> */}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ImmediateProposalInfo";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import "../../../../../../constants";

.container {
display: flex;
}

.voteInfo {
flex-direction: column;
margin-left: 0.8rem;
}

.title {
font-size: $moderate-small-2;
}

.subtitle {
font-size: $mobile-title;
color: var(--gray-60);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { ReactNode, useEffect } from "react";
import { useSelector } from "react-redux";
import { selectUser } from "@/pages/Auth/store/selectors";
import { useEligibleVoters } from "@/shared/hooks/useCases";
import { VoteAbstain, VoteAgainst, VoteFor } from "@/shared/icons";
import {
DateFormat,
Proposal,
ProposalState,
Vote,
VoteOutcome,
} from "@/shared/models";
import { Loader } from "@/shared/ui-kit";
import { formatDate } from "@/shared/utils";
import styles from "./ImmediateProposalVoteInfo.module.scss";

interface ImmediateProposalVoteInfoProps {
proposal: Proposal;
userVote: Vote | null;
}

const VOTE_OUTCOME_TO_TEXT_MAP: Record<VoteOutcome, string> = {
[VoteOutcome.Approved]: "Approved",
[VoteOutcome.Abstained]: "Abstained",
[VoteOutcome.Rejected]: "Rejected",
};

const VOTE_OUTCOME_TO_ICON_MAP: Record<VoteOutcome, ReactNode> = {
[VoteOutcome.Approved]: <VoteFor className={styles.icon} />,
[VoteOutcome.Abstained]: <VoteAbstain className={styles.icon} />,
[VoteOutcome.Rejected]: <VoteAgainst className={styles.icon} />,
};

export const ImmediateProposalVoteInfo = ({
proposal,
userVote,
}: ImmediateProposalVoteInfoProps) => {
const user = useSelector(selectUser());
const { loading, voters, fetchEligibleVoters, error } = useEligibleVoters();

const isExpired =
proposal.state === ProposalState.FAILED && proposal.votes.total === 0;

/**
* For now we assume that IMMEDIATE proposal is always a single vote proposal.
* In future we would want to support more than a single vote so the logic and the UI might change.
* See more details here https://github.com/daostack/common-backend/issues/1844.
*/
const vote = voters && voters.length > 0 ? voters[0] : undefined;

useEffect(() => {
if (!isExpired) {
fetchEligibleVoters(proposal.id);
}
}, [proposal.id, isExpired]);

if (error) {
return (
<div>
Oops! Something went wrong while loading the vote info. Please try again
later.
</div>
);
}

if (!isExpired && loading) {
return <Loader />;
}

const outcome = userVote?.outcome || vote?.vote?.outcome;
const userId = userVote?.voterId || vote?.userId;
const createdAt =
userVote?.createdAt.seconds || vote?.vote?.createdAt.seconds;

return (
<div className={styles.container}>
{outcome && VOTE_OUTCOME_TO_ICON_MAP[outcome]}
<div className={styles.voteInfo}>
<div className={styles.title}>
{isExpired && "Expired"}
{outcome &&
`${VOTE_OUTCOME_TO_TEXT_MAP[outcome]} by ${
user?.uid === userId ? "You" : vote?.user.displayName
}`}
</div>
<div className={styles.subtitle}>
{createdAt &&
formatDate(new Date(createdAt * 1000), DateFormat.LongHuman)}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ImmediateProposalVoteInfo";
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
}
}

.containerImmediate {
grid-template-areas: "approve reject";
}

.buttonApprove {
grid-area: approve;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React, { FC, useState } from "react";
import classNames from "classnames";
import { useModal } from "@/shared/hooks";
import { Vote, VoteOutcome } from "@/shared/models";
import { ResolutionType, Vote, VoteOutcome } from "@/shared/models";
import { VoteButton } from "../VoteButton";
import { VoteModal } from "./components";
import styles from "./ProposalFeedButtonContainer.module.scss";

interface ProposalFeedButtonContainerProps {
proposalId: string;
onVoteCreate: (vote: Vote) => void;
resolutionType: ResolutionType;
}

export const ProposalFeedButtonContainer: FC<
ProposalFeedButtonContainerProps
> = (props) => {
const { proposalId, onVoteCreate } = props;
const { proposalId, onVoteCreate, resolutionType } = props;
const [voteOutcome, setVoteOutcome] = useState<VoteOutcome | null>(null);
const {
isShowing: isVoteModalOpen,
Expand All @@ -31,23 +33,33 @@ export const ProposalFeedButtonContainer: FC<
onVoteCreate(vote);
};

const isImmediate = resolutionType === ResolutionType.IMMEDIATE;

return (
<>
<div className={styles.container}>
<div
className={classNames(styles.container, {
[styles.containerImmediate]: isImmediate,
})}
>
<VoteButton
className={styles.buttonApprove}
voteOutcome={VoteOutcome.Approved}
onClick={handleVoteButtonClick}
resolutionType={resolutionType}
/>
<VoteButton
className={styles.buttonAbstain}
voteOutcome={VoteOutcome.Abstained}
onClick={handleVoteButtonClick}
/>
{!isImmediate && (
<VoteButton
className={styles.buttonAbstain}
voteOutcome={VoteOutcome.Abstained}
onClick={handleVoteButtonClick}
/>
)}
<VoteButton
className={styles.buttonReject}
voteOutcome={VoteOutcome.Rejected}
onClick={handleVoteButtonClick}
resolutionType={resolutionType}
/>
</div>
{voteOutcome && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
.button {
display: flex;
justify-content: space-between;
border-radius: 0.25rem;
border-radius: 0.3rem;
padding: 0.875rem 1.25rem;
background-color: transparent;
border: 0.06rem solid;
}

.buttonText {
Expand All @@ -15,41 +17,35 @@
}

.approveText {
color: $logo-1;
}
.approveButton {
background-color: $success-100;
box-shadow: $success-100-shadow;
color: var(--primary-fill);
border-color: var(--primary-fill);
}

.approveButton:hover {
background-color: $success-100-hover;
}

.abstainText {
color: $c-neutrals-300;
}

.abstainButton {
background-color: $c-neutrals-100;
box-shadow: $c-neutrals-100-shadow;
color: var(--strong-gray);
border-color: var(--strong-gray);
}

.abstainButton:hover {
background-color: $c-neutrals-100-hover;
}

.rejectText {
color: $c-error-300;
}

.rejectButton {
background-color: $c-error-100;
box-shadow: $c-error-100-shadow;
color: var(--red);
border-color: var(--red);
}

.rejectButton:hover {
background-color: $c-error-100-hover;
}

.icon {
Expand Down
Loading
Loading