Skip to content

Commit 9b17ff5

Browse files
authored
Merge pull request #1022 from kleros/feat(web)/case-verdict-component
Feat(web)/case verdict component
2 parents b779323 + df89ed6 commit 9b17ff5

File tree

12 files changed

+208
-123
lines changed

12 files changed

+208
-123
lines changed

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"dependencies": {
6464
"@filebase/client": "^0.0.4",
6565
"@kleros/kleros-v2-contracts": "workspace:^",
66-
"@kleros/ui-components-library": "^2.5.2",
66+
"@kleros/ui-components-library": "^2.6.1",
6767
"@sentry/react": "^7.55.2",
6868
"@sentry/tracing": "^7.55.2",
6969
"@types/react-modal": "^3.16.0",
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React, { useMemo } from "react";
2+
import { useParams } from "react-router-dom";
3+
import styled, { useTheme } from "styled-components";
4+
import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library";
5+
import { Periods } from "consts/periods";
6+
import { useVotingHistory } from "queries/useVotingHistory";
7+
import { useDisputeTemplate } from "queries/useDisputeTemplate";
8+
import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
9+
import ClosedCaseIcon from "assets/svgs/icons/check-circle-outline.svg";
10+
import AppealedCaseIcon from "assets/svgs/icons/close-circle.svg";
11+
import CalendarIcon from "assets/svgs/icons/calendar.svg";
12+
13+
const Container = styled.div`
14+
display: flex;
15+
position: relative;
16+
margin-left: 8px;
17+
`;
18+
19+
const StyledTimeline = styled(CustomTimeline)`
20+
width: 100%;
21+
margin-bottom: 32px;
22+
`;
23+
24+
const EnforcementContainer = styled.div`
25+
position: absolute;
26+
bottom: 0;
27+
display: flex;
28+
gap: 8px;
29+
margin-bottom: 8px;
30+
fill: ${({ theme }) => theme.secondaryText};
31+
32+
small {
33+
font-weight: 400;
34+
line-height: 19px;
35+
color: ${({ theme }) => theme.secondaryText};
36+
}
37+
`;
38+
39+
const StyledCalendarIcon = styled(CalendarIcon)`
40+
width: 14px;
41+
height: 14px;
42+
`;
43+
44+
const getCaseEventTimes = (
45+
lastPeriodChange: string,
46+
currentPeriodIndex: number,
47+
timesPerPeriod: string[],
48+
isCreation: boolean
49+
) => {
50+
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
51+
const durationCurrentPeriod = parseInt(timesPerPeriod[currentPeriodIndex - 1]);
52+
const startingDate = new Date(
53+
(parseInt(lastPeriodChange) + (isCreation ? -durationCurrentPeriod : durationCurrentPeriod)) * 1000
54+
);
55+
56+
const formattedDate = startingDate.toLocaleDateString("en-US", options);
57+
return formattedDate;
58+
};
59+
60+
type TimelineItems = [_TimelineItem1, ..._TimelineItem1[]];
61+
62+
const useItems = (disputeDetails?: DisputeDetailsQuery) => {
63+
const { data: disputeTemplate } = useDisputeTemplate();
64+
const { id } = useParams();
65+
const { data: votingHistory } = useVotingHistory(id);
66+
const localRounds = votingHistory?.dispute?.disputeKitDispute?.localRounds;
67+
const theme = useTheme();
68+
69+
return useMemo<TimelineItems | undefined>(() => {
70+
const dispute = disputeDetails?.dispute;
71+
if (dispute) {
72+
const currentPeriodIndex = Periods[dispute.period];
73+
const lastPeriodChange = dispute.lastPeriodChange;
74+
const courtTimePeriods = dispute.court.timesPerPeriod;
75+
return localRounds?.reduce<TimelineItems>(
76+
(acc, { winningChoice }, index) => {
77+
const parsedWinningChoice = parseInt(winningChoice);
78+
const eventDate = getCaseEventTimes(lastPeriodChange, currentPeriodIndex, courtTimePeriods, false);
79+
const icon = disputeDetails?.dispute?.ruled && index === localRounds.length - 1 ? ClosedCaseIcon : "";
80+
81+
acc.push({
82+
title: `Jury Decision - Round ${index + 1}`,
83+
party:
84+
parsedWinningChoice !== 0
85+
? disputeTemplate?.answers?.[parseInt(winningChoice) - 1].title
86+
: "Refuse to Arbitrate",
87+
subtitle: eventDate,
88+
rightSided: true,
89+
variant: theme.secondaryPurple,
90+
Icon: icon !== "" ? icon : undefined,
91+
});
92+
93+
if (index < localRounds.length - 1) {
94+
acc.push({
95+
title: "Appealed",
96+
party: "",
97+
subtitle: eventDate,
98+
rightSided: true,
99+
Icon: AppealedCaseIcon,
100+
});
101+
}
102+
103+
return acc;
104+
},
105+
[
106+
{
107+
title: "Dispute created",
108+
party: "",
109+
subtitle: getCaseEventTimes(lastPeriodChange, currentPeriodIndex, courtTimePeriods, true),
110+
rightSided: true,
111+
variant: theme.secondaryPurple,
112+
},
113+
]
114+
);
115+
}
116+
return;
117+
}, [disputeDetails, disputeTemplate, localRounds, theme]);
118+
};
119+
120+
const DisputeTimeline: React.FC = () => {
121+
const { id } = useParams();
122+
const { data: disputeDetails } = useDisputeDetailsQuery(id);
123+
const items = useItems(disputeDetails);
124+
125+
return (
126+
<Container>
127+
{items && <StyledTimeline {...{ items }} />}
128+
{disputeDetails?.dispute?.ruled && items && (
129+
<EnforcementContainer>
130+
<StyledCalendarIcon />
131+
<small>Enforcement: {items.at(-1)?.subtitle}</small>
132+
</EnforcementContainer>
133+
)}
134+
</Container>
135+
);
136+
};
137+
export default DisputeTimeline;
Lines changed: 48 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,22 @@
11
import React from "react";
2+
import { useNavigate, useParams } from "react-router-dom";
23
import styled from "styled-components";
34
import Identicon from "react-identicons";
4-
import { useNavigate } from "react-router-dom";
55
import ArrowIcon from "assets/svgs/icons/arrow.svg";
6+
import { useDisputeTemplate } from "queries/useDisputeTemplate";
7+
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
8+
import { useKlerosCoreCurrentRuling } from "hooks/contracts/generated";
69
import LightButton from "../LightButton";
710
import VerdictBanner from "./VerdictBanner";
8-
import { useKlerosCoreCurrentRuling } from "hooks/contracts/generated";
911

1012
const Container = styled.div`
11-
position: relative;
12-
width: calc(200px + (360 - 200) * (100vw - 375px) / (1250 - 375));
13-
14-
height: 400px;
15-
margin-left: 16px;
16-
.reverse-button {
17-
display: flex;
18-
flex-direction: row-reverse;
19-
gap: 8px;
20-
.button-text {
21-
color: ${({ theme }) => theme.primaryBlue};
22-
}
23-
}
13+
width: 100%;
2414
`;
2515

26-
const JuryContanier = styled.div`
16+
const JuryContainer = styled.div`
2717
display: flex;
2818
flex-direction: column;
2919
gap: 8px;
30-
margin-top: 32px;
3120
h3 {
3221
line-height: 21px;
3322
}
@@ -40,7 +29,7 @@ const JuryDecisionTag = styled.small`
4029
`;
4130

4231
const Divider = styled.hr`
43-
color: ${({ theme }) => theme.secondaryText};
32+
color: ${({ theme }) => theme.stroke};
4433
`;
4534

4635
const UserContainer = styled.div`
@@ -66,71 +55,72 @@ const StyledIdenticon = styled(Identicon)`
6655
`;
6756

6857
const Header = styled.h1`
69-
margin: 20px 0px 48px;
58+
margin: 20px 0px 32px 0px;
7059
`;
7160

7261
const Title = styled.small`
7362
color: ${({ theme }) => theme.secondaryText};
7463
`;
7564

7665
const StyledButton = styled(LightButton)`
77-
position: absolute;
78-
bottom: 0;
66+
display: flex;
67+
flex-direction: row-reverse;
68+
gap: 8px;
69+
> .button-text {
70+
color: ${({ theme }) => theme.primaryBlue};
71+
}
7972
`;
8073

81-
interface IDecisionText {
82-
ruled: boolean;
83-
}
84-
85-
const DecisionText: React.FC<IDecisionText> = ({ ruled }) => {
86-
return ruled ? <>Final Decision</> : <>Current Ruling</>;
87-
};
88-
89-
interface IFinalDecision {
90-
id: string;
91-
disputeTemplate: any;
92-
ruled: boolean;
93-
}
74+
const AnswerTitle = styled.h3`
75+
margin: 0;
76+
`;
9477

95-
const FinalDecision: React.FC<IFinalDecision> = ({ id, disputeTemplate, ruled }) => {
78+
const FinalDecision: React.FC = () => {
79+
const { id } = useParams();
80+
const { data: disputeTemplate } = useDisputeTemplate(id);
81+
const { data: disputeDetails } = useDisputeDetailsQuery(id);
82+
const ruled = disputeDetails?.dispute?.ruled ?? false;
9683
const navigate = useNavigate();
97-
const { data: currentRulingArray } = useKlerosCoreCurrentRuling({ args: [BigInt(id)], watch: true });
84+
const { data: currentRulingArray } = useKlerosCoreCurrentRuling({ args: [BigInt(id ?? 0)], watch: true });
9885
const currentRuling = Number(currentRulingArray?.[0]);
99-
console.log("🚀 ~ file: FinalDecision.tsx:90 ~ currentRuling:", currentRuling);
100-
console.log("disputeTemplate", disputeTemplate);
10186
const answer = disputeTemplate?.answers?.[currentRuling! - 1];
102-
console.log("🚀 ~ file: FinalDecision.tsx:92 ~ answer:", answer);
103-
104-
const handleClick = () => {
105-
navigate(`/cases/${id.toString()}/voting`);
106-
};
10787

10888
return (
10989
<Container>
11090
<VerdictBanner ruled={ruled} />
111-
<Header>
112-
<DecisionText ruled={ruled} />
113-
</Header>
114-
<JuryContanier>
91+
<Header> {ruled ? "Final Decision" : "Current Ruling"} </Header>
92+
<JuryContainer>
11593
<JuryDecisionTag>The jury decided in favor of:</JuryDecisionTag>
116-
{answer ? <h3>{`${answer.title}. ${answer.description}`}</h3> : <h3>Refuse to Arbitrate</h3>}
117-
</JuryContanier>
118-
<Divider />
119-
<UserContainer>
120-
<StyledIdenticon size="24" />
121-
<AliasTag>
122-
{disputeTemplate?.aliases?.challenger && <small>Alice.eth</small>}
123-
<Title>Claimant</Title>
124-
</AliasTag>
125-
</UserContainer>
94+
{answer ? (
95+
<div>
96+
<AnswerTitle>{answer.title}</AnswerTitle>
97+
<small>{answer.description}</small>
98+
</div>
99+
) : (
100+
<h3>Refuse to Arbitrate</h3>
101+
)}
102+
</JuryContainer>
126103
<Divider />
104+
{disputeTemplate?.aliases && (
105+
<>
106+
<UserContainer>
107+
<StyledIdenticon size="24" />
108+
<AliasTag>
109+
{disputeTemplate?.aliases?.challenger && <small>Alice.eth</small>}
110+
<Title>Claimant</Title>
111+
</AliasTag>
112+
</UserContainer>
113+
<Divider />
114+
</>
115+
)}
127116
<StyledButton
128-
onClick={handleClick}
117+
onClick={() => navigate(`/cases/${id?.toString()}/voting`)}
129118
text={"Check how the jury voted"}
130119
Icon={ArrowIcon}
131120
className="reverse-button"
132121
/>
133122
</Container>
134123
);
135124
};
125+
136126
export default FinalDecision;

web/src/components/Verdict/Timeline.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

web/src/components/Verdict/VerdictBanner.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ interface IVerdictBanner {
4444
}
4545

4646
const VerdictBanner: React.FC<IVerdictBanner> = ({ ruled }) => {
47-
console.log("ruledinside verdict banner", ruled);
4847
return (
4948
<BannerContainer>
5049
<VerdictIcon ruled={ruled} />

web/src/components/Verdict/index.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import React from "react";
22
import styled from "styled-components";
33
import FinalDecision from "./FinalDecision";
4-
import DisputeTimeline from "./Timeline";
4+
import DisputeTimeline from "./DisputeTimeline";
55

66
const Container = styled.div`
77
display: flex;
8-
gap: 48px;
8+
flex-wrap: wrap;
9+
gap: 24px;
910
`;
1011

11-
interface IVerdict {
12-
id: string;
13-
disputeTemplate: any;
14-
ruled: boolean;
15-
}
16-
17-
const Verdict: React.FC<IVerdict> = ({ id, disputeTemplate, ruled }) => {
12+
const Verdict: React.FC = () => {
1813
return (
1914
<Container>
20-
<FinalDecision id={id} disputeTemplate={disputeTemplate} ruled={ruled} />
21-
{/* <DisputeTimeline id={id} disputeTemplate={disputeTemplate} /> */}
15+
<FinalDecision />
16+
<DisputeTimeline />
2217
</Container>
2318
);
2419
};
20+
2521
export default Verdict;

0 commit comments

Comments
 (0)