diff --git a/web/package.json b/web/package.json
index 210cec246..d592d774b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -63,7 +63,7 @@
"dependencies": {
"@filebase/client": "^0.0.4",
"@kleros/kleros-v2-contracts": "workspace:^",
- "@kleros/ui-components-library": "^2.5.2",
+ "@kleros/ui-components-library": "^2.6.1",
"@sentry/react": "^7.55.2",
"@sentry/tracing": "^7.55.2",
"@types/react-modal": "^3.16.0",
diff --git a/web/src/assets/svgs/icons/close-circle.svg b/web/src/assets/svgs/icons/close-circle.svg
new file mode 100644
index 000000000..f3d4a2477
--- /dev/null
+++ b/web/src/assets/svgs/icons/close-circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/components/Verdict/DisputeTimeline.tsx b/web/src/components/Verdict/DisputeTimeline.tsx
new file mode 100644
index 000000000..439abd8e2
--- /dev/null
+++ b/web/src/components/Verdict/DisputeTimeline.tsx
@@ -0,0 +1,137 @@
+import React, { useMemo } from "react";
+import { useParams } from "react-router-dom";
+import styled, { useTheme } from "styled-components";
+import { _TimelineItem1, CustomTimeline } from "@kleros/ui-components-library";
+import { Periods } from "consts/periods";
+import { useVotingHistory } from "queries/useVotingHistory";
+import { useDisputeTemplate } from "queries/useDisputeTemplate";
+import { DisputeDetailsQuery, useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
+import ClosedCaseIcon from "assets/svgs/icons/check-circle-outline.svg";
+import AppealedCaseIcon from "assets/svgs/icons/close-circle.svg";
+import CalendarIcon from "assets/svgs/icons/calendar.svg";
+
+const Container = styled.div`
+ display: flex;
+ position: relative;
+ margin-left: 8px;
+`;
+
+const StyledTimeline = styled(CustomTimeline)`
+ width: 100%;
+ margin-bottom: 32px;
+`;
+
+const EnforcementContainer = styled.div`
+ position: absolute;
+ bottom: 0;
+ display: flex;
+ gap: 8px;
+ margin-bottom: 8px;
+ fill: ${({ theme }) => theme.secondaryText};
+
+ small {
+ font-weight: 400;
+ line-height: 19px;
+ color: ${({ theme }) => theme.secondaryText};
+ }
+`;
+
+const StyledCalendarIcon = styled(CalendarIcon)`
+ width: 14px;
+ height: 14px;
+`;
+
+const getCaseEventTimes = (
+ lastPeriodChange: string,
+ currentPeriodIndex: number,
+ timesPerPeriod: string[],
+ isCreation: boolean
+) => {
+ const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
+ const durationCurrentPeriod = parseInt(timesPerPeriod[currentPeriodIndex - 1]);
+ const startingDate = new Date(
+ (parseInt(lastPeriodChange) + (isCreation ? -durationCurrentPeriod : durationCurrentPeriod)) * 1000
+ );
+
+ const formattedDate = startingDate.toLocaleDateString("en-US", options);
+ return formattedDate;
+};
+
+type TimelineItems = [_TimelineItem1, ..._TimelineItem1[]];
+
+const useItems = (disputeDetails?: DisputeDetailsQuery) => {
+ const { data: disputeTemplate } = useDisputeTemplate();
+ const { id } = useParams();
+ const { data: votingHistory } = useVotingHistory(id);
+ const localRounds = votingHistory?.dispute?.disputeKitDispute?.localRounds;
+ const theme = useTheme();
+
+ return useMemo(() => {
+ const dispute = disputeDetails?.dispute;
+ if (dispute) {
+ const currentPeriodIndex = Periods[dispute.period];
+ const lastPeriodChange = dispute.lastPeriodChange;
+ const courtTimePeriods = dispute.court.timesPerPeriod;
+ return localRounds?.reduce(
+ (acc, { winningChoice }, index) => {
+ const parsedWinningChoice = parseInt(winningChoice);
+ const eventDate = getCaseEventTimes(lastPeriodChange, currentPeriodIndex, courtTimePeriods, false);
+ const icon = disputeDetails?.dispute?.ruled && index === localRounds.length - 1 ? ClosedCaseIcon : "";
+
+ acc.push({
+ title: `Jury Decision - Round ${index + 1}`,
+ party:
+ parsedWinningChoice !== 0
+ ? disputeTemplate?.answers?.[parseInt(winningChoice) - 1].title
+ : "Refuse to Arbitrate",
+ subtitle: eventDate,
+ rightSided: true,
+ variant: theme.secondaryPurple,
+ Icon: icon !== "" ? icon : undefined,
+ });
+
+ if (index < localRounds.length - 1) {
+ acc.push({
+ title: "Appealed",
+ party: "",
+ subtitle: eventDate,
+ rightSided: true,
+ Icon: AppealedCaseIcon,
+ });
+ }
+
+ return acc;
+ },
+ [
+ {
+ title: "Dispute created",
+ party: "",
+ subtitle: getCaseEventTimes(lastPeriodChange, currentPeriodIndex, courtTimePeriods, true),
+ rightSided: true,
+ variant: theme.secondaryPurple,
+ },
+ ]
+ );
+ }
+ return;
+ }, [disputeDetails, disputeTemplate, localRounds, theme]);
+};
+
+const DisputeTimeline: React.FC = () => {
+ const { id } = useParams();
+ const { data: disputeDetails } = useDisputeDetailsQuery(id);
+ const items = useItems(disputeDetails);
+
+ return (
+
+ {items && }
+ {disputeDetails?.dispute?.ruled && items && (
+
+
+ Enforcement: {items.at(-1)?.subtitle}
+
+ )}
+
+ );
+};
+export default DisputeTimeline;
diff --git a/web/src/components/Verdict/FinalDecision.tsx b/web/src/components/Verdict/FinalDecision.tsx
index d0f64e872..eb0393fda 100644
--- a/web/src/components/Verdict/FinalDecision.tsx
+++ b/web/src/components/Verdict/FinalDecision.tsx
@@ -1,33 +1,22 @@
import React from "react";
+import { useNavigate, useParams } from "react-router-dom";
import styled from "styled-components";
import Identicon from "react-identicons";
-import { useNavigate } from "react-router-dom";
import ArrowIcon from "assets/svgs/icons/arrow.svg";
+import { useDisputeTemplate } from "queries/useDisputeTemplate";
+import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
+import { useKlerosCoreCurrentRuling } from "hooks/contracts/generated";
import LightButton from "../LightButton";
import VerdictBanner from "./VerdictBanner";
-import { useKlerosCoreCurrentRuling } from "hooks/contracts/generated";
const Container = styled.div`
- position: relative;
- width: calc(200px + (360 - 200) * (100vw - 375px) / (1250 - 375));
-
- height: 400px;
- margin-left: 16px;
- .reverse-button {
- display: flex;
- flex-direction: row-reverse;
- gap: 8px;
- .button-text {
- color: ${({ theme }) => theme.primaryBlue};
- }
- }
+ width: 100%;
`;
-const JuryContanier = styled.div`
+const JuryContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
- margin-top: 32px;
h3 {
line-height: 21px;
}
@@ -40,7 +29,7 @@ const JuryDecisionTag = styled.small`
`;
const Divider = styled.hr`
- color: ${({ theme }) => theme.secondaryText};
+ color: ${({ theme }) => theme.stroke};
`;
const UserContainer = styled.div`
@@ -66,7 +55,7 @@ const StyledIdenticon = styled(Identicon)`
`;
const Header = styled.h1`
- margin: 20px 0px 48px;
+ margin: 20px 0px 32px 0px;
`;
const Title = styled.small`
@@ -74,58 +63,58 @@ const Title = styled.small`
`;
const StyledButton = styled(LightButton)`
- position: absolute;
- bottom: 0;
+ display: flex;
+ flex-direction: row-reverse;
+ gap: 8px;
+ > .button-text {
+ color: ${({ theme }) => theme.primaryBlue};
+ }
`;
-interface IDecisionText {
- ruled: boolean;
-}
-
-const DecisionText: React.FC = ({ ruled }) => {
- return ruled ? <>Final Decision> : <>Current Ruling>;
-};
-
-interface IFinalDecision {
- id: string;
- disputeTemplate: any;
- ruled: boolean;
-}
+const AnswerTitle = styled.h3`
+ margin: 0;
+`;
-const FinalDecision: React.FC = ({ id, disputeTemplate, ruled }) => {
+const FinalDecision: React.FC = () => {
+ const { id } = useParams();
+ const { data: disputeTemplate } = useDisputeTemplate(id);
+ const { data: disputeDetails } = useDisputeDetailsQuery(id);
+ const ruled = disputeDetails?.dispute?.ruled ?? false;
const navigate = useNavigate();
- const { data: currentRulingArray } = useKlerosCoreCurrentRuling({ args: [BigInt(id)], watch: true });
+ const { data: currentRulingArray } = useKlerosCoreCurrentRuling({ args: [BigInt(id ?? 0)], watch: true });
const currentRuling = Number(currentRulingArray?.[0]);
- console.log("🚀 ~ file: FinalDecision.tsx:90 ~ currentRuling:", currentRuling);
- console.log("disputeTemplate", disputeTemplate);
const answer = disputeTemplate?.answers?.[currentRuling! - 1];
- console.log("🚀 ~ file: FinalDecision.tsx:92 ~ answer:", answer);
-
- const handleClick = () => {
- navigate(`/cases/${id.toString()}/voting`);
- };
return (
-
-
+ {ruled ? "Final Decision" : "Current Ruling"}
+
The jury decided in favor of:
- {answer ? {`${answer.title}. ${answer.description}`}
: Refuse to Arbitrate
}
-
-
-
-
-
- {disputeTemplate?.aliases?.challenger && Alice.eth}
- Claimant
-
-
+ {answer ? (
+
+
{answer.title}
+
{answer.description}
+
+ ) : (
+ Refuse to Arbitrate
+ )}
+
+ {disputeTemplate?.aliases && (
+ <>
+
+
+
+ {disputeTemplate?.aliases?.challenger && Alice.eth}
+ Claimant
+
+
+
+ >
+ )}
navigate(`/cases/${id?.toString()}/voting`)}
text={"Check how the jury voted"}
Icon={ArrowIcon}
className="reverse-button"
@@ -133,4 +122,5 @@ const FinalDecision: React.FC = ({ id, disputeTemplate, ruled })
);
};
+
export default FinalDecision;
diff --git a/web/src/components/Verdict/Timeline.tsx b/web/src/components/Verdict/Timeline.tsx
deleted file mode 100644
index 7b8ee9592..000000000
--- a/web/src/components/Verdict/Timeline.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-import { Timeline } from "@kleros/ui-components-library";
-import { useKlerosCoreCurrentRuling } from "hooks/contracts/generated";
-
-const StyledTimeline = styled(Timeline)`
- margin: 0px 100px;
-`;
-
-interface IDisputeTimeline {
- id: string;
- disputeTemplate: any;
-}
-
-const DisputeTimeline: React.FC = ({ id, disputeTemplate }) => {
- const { data: currentRulingArray } = useKlerosCoreCurrentRuling({ args: [BigInt(id)], watch: true });
- const currentRuling = Number(currentRulingArray?.[0]);
-
- const answer = disputeTemplate?.answers?.[currentRuling!];
- return (
-
-
-
- );
-};
-export default DisputeTimeline;
diff --git a/web/src/components/Verdict/VerdictBanner.tsx b/web/src/components/Verdict/VerdictBanner.tsx
index 63c5e659e..8cfa591f0 100644
--- a/web/src/components/Verdict/VerdictBanner.tsx
+++ b/web/src/components/Verdict/VerdictBanner.tsx
@@ -44,7 +44,6 @@ interface IVerdictBanner {
}
const VerdictBanner: React.FC = ({ ruled }) => {
- console.log("ruledinside verdict banner", ruled);
return (
diff --git a/web/src/components/Verdict/index.tsx b/web/src/components/Verdict/index.tsx
index 1ecc1dca2..76f14bff4 100644
--- a/web/src/components/Verdict/index.tsx
+++ b/web/src/components/Verdict/index.tsx
@@ -1,25 +1,21 @@
import React from "react";
import styled from "styled-components";
import FinalDecision from "./FinalDecision";
-import DisputeTimeline from "./Timeline";
+import DisputeTimeline from "./DisputeTimeline";
const Container = styled.div`
display: flex;
- gap: 48px;
+ flex-wrap: wrap;
+ gap: 24px;
`;
-interface IVerdict {
- id: string;
- disputeTemplate: any;
- ruled: boolean;
-}
-
-const Verdict: React.FC = ({ id, disputeTemplate, ruled }) => {
+const Verdict: React.FC = () => {
return (
-
- {/* */}
+
+
);
};
+
export default Verdict;
diff --git a/web/src/graphql/gql.ts b/web/src/graphql/gql.ts
index 81fc515f5..90c809e70 100644
--- a/web/src/graphql/gql.ts
+++ b/web/src/graphql/gql.ts
@@ -33,7 +33,7 @@ const documents = {
types.HomePageDocument,
"\n query User($address: ID!) {\n user(id: $address) {\n totalDisputes\n totalResolvedDisputes\n totalCoherent\n tokens {\n court {\n id\n name\n }\n }\n shifts {\n tokenAmount\n ethAmount\n }\n }\n }\n":
types.UserDocument,
- "\n query VotingHistory($disputeID: ID!) {\n dispute(id: $disputeID) {\n id\n rounds {\n nbVotes\n }\n disputeKitDispute {\n localRounds {\n ... on ClassicRound {\n totalVoted\n votes {\n id\n juror {\n id\n }\n ... on ClassicVote {\n choice\n justification\n }\n }\n }\n }\n }\n }\n }\n":
+ "\n query VotingHistory($disputeID: ID!) {\n dispute(id: $disputeID) {\n id\n rounds {\n nbVotes\n }\n disputeKitDispute {\n localRounds {\n ... on ClassicRound {\n winningChoice\n totalVoted\n votes {\n id\n juror {\n id\n }\n ... on ClassicVote {\n choice\n justification\n }\n }\n }\n }\n }\n }\n }\n":
types.VotingHistoryDocument,
};
@@ -115,8 +115,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
- source: "\n query VotingHistory($disputeID: ID!) {\n dispute(id: $disputeID) {\n id\n rounds {\n nbVotes\n }\n disputeKitDispute {\n localRounds {\n ... on ClassicRound {\n totalVoted\n votes {\n id\n juror {\n id\n }\n ... on ClassicVote {\n choice\n justification\n }\n }\n }\n }\n }\n }\n }\n"
-): (typeof documents)["\n query VotingHistory($disputeID: ID!) {\n dispute(id: $disputeID) {\n id\n rounds {\n nbVotes\n }\n disputeKitDispute {\n localRounds {\n ... on ClassicRound {\n totalVoted\n votes {\n id\n juror {\n id\n }\n ... on ClassicVote {\n choice\n justification\n }\n }\n }\n }\n }\n }\n }\n"];
+ source: "\n query VotingHistory($disputeID: ID!) {\n dispute(id: $disputeID) {\n id\n rounds {\n nbVotes\n }\n disputeKitDispute {\n localRounds {\n ... on ClassicRound {\n winningChoice\n totalVoted\n votes {\n id\n juror {\n id\n }\n ... on ClassicVote {\n choice\n justification\n }\n }\n }\n }\n }\n }\n }\n"
+): (typeof documents)["\n query VotingHistory($disputeID: ID!) {\n dispute(id: $disputeID) {\n id\n rounds {\n nbVotes\n }\n disputeKitDispute {\n localRounds {\n ... on ClassicRound {\n winningChoice\n totalVoted\n votes {\n id\n juror {\n id\n }\n ... on ClassicVote {\n choice\n justification\n }\n }\n }\n }\n }\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
diff --git a/web/src/graphql/graphql.ts b/web/src/graphql/graphql.ts
index 528128ffe..879939a1f 100644
--- a/web/src/graphql/graphql.ts
+++ b/web/src/graphql/graphql.ts
@@ -3761,6 +3761,7 @@ export type VotingHistoryQuery = {
__typename?: "ClassicDispute";
localRounds: Array<{
__typename?: "ClassicRound";
+ winningChoice: any;
totalVoted: any;
votes: Array<{
__typename?: "ClassicVote";
@@ -4523,6 +4524,7 @@ export const VotingHistoryDocument = {
selectionSet: {
kind: "SelectionSet",
selections: [
+ { kind: "Field", name: { kind: "Name", value: "winningChoice" } },
{ kind: "Field", name: { kind: "Name", value: "totalVoted" } },
{
kind: "Field",
diff --git a/web/src/hooks/queries/useVotingHistory.ts b/web/src/hooks/queries/useVotingHistory.ts
index b428c2ffe..9bf5a6808 100644
--- a/web/src/hooks/queries/useVotingHistory.ts
+++ b/web/src/hooks/queries/useVotingHistory.ts
@@ -13,6 +13,7 @@ const votingHistoryQuery = graphql(`
disputeKitDispute {
localRounds {
... on ClassicRound {
+ winningChoice
totalVoted
votes {
id
diff --git a/web/src/pages/Cases/CaseDetails/Overview.tsx b/web/src/pages/Cases/CaseDetails/Overview.tsx
index 39add2fac..3935ab568 100644
--- a/web/src/pages/Cases/CaseDetails/Overview.tsx
+++ b/web/src/pages/Cases/CaseDetails/Overview.tsx
@@ -120,7 +120,7 @@ const Overview: React.FC = ({ arbitrable, courtID, currentPeriodIndex
{currentPeriodIndex !== Periods.evidence && (
<>
-
+
>
)}
diff --git a/yarn.lock b/yarn.lock
index 23dbea085..5735febbc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5247,7 +5247,7 @@ __metadata:
"@kleros/kleros-v2-eslint-config": "workspace:^"
"@kleros/kleros-v2-prettier-config": "workspace:^"
"@kleros/kleros-v2-tsconfig": "workspace:^"
- "@kleros/ui-components-library": ^2.5.2
+ "@kleros/ui-components-library": ^2.6.1
"@netlify/functions": ^1.6.0
"@parcel/transformer-svg-react": ~2.8.0
"@parcel/watcher": ~2.1.0
@@ -5301,9 +5301,9 @@ __metadata:
languageName: unknown
linkType: soft
-"@kleros/ui-components-library@npm:^2.5.2":
- version: 2.5.2
- resolution: "@kleros/ui-components-library@npm:2.5.2"
+"@kleros/ui-components-library@npm:^2.6.1":
+ version: 2.6.1
+ resolution: "@kleros/ui-components-library@npm:2.6.1"
dependencies:
"@datepicker-react/hooks": ^2.8.4
"@swc/helpers": ^0.3.2
@@ -5320,7 +5320,7 @@ __metadata:
react-dom: ^18.0.0
react-is: ^18.0.0
styled-components: ^5.3.3
- checksum: 159a999c4e13fb288f1594a677f325002552a06cf3c01e5139a16e2749a795934c3876e7e62eb0401ebeec85dec3ae9c66d9a14ff83de8134c58b0eadbbe6883
+ checksum: 776b95e0af1b223ad1b0e8b82819d9a3275a05df51effc40926010977d39dbd162b8eafd0241ad1efd953a96c47bb270e5839cf390e45e9b74c583695a47a37f
languageName: node
linkType: hard