diff --git a/web/src/assets/svgs/icons/gavel-executed.svg b/web/src/assets/svgs/icons/gavel-executed.svg new file mode 100644 index 000000000..5e9b5777f --- /dev/null +++ b/web/src/assets/svgs/icons/gavel-executed.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/components/DisputePreview/DisputeContext.tsx b/web/src/components/DisputePreview/DisputeContext.tsx index d5e7130d1..435bc47a8 100644 --- a/web/src/components/DisputePreview/DisputeContext.tsx +++ b/web/src/components/DisputePreview/DisputeContext.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useMemo } from "react"; import styled from "styled-components"; import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes"; +import { useAccount } from "wagmi"; import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index"; import { Answer as IAnswer } from "context/NewDisputeContext"; @@ -9,6 +10,8 @@ import { isUndefined } from "utils/index"; import { responsiveSize } from "styles/responsiveSize"; +import { DisputeDetailsQuery, VotingHistoryQuery } from "src/graphql/graphql"; + import ReactMarkdown from "components/ReactMarkdown"; import { StyledSkeleton } from "components/StyledSkeleton"; @@ -16,14 +19,22 @@ import { Divider } from "../Divider"; import { ExternalLink } from "../ExternalLink"; import AliasDisplay from "./Alias"; +import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators"; +import CardLabel from "../DisputeView/CardLabels"; const StyledH1 = styled.h1` margin: 0; word-wrap: break-word; - font-size: ${responsiveSize(18, 24)}; + font-size: ${responsiveSize(20, 26)}; line-height: 24px; `; +const TitleSection = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + const ReactMarkdownWrapper = styled.div` & p:first-of-type { margin: 0; @@ -66,19 +77,59 @@ const AliasesContainer = styled.div` gap: ${responsiveSize(8, 20)}; `; +const RulingAndRewardsAndLabels = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; +`; + interface IDisputeContext { disputeDetails?: DisputeDetails; isRpcError?: boolean; + dispute?: DisputeDetailsQuery | undefined; + + disputeId?: string; + votingHistory?: VotingHistoryQuery | undefined; } -export const DisputeContext: React.FC = ({ disputeDetails, isRpcError = false }) => { +export const DisputeContext: React.FC = ({ + disputeDetails, + isRpcError = false, + dispute, + disputeId, + votingHistory, +}) => { + const { isDisconnected } = useAccount(); const errMsg = isRpcError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR; + const rounds = votingHistory?.dispute?.rounds; + const jurorRewardsDispersed = useMemo(() => Boolean(rounds?.every((round) => round.jurorRewardsDispersed)), [rounds]); + console.log({ jurorRewardsDispersed }, disputeDetails); return ( <> - - {isUndefined(disputeDetails) ? : (disputeDetails?.title ?? errMsg)} - + + + {isUndefined(disputeDetails) ? : (disputeDetails?.title ?? errMsg)} + + {!isUndefined(disputeDetails) && + !isUndefined(dispute) && + !isUndefined(disputeId) && + !isUndefined(votingHistory) ? ( + + {!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? ( + + ) : null} + {!isDisconnected ? ( + + ) : null} + + ) : null} + + {disputeDetails?.question?.trim() || disputeDetails?.description?.trim() ? (
{disputeDetails?.question?.trim() ? ( diff --git a/web/src/components/DisputeView/CardLabels/index.tsx b/web/src/components/DisputeView/CardLabels/index.tsx index 523539ddf..d6ca94a56 100644 --- a/web/src/components/DisputeView/CardLabels/index.tsx +++ b/web/src/components/DisputeView/CardLabels/index.tsx @@ -22,11 +22,12 @@ import { ClassicContribution } from "src/graphql/graphql"; import Label, { IColors } from "./Label"; import RewardsAndFundLabel from "./RewardsAndFundLabel"; -const Container = styled.div<{ isList: boolean }>` +const Container = styled.div<{ isList: boolean; isOverview: boolean }>` display: flex; gap: 8px; flex-direction: column; align-items: end; + ${({ isList }) => !isList && css` @@ -36,7 +37,16 @@ const Container = styled.div<{ isList: boolean }>` flex-direction: row; align-items: center; `} + + ${({ isOverview }) => + isOverview && + css` + margin-top: 0; + flex-direction: row; + width: auto; + `} `; + const RewardsContainer = styled.div` display: flex; gap: 4px 8px; @@ -47,6 +57,7 @@ interface ICardLabels { disputeId: string; round: number; isList: boolean; + isOverview?: boolean; } const LabelArgs: Record>; color: IColors }> = { @@ -73,7 +84,7 @@ const getFundingRewards = (contributions: ClassicContribution[], closed: boolean return Number(formatUnits(BigInt(contribution), 18)); }; -const CardLabel: React.FC = ({ disputeId, round, isList }) => { +const CardLabel: React.FC = ({ disputeId, round, isList, isOverview = false }) => { const { address } = useAccount(); const { data: labelInfo, isLoading } = useLabelInfoQuery(address?.toLowerCase(), disputeId); const localRounds = getLocalRounds(labelInfo?.dispute?.disputeKitDispute); @@ -139,7 +150,7 @@ const CardLabel: React.FC = ({ disputeId, round, isList }) => { }, [contributionRewards, shifts]); return ( - + {isLoading ? ( ) : ( diff --git a/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx b/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx index 6bc83242d..0f1818fda 100644 --- a/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx +++ b/web/src/components/DisputeView/DisputeInfo/DisputeInfoCard.tsx @@ -60,7 +60,7 @@ const DisputeInfoCard: React.FC = ({ isOverview, showLabels, f item.display ? : null )} - {showLabels ? : null} + {showLabels ? : null} ); }; diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx index 2bbc53879..754f1dadf 100644 --- a/web/src/components/Verdict/DisputeTimeline.tsx +++ b/web/src/components/Verdict/DisputeTimeline.tsx @@ -1,14 +1,13 @@ import React, { useMemo } from "react"; import styled, { useTheme } from "styled-components"; -import Skeleton from "react-loading-skeleton"; import { useParams } from "react-router-dom"; import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library"; -import CalendarIcon from "svgs/icons/calendar.svg"; import ClosedCaseIcon from "svgs/icons/check-circle-outline.svg"; import NewTabIcon from "svgs/icons/new-tab.svg"; +import GavelExecutedIcon from "svgs/icons/gavel-executed.svg"; import { Periods } from "consts/periods"; import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData"; @@ -21,8 +20,6 @@ import { useVotingHistory } from "queries/useVotingHistory"; import { ClassicRound } from "src/graphql/graphql"; import { getTxnExplorerLink } from "src/utils"; -import { responsiveSize } from "styles/responsiveSize"; - import { StyledClosedCircle } from "components/StyledIcons/ClosedCircleIcon"; import { ExternalLink } from "../ExternalLink"; @@ -37,24 +34,6 @@ const StyledTimeline = styled(CustomTimeline)` width: 100%; `; -const EnforcementContainer = styled.div` - display: flex; - gap: 8px; - margin-top: ${responsiveSize(12, 24)}; - fill: ${({ theme }) => theme.secondaryText}; - - small { - font-weight: 400; - line-height: 19px; - color: ${({ theme }) => theme.secondaryText}; - } -`; - -const StyledCalendarIcon = styled(CalendarIcon)` - width: 14px; - height: 14px; -`; - const StyledNewTabIcon = styled(NewTabIcon)` margin-bottom: 2px; path { @@ -84,73 +63,95 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string const localRounds: ClassicRound[] = getLocalRounds(votingHistory?.dispute?.disputeKitDispute) as ClassicRound[]; const rounds = votingHistory?.dispute?.rounds; const theme = useTheme(); - const txnExplorerLink = useMemo(() => { + const txnDisputeCreatedLink = useMemo(() => { return getTxnExplorerLink(votingHistory?.dispute?.transactionHash ?? ""); }, [votingHistory]); + const txnEnforcementLink = useMemo(() => { + return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? ""); + }, [disputeDetails]); return useMemo(() => { const dispute = disputeDetails?.dispute; - if (dispute) { - const rulingOverride = dispute.overridden; - const currentPeriodIndex = Periods[dispute.period]; - - return localRounds?.reduce( - (acc, { winningChoice }, index) => { - const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3; - const roundTimeline = rounds?.[index].timeline; - - const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : ""; - const answers = disputeData?.answers; - acc.push({ - title: `Jury Decision - Round ${index + 1}`, - party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers), - subtitle: isOngoing - ? "" - : `${formatDate(roundTimeline?.[Periods.vote])} / ${ - votingHistory?.dispute?.rounds.at(index)?.court.name - }`, - rightSided: true, - variant: theme.secondaryPurple, - Icon: icon !== "" ? icon : undefined, - }); - - if (index < localRounds.length - 1) { - acc.push({ - title: "Appealed", - party: "", - subtitle: formatDate(roundTimeline?.[Periods.appeal]), - rightSided: true, - Icon: StyledClosedCircle, - }); - } else if (rulingOverride && dispute.currentRuling !== winningChoice) { - acc.push({ - title: "Won by Appeal", - party: getVoteChoice(dispute.currentRuling, answers), - subtitle: formatDate(roundTimeline?.[Periods.appeal]), - rightSided: true, - Icon: ClosedCaseIcon, - }); - } - - return acc; - }, - [ - { - title: "Dispute created", - party: ( - - - - ), - subtitle: formatDate(votingHistory?.dispute?.createdAt), - rightSided: true, - variant: theme.secondaryPurple, - }, - ] - ); + if (!dispute) return; + + const rulingOverride = dispute.overridden; + const currentPeriodIndex = Periods[dispute.period]; + + const base: TimelineItems = [ + { + title: "Dispute created", + party: ( + + + + ), + subtitle: formatDate(votingHistory?.dispute?.createdAt), + rightSided: true, + variant: theme.secondaryPurple, + }, + ]; + + const items = localRounds?.reduce<_TimelineItem1[]>((acc, { winningChoice }, index) => { + const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3; + const roundTimeline = rounds?.[index].timeline; + const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : undefined; + const answers = disputeData?.answers; + + acc.push({ + title: `Jury Decision - Round ${index + 1}`, + party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers), + subtitle: isOngoing ? "" : `${formatDate(roundTimeline?.[Periods.vote])} / ${rounds?.[index]?.court.name}`, + rightSided: true, + variant: theme.secondaryPurple, + Icon: icon, + }); + + if (index < localRounds.length - 1) { + acc.push({ + title: "Appealed", + party: "", + subtitle: formatDate(roundTimeline?.[Periods.appeal]), + rightSided: true, + Icon: StyledClosedCircle, + }); + } else if (rulingOverride && dispute.currentRuling !== winningChoice) { + acc.push({ + title: "Won by Appeal", + party: getVoteChoice(dispute.currentRuling, answers), + subtitle: formatDate(roundTimeline?.[Periods.appeal]), + rightSided: true, + Icon: ClosedCaseIcon, + }); + } + + return acc; + }, []); + + if (dispute.ruled) { + items.push({ + title: "Enforcement", + party: ( + + + + ), + subtitle: `${formatDate(dispute.rulingTimestamp)} / ${rounds?.at(-1)?.court.name}`, + rightSided: true, + Icon: GavelExecutedIcon, + }); } - return; - }, [disputeDetails, disputeData, localRounds, theme, rounds, votingHistory, txnExplorerLink]); + + return [...base, ...items] as TimelineItems; + }, [ + disputeDetails, + disputeData, + localRounds, + theme, + rounds, + votingHistory, + txnDisputeCreatedLink, + txnEnforcementLink, + ]); }; interface IDisputeTimeline { @@ -160,33 +161,8 @@ interface IDisputeTimeline { const DisputeTimeline: React.FC = ({ arbitrable }) => { const { id } = useParams(); const { data: disputeDetails } = useDisputeDetailsQuery(id); - const { data: votingHistory } = useVotingHistory(id); const items = useItems(disputeDetails, arbitrable); - const transactionExplorerLink = useMemo(() => { - return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? ""); - }, [disputeDetails]); - - return ( - - {items && } - {disputeDetails?.dispute?.ruled && ( - - - - Enforcement:{" "} - {disputeDetails.dispute.rulingTimestamp ? ( - - {formatDate(disputeDetails.dispute.rulingTimestamp)} - - ) : ( - - )}{" "} - / {votingHistory?.dispute?.rounds.at(-1)?.court.name} - - - )} - - ); + return {items && }; }; export default DisputeTimeline; diff --git a/web/src/components/Verdict/FinalDecision.tsx b/web/src/components/Verdict/FinalDecision.tsx index d58a216ce..7c1bb4864 100644 --- a/web/src/components/Verdict/FinalDecision.tsx +++ b/web/src/components/Verdict/FinalDecision.tsx @@ -12,10 +12,9 @@ import { REFETCH_INTERVAL } from "consts/index"; import { Periods } from "consts/periods"; import { useReadKlerosCoreCurrentRuling } from "hooks/contracts/generated"; import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData"; -import { useVotingHistory } from "hooks/queries/useVotingHistory"; +import { VotingHistoryQuery } from "hooks/queries/useVotingHistory"; import { useVotingContext } from "hooks/useVotingContext"; import { getLocalRounds } from "utils/getLocalRounds"; -import { isUndefined } from "utils/index"; import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; @@ -25,7 +24,6 @@ import { Divider } from "../Divider"; import { StyledArrowLink } from "../StyledArrowLink"; import AnswerDisplay from "./Answer"; -import RulingAndRewardsIndicators from "./RulingAndRewardsIndicators"; const Container = styled.div` width: 100%; @@ -61,11 +59,11 @@ const JuryDecisionTag = styled.small` `; const StyledDivider = styled(Divider)` - margin: 16px 0px; + margin: 16px 0 0; ${landscapeStyle( () => css` - margin: 24px 0px; + margin: 24px 0 0; ` )} `; @@ -81,15 +79,15 @@ const ReStyledArrowLink = styled(StyledArrowLink)` interface IFinalDecision { arbitrable?: `0x${string}`; + votingHistory: VotingHistoryQuery | undefined; } -const FinalDecision: React.FC = ({ arbitrable }) => { +const FinalDecision: React.FC = ({ arbitrable, votingHistory }) => { const { id } = useParams(); const { isDisconnected } = useAccount(); const { data: populatedDisputeData } = usePopulatedDisputeData(id, arbitrable); const { data: disputeDetails } = useDisputeDetailsQuery(id); const { wasDrawn, hasVoted, isLoading, isCommitPeriod, isVotingPeriod, commited, isHiddenVotes } = useVotingContext(); - const { data: votingHistory } = useVotingHistory(id); const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute); const ruled = disputeDetails?.dispute?.ruled ?? false; const periodIndex = Periods[disputeDetails?.dispute?.period ?? "evidence"]; @@ -101,25 +99,17 @@ const FinalDecision: React.FC = ({ arbitrable }) => { const currentRuling = Number(currentRulingArray?.[0] ?? 0); const answer = populatedDisputeData?.answers?.find((answer) => BigInt(answer.id) === BigInt(currentRuling)); - const rounds = votingHistory?.dispute?.rounds; - const jurorRewardsDispersed = useMemo(() => Boolean(rounds?.every((round) => round.jurorRewardsDispersed)), [rounds]); const buttonText = useMemo(() => { - if (!wasDrawn || isDisconnected) return "Check how the jury voted"; + if (!wasDrawn || isDisconnected) return "Check votes"; if (isCommitPeriod && !commited) return "Commit your vote"; if (isVotingPeriod && isHiddenVotes && commited && !hasVoted) return "Reveal your vote"; if (isVotingPeriod && !isHiddenVotes && !hasVoted) return "Cast your vote"; - return "Check how the jury voted"; + return "Check votes"; }, [wasDrawn, hasVoted, isCommitPeriod, isVotingPeriod, commited, isHiddenVotes, isDisconnected]); return ( - {!isUndefined(Boolean(disputeDetails?.dispute?.ruled)) || jurorRewardsDispersed ? ( - - ) : null} {ruled && ( The jury decided in favor of: @@ -140,15 +130,15 @@ const FinalDecision: React.FC = ({ arbitrable }) => { )} )} + {isLoading && !isDisconnected ? ( + + ) : ( + + {buttonText} + + )} - {isLoading && !isDisconnected ? ( - - ) : ( - - {buttonText} - - )} ); }; diff --git a/web/src/components/Verdict/index.tsx b/web/src/components/Verdict/index.tsx index b3ee84d7e..09c179f81 100644 --- a/web/src/components/Verdict/index.tsx +++ b/web/src/components/Verdict/index.tsx @@ -3,6 +3,8 @@ import styled from "styled-components"; import { responsiveSize } from "styles/responsiveSize"; +import { VotingHistoryQuery } from "src/graphql/graphql"; + import DisputeTimeline from "./DisputeTimeline"; import FinalDecision from "./FinalDecision"; @@ -14,13 +16,14 @@ const Container = styled.div` interface IVerdict { arbitrable?: `0x${string}`; + votingHistory: VotingHistoryQuery | undefined; } -const Verdict: React.FC = ({ arbitrable }) => { +const Verdict: React.FC = ({ arbitrable, votingHistory }) => { return ( - - + + ); }; diff --git a/web/src/pages/Cases/CaseDetails/Overview/index.tsx b/web/src/pages/Cases/CaseDetails/Overview/index.tsx index 32e9cd0f6..0180342c0 100644 --- a/web/src/pages/Cases/CaseDetails/Overview/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Overview/index.tsx @@ -56,10 +56,10 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex return ( <> - + - +