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