Skip to content

Commit 47aaca4

Browse files
committedJun 10, 2025··
feat: new overview design
1 parent 4adedb5 commit 47aaca4

File tree

6 files changed

+152
-144
lines changed

6 files changed

+152
-144
lines changed
 
Lines changed: 11 additions & 0 deletions
Loading

‎web/src/components/DisputePreview/DisputeContext.tsx‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import styled from "styled-components";
33

44
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
@@ -9,21 +9,30 @@ import { isUndefined } from "utils/index";
99

1010
import { responsiveSize } from "styles/responsiveSize";
1111

12+
import { DisputeDetailsQuery, VotingHistoryQuery } from "src/graphql/graphql";
13+
1214
import ReactMarkdown from "components/ReactMarkdown";
1315
import { StyledSkeleton } from "components/StyledSkeleton";
1416

1517
import { Divider } from "../Divider";
1618
import { ExternalLink } from "../ExternalLink";
1719

1820
import AliasDisplay from "./Alias";
21+
import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators";
1922

2023
const StyledH1 = styled.h1`
2124
margin: 0;
2225
word-wrap: break-word;
23-
font-size: ${responsiveSize(18, 24)};
26+
font-size: ${responsiveSize(20, 26)};
2427
line-height: 24px;
2528
`;
2629

30+
const TitleSection = styled.div`
31+
display: flex;
32+
flex-direction: column;
33+
gap: 12px;
34+
`;
35+
2736
const ReactMarkdownWrapper = styled.div`
2837
& p:first-of-type {
2938
margin: 0;
@@ -68,17 +77,36 @@ const AliasesContainer = styled.div`
6877

6978
interface IDisputeContext {
7079
disputeDetails?: DisputeDetails;
80+
dispute: DisputeDetailsQuery | undefined;
7181
isRpcError?: boolean;
82+
votingHistory: VotingHistoryQuery | undefined;
7283
}
7384

74-
export const DisputeContext: React.FC<IDisputeContext> = ({ disputeDetails, isRpcError = false }) => {
85+
export const DisputeContext: React.FC<IDisputeContext> = ({
86+
disputeDetails,
87+
dispute,
88+
isRpcError = false,
89+
votingHistory,
90+
}) => {
7591
const errMsg = isRpcError ? RPC_ERROR : INVALID_DISPUTE_DATA_ERROR;
92+
const rounds = votingHistory?.dispute?.rounds;
93+
const jurorRewardsDispersed = useMemo(() => Boolean(rounds?.every((round) => round.jurorRewardsDispersed)), [rounds]);
94+
console.log({ jurorRewardsDispersed }, disputeDetails);
7695

7796
return (
7897
<>
79-
<StyledH1 dir="auto">
80-
{isUndefined(disputeDetails) ? <StyledSkeleton /> : (disputeDetails?.title ?? errMsg)}
81-
</StyledH1>
98+
<TitleSection>
99+
<StyledH1 dir="auto">
100+
{isUndefined(disputeDetails) ? <StyledSkeleton /> : (disputeDetails?.title ?? errMsg)}
101+
</StyledH1>
102+
{!isUndefined(Boolean(dispute?.dispute?.ruled)) || jurorRewardsDispersed ? (
103+
<RulingAndRewardsIndicators
104+
ruled={Boolean(dispute?.dispute?.ruled)}
105+
jurorRewardsDispersed={jurorRewardsDispersed}
106+
/>
107+
) : null}
108+
<Divider />
109+
</TitleSection>
82110
{disputeDetails?.question?.trim() || disputeDetails?.description?.trim() ? (
83111
<div>
84112
{disputeDetails?.question?.trim() ? (
Lines changed: 85 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React, { useMemo } from "react";
22
import styled, { useTheme } from "styled-components";
33

4-
import Skeleton from "react-loading-skeleton";
54
import { useParams } from "react-router-dom";
65

76
import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library";
87

9-
import CalendarIcon from "svgs/icons/calendar.svg";
108
import ClosedCaseIcon from "svgs/icons/check-circle-outline.svg";
119
import NewTabIcon from "svgs/icons/new-tab.svg";
10+
import GavelExecutedIcon from "svgs/icons/gavel-executed.svg";
1211

1312
import { Periods } from "consts/periods";
1413
import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData";
@@ -21,8 +20,6 @@ import { useVotingHistory } from "queries/useVotingHistory";
2120
import { ClassicRound } from "src/graphql/graphql";
2221
import { getTxnExplorerLink } from "src/utils";
2322

24-
import { responsiveSize } from "styles/responsiveSize";
25-
2623
import { StyledClosedCircle } from "components/StyledIcons/ClosedCircleIcon";
2724

2825
import { ExternalLink } from "../ExternalLink";
@@ -37,24 +34,6 @@ const StyledTimeline = styled(CustomTimeline)`
3734
width: 100%;
3835
`;
3936

40-
const EnforcementContainer = styled.div`
41-
display: flex;
42-
gap: 8px;
43-
margin-top: ${responsiveSize(12, 24)};
44-
fill: ${({ theme }) => theme.secondaryText};
45-
46-
small {
47-
font-weight: 400;
48-
line-height: 19px;
49-
color: ${({ theme }) => theme.secondaryText};
50-
}
51-
`;
52-
53-
const StyledCalendarIcon = styled(CalendarIcon)`
54-
width: 14px;
55-
height: 14px;
56-
`;
57-
5837
const StyledNewTabIcon = styled(NewTabIcon)`
5938
margin-bottom: 2px;
6039
path {
@@ -84,73 +63,95 @@ const useItems = (disputeDetails?: DisputeDetailsQuery, arbitrable?: `0x${string
8463
const localRounds: ClassicRound[] = getLocalRounds(votingHistory?.dispute?.disputeKitDispute) as ClassicRound[];
8564
const rounds = votingHistory?.dispute?.rounds;
8665
const theme = useTheme();
87-
const txnExplorerLink = useMemo(() => {
66+
const txnDisputeCreatedLink = useMemo(() => {
8867
return getTxnExplorerLink(votingHistory?.dispute?.transactionHash ?? "");
8968
}, [votingHistory]);
69+
const txnEnforcementLink = useMemo(() => {
70+
return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? "");
71+
}, [disputeDetails]);
9072

9173
return useMemo<TimelineItems | undefined>(() => {
9274
const dispute = disputeDetails?.dispute;
93-
if (dispute) {
94-
const rulingOverride = dispute.overridden;
95-
const currentPeriodIndex = Periods[dispute.period];
96-
97-
return localRounds?.reduce<TimelineItems>(
98-
(acc, { winningChoice }, index) => {
99-
const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3;
100-
const roundTimeline = rounds?.[index].timeline;
101-
102-
const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : "";
103-
const answers = disputeData?.answers;
104-
acc.push({
105-
title: `Jury Decision - Round ${index + 1}`,
106-
party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers),
107-
subtitle: isOngoing
108-
? ""
109-
: `${formatDate(roundTimeline?.[Periods.vote])} / ${
110-
votingHistory?.dispute?.rounds.at(index)?.court.name
111-
}`,
112-
rightSided: true,
113-
variant: theme.secondaryPurple,
114-
Icon: icon !== "" ? icon : undefined,
115-
});
116-
117-
if (index < localRounds.length - 1) {
118-
acc.push({
119-
title: "Appealed",
120-
party: "",
121-
subtitle: formatDate(roundTimeline?.[Periods.appeal]),
122-
rightSided: true,
123-
Icon: StyledClosedCircle,
124-
});
125-
} else if (rulingOverride && dispute.currentRuling !== winningChoice) {
126-
acc.push({
127-
title: "Won by Appeal",
128-
party: getVoteChoice(dispute.currentRuling, answers),
129-
subtitle: formatDate(roundTimeline?.[Periods.appeal]),
130-
rightSided: true,
131-
Icon: ClosedCaseIcon,
132-
});
133-
}
134-
135-
return acc;
136-
},
137-
[
138-
{
139-
title: "Dispute created",
140-
party: (
141-
<ExternalLink to={txnExplorerLink} rel="noopener noreferrer" target="_blank">
142-
<StyledNewTabIcon />
143-
</ExternalLink>
144-
),
145-
subtitle: formatDate(votingHistory?.dispute?.createdAt),
146-
rightSided: true,
147-
variant: theme.secondaryPurple,
148-
},
149-
]
150-
);
75+
if (!dispute) return;
76+
77+
const rulingOverride = dispute.overridden;
78+
const currentPeriodIndex = Periods[dispute.period];
79+
80+
const base: TimelineItems = [
81+
{
82+
title: "Dispute created",
83+
party: (
84+
<ExternalLink to={txnDisputeCreatedLink} rel="noopener noreferrer" target="_blank">
85+
<StyledNewTabIcon />
86+
</ExternalLink>
87+
),
88+
subtitle: formatDate(votingHistory?.dispute?.createdAt),
89+
rightSided: true,
90+
variant: theme.secondaryPurple,
91+
},
92+
];
93+
94+
const items = localRounds?.reduce<_TimelineItem1[]>((acc, { winningChoice }, index) => {
95+
const isOngoing = index === localRounds.length - 1 && currentPeriodIndex < 3;
96+
const roundTimeline = rounds?.[index].timeline;
97+
const icon = dispute.ruled && !rulingOverride && index === localRounds.length - 1 ? ClosedCaseIcon : undefined;
98+
const answers = disputeData?.answers;
99+
100+
acc.push({
101+
title: `Jury Decision - Round ${index + 1}`,
102+
party: isOngoing ? "Voting is ongoing" : getVoteChoice(winningChoice, answers),
103+
subtitle: isOngoing ? "" : `${formatDate(roundTimeline?.[Periods.vote])} / ${rounds?.[index]?.court.name}`,
104+
rightSided: true,
105+
variant: theme.secondaryPurple,
106+
Icon: icon,
107+
});
108+
109+
if (index < localRounds.length - 1) {
110+
acc.push({
111+
title: "Appealed",
112+
party: "",
113+
subtitle: formatDate(roundTimeline?.[Periods.appeal]),
114+
rightSided: true,
115+
Icon: StyledClosedCircle,
116+
});
117+
} else if (rulingOverride && dispute.currentRuling !== winningChoice) {
118+
acc.push({
119+
title: "Won by Appeal",
120+
party: getVoteChoice(dispute.currentRuling, answers),
121+
subtitle: formatDate(roundTimeline?.[Periods.appeal]),
122+
rightSided: true,
123+
Icon: ClosedCaseIcon,
124+
});
125+
}
126+
127+
return acc;
128+
}, []);
129+
130+
if (dispute.ruled) {
131+
items.push({
132+
title: "Enforcement",
133+
party: (
134+
<ExternalLink to={txnEnforcementLink} rel="noopener noreferrer" target="_blank">
135+
<StyledNewTabIcon />
136+
</ExternalLink>
137+
),
138+
subtitle: `${formatDate(dispute.rulingTimestamp)} / ${rounds?.at(-1)?.court.name}`,
139+
rightSided: true,
140+
Icon: GavelExecutedIcon,
141+
});
151142
}
152-
return;
153-
}, [disputeDetails, disputeData, localRounds, theme, rounds, votingHistory, txnExplorerLink]);
143+
144+
return [...base, ...items] as TimelineItems;
145+
}, [
146+
disputeDetails,
147+
disputeData,
148+
localRounds,
149+
theme,
150+
rounds,
151+
votingHistory,
152+
txnDisputeCreatedLink,
153+
txnEnforcementLink,
154+
]);
154155
};
155156

156157
interface IDisputeTimeline {
@@ -160,33 +161,8 @@ interface IDisputeTimeline {
160161
const DisputeTimeline: React.FC<IDisputeTimeline> = ({ arbitrable }) => {
161162
const { id } = useParams();
162163
const { data: disputeDetails } = useDisputeDetailsQuery(id);
163-
const { data: votingHistory } = useVotingHistory(id);
164164
const items = useItems(disputeDetails, arbitrable);
165165

166-
const transactionExplorerLink = useMemo(() => {
167-
return getTxnExplorerLink(disputeDetails?.dispute?.rulingTransactionHash ?? "");
168-
}, [disputeDetails]);
169-
170-
return (
171-
<Container>
172-
{items && <StyledTimeline {...{ items }} />}
173-
{disputeDetails?.dispute?.ruled && (
174-
<EnforcementContainer>
175-
<StyledCalendarIcon />
176-
<small>
177-
Enforcement:{" "}
178-
{disputeDetails.dispute.rulingTimestamp ? (
179-
<ExternalLink to={transactionExplorerLink} rel="noopener noreferrer" target="_blank">
180-
{formatDate(disputeDetails.dispute.rulingTimestamp)}
181-
</ExternalLink>
182-
) : (
183-
<Skeleton height={16} width={56} />
184-
)}{" "}
185-
/ {votingHistory?.dispute?.rounds.at(-1)?.court.name}
186-
</small>
187-
</EnforcementContainer>
188-
)}
189-
</Container>
190-
);
166+
return <Container>{items && <StyledTimeline {...{ items }} />}</Container>;
191167
};
192168
export default DisputeTimeline;

‎web/src/components/Verdict/FinalDecision.tsx‎

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import { REFETCH_INTERVAL } from "consts/index";
1212
import { Periods } from "consts/periods";
1313
import { useReadKlerosCoreCurrentRuling } from "hooks/contracts/generated";
1414
import { usePopulatedDisputeData } from "hooks/queries/usePopulatedDisputeData";
15-
import { useVotingHistory } from "hooks/queries/useVotingHistory";
15+
import { VotingHistoryQuery } from "hooks/queries/useVotingHistory";
1616
import { useVotingContext } from "hooks/useVotingContext";
1717
import { getLocalRounds } from "utils/getLocalRounds";
18-
import { isUndefined } from "utils/index";
1918

2019
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
2120

@@ -25,7 +24,6 @@ import { Divider } from "../Divider";
2524
import { StyledArrowLink } from "../StyledArrowLink";
2625

2726
import AnswerDisplay from "./Answer";
28-
import RulingAndRewardsIndicators from "./RulingAndRewardsIndicators";
2927

3028
const Container = styled.div`
3129
width: 100%;
@@ -61,11 +59,11 @@ const JuryDecisionTag = styled.small`
6159
`;
6260

6361
const StyledDivider = styled(Divider)`
64-
margin: 16px 0px;
62+
margin: 16px 0 0;
6563
6664
${landscapeStyle(
6765
() => css`
68-
margin: 24px 0px;
66+
margin: 24px 0 0;
6967
`
7068
)}
7169
`;
@@ -81,15 +79,15 @@ const ReStyledArrowLink = styled(StyledArrowLink)`
8179

8280
interface IFinalDecision {
8381
arbitrable?: `0x${string}`;
82+
votingHistory: VotingHistoryQuery | undefined;
8483
}
8584

86-
const FinalDecision: React.FC<IFinalDecision> = ({ arbitrable }) => {
85+
const FinalDecision: React.FC<IFinalDecision> = ({ arbitrable, votingHistory }) => {
8786
const { id } = useParams();
8887
const { isDisconnected } = useAccount();
8988
const { data: populatedDisputeData } = usePopulatedDisputeData(id, arbitrable);
9089
const { data: disputeDetails } = useDisputeDetailsQuery(id);
9190
const { wasDrawn, hasVoted, isLoading, isCommitPeriod, isVotingPeriod, commited, isHiddenVotes } = useVotingContext();
92-
const { data: votingHistory } = useVotingHistory(id);
9391
const localRounds = getLocalRounds(votingHistory?.dispute?.disputeKitDispute);
9492
const ruled = disputeDetails?.dispute?.ruled ?? false;
9593
const periodIndex = Periods[disputeDetails?.dispute?.period ?? "evidence"];
@@ -101,25 +99,17 @@ const FinalDecision: React.FC<IFinalDecision> = ({ arbitrable }) => {
10199
const currentRuling = Number(currentRulingArray?.[0] ?? 0);
102100

103101
const answer = populatedDisputeData?.answers?.find((answer) => BigInt(answer.id) === BigInt(currentRuling));
104-
const rounds = votingHistory?.dispute?.rounds;
105-
const jurorRewardsDispersed = useMemo(() => Boolean(rounds?.every((round) => round.jurorRewardsDispersed)), [rounds]);
106102
const buttonText = useMemo(() => {
107-
if (!wasDrawn || isDisconnected) return "Check how the jury voted";
103+
if (!wasDrawn || isDisconnected) return "Check votes";
108104
if (isCommitPeriod && !commited) return "Commit your vote";
109105
if (isVotingPeriod && isHiddenVotes && commited && !hasVoted) return "Reveal your vote";
110106
if (isVotingPeriod && !isHiddenVotes && !hasVoted) return "Cast your vote";
111-
return "Check how the jury voted";
107+
return "Check votes";
112108
}, [wasDrawn, hasVoted, isCommitPeriod, isVotingPeriod, commited, isHiddenVotes, isDisconnected]);
113109

114110
return (
115111
<Container>
116112
<VerdictContainer>
117-
{!isUndefined(Boolean(disputeDetails?.dispute?.ruled)) || jurorRewardsDispersed ? (
118-
<RulingAndRewardsIndicators
119-
ruled={Boolean(disputeDetails?.dispute?.ruled)}
120-
jurorRewardsDispersed={jurorRewardsDispersed}
121-
/>
122-
) : null}
123113
{ruled && (
124114
<JuryContainer>
125115
<JuryDecisionTag>The jury decided in favor of:</JuryDecisionTag>
@@ -140,15 +130,15 @@ const FinalDecision: React.FC<IFinalDecision> = ({ arbitrable }) => {
140130
)}
141131
</JuryContainer>
142132
)}
133+
{isLoading && !isDisconnected ? (
134+
<Skeleton width={250} height={20} />
135+
) : (
136+
<ReStyledArrowLink to={`/cases/${id?.toString()}/voting`}>
137+
{buttonText} <ArrowIcon />
138+
</ReStyledArrowLink>
139+
)}
143140
</VerdictContainer>
144141
<StyledDivider />
145-
{isLoading && !isDisconnected ? (
146-
<Skeleton width={250} height={20} />
147-
) : (
148-
<ReStyledArrowLink to={`/cases/${id?.toString()}/voting`}>
149-
{buttonText} <ArrowIcon />
150-
</ReStyledArrowLink>
151-
)}
152142
</Container>
153143
);
154144
};

‎web/src/components/Verdict/index.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import styled from "styled-components";
33

44
import { responsiveSize } from "styles/responsiveSize";
55

6+
import { VotingHistoryQuery } from "src/graphql/graphql";
7+
68
import DisputeTimeline from "./DisputeTimeline";
79
import FinalDecision from "./FinalDecision";
810

@@ -14,13 +16,14 @@ const Container = styled.div`
1416

1517
interface IVerdict {
1618
arbitrable?: `0x${string}`;
19+
votingHistory: VotingHistoryQuery | undefined;
1720
}
1821

19-
const Verdict: React.FC<IVerdict> = ({ arbitrable }) => {
22+
const Verdict: React.FC<IVerdict> = ({ arbitrable, votingHistory }) => {
2023
return (
2124
<Container>
22-
<FinalDecision arbitrable={arbitrable} />
23-
<DisputeTimeline arbitrable={arbitrable} />
25+
<FinalDecision {...{ votingHistory, arbitrable }} />
26+
<DisputeTimeline {...{ arbitrable }} />
2427
</Container>
2528
);
2629
};

‎web/src/pages/Cases/CaseDetails/Overview/index.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ const Overview: React.FC<IOverview> = ({ arbitrable, courtID, currentPeriodIndex
5656
return (
5757
<>
5858
<Container>
59-
<DisputeContext disputeDetails={disputeDetails} isRpcError={isError} />
59+
<DisputeContext isRpcError={isError} {...{ votingHistory, disputeDetails, dispute }} />
6060
<Divider />
6161

62-
<Verdict arbitrable={arbitrable} />
62+
<Verdict {...{ arbitrable, votingHistory }} />
6363
<Divider />
6464

6565
<DisputeInfo

0 commit comments

Comments
 (0)
Please sign in to comment.