Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 68f66a3

Browse files
authoredMay 15, 2025··
Merge pull request #1982 from kleros/feat/batch-disputes
Feat/batch disputes
2 parents 05c5b3d + 0ea27b4 commit 68f66a3

File tree

25 files changed

+938
-123
lines changed

25 files changed

+938
-123
lines changed
 

‎web/src/app.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Loader from "components/Loader";
2626
import Layout from "layout/index";
2727

2828
import ErrorFallback from "./components/ErrorFallback";
29+
import AttachmentDisplay from "./pages/AttachmentDisplay";
2930
import { SentryRoutes } from "./utils/sentry";
3031

3132
const App: React.FC = () => {
@@ -104,6 +105,14 @@ const App: React.FC = () => {
104105
</Suspense>
105106
}
106107
/>
108+
<Route
109+
path="attachment/*"
110+
element={
111+
<Suspense fallback={<Loader width={"48px"} height={"48px"} />}>
112+
<AttachmentDisplay />
113+
</Suspense>
114+
}
115+
/>
107116
<Route path="*" element={<h1>Page not found</h1>} />
108117
</Route>
109118
</SentryRoutes>
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ export const Policies: React.FC<IPolicies> = ({ disputePolicyURI, courtId, attac
7171
<Container>
7272
<StyledP>Policy documents:</StyledP>
7373
{!isUndefined(attachment) && !isUndefined(attachment.uri) ? (
74-
<StyledInternalLink to={`attachment/?url=${getIpfsUrl(attachment.uri)}`}>
74+
<StyledInternalLink to={`/attachment/?title=${"Case Policy"}&url=${getIpfsUrl(attachment.uri)}`}>
7575
<StyledPaperclipIcon />
7676
{attachment.label ?? "Attachment"}
7777
</StyledInternalLink>
7878
) : null}
7979
{isUndefined(disputePolicyURI) ? null : (
80-
<StyledInternalLink to={`policy/attachment/?url=${getIpfsUrl(disputePolicyURI)}`}>
80+
<StyledInternalLink to={`/attachment/?title=${"Dispute Policy"}&url=${getIpfsUrl(disputePolicyURI)}`}>
8181
<StyledPolicyIcon />
8282
Dispute Policy
8383
</StyledInternalLink>

‎web/src/components/EnsureAuth.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
import React, { useCallback } from "react";
2+
import styled from "styled-components";
23

34
import { useAccount } from "wagmi";
5+
46
import { useAtlasProvider } from "@kleros/kleros-app";
57
import { Button } from "@kleros/ui-components-library";
8+
69
import { errorToast, infoToast, successToast } from "utils/wrapWithToast";
710

11+
const Container = styled.div`
12+
display: flex;
13+
flex-direction: column;
14+
gap: 16px;
15+
justify-content: center;
16+
align-items: center;
17+
`;
18+
19+
const StyledInfo = styled.p`
20+
margin: 0;
21+
padding: 0;
22+
`;
23+
824
interface IEnsureAuth {
925
children: React.ReactElement;
26+
message?: string;
27+
buttonText?: string;
1028
className?: string;
1129
}
1230

13-
const EnsureAuth: React.FC<IEnsureAuth> = ({ children, className }) => {
31+
const EnsureAuth: React.FC<IEnsureAuth> = ({ children, message, buttonText, className }) => {
1432
const { address } = useAccount();
1533
const { isVerified, isSigningIn, authoriseUser } = useAtlasProvider();
34+
1635
const handleClick = useCallback(() => {
1736
infoToast(`Signing in User...`);
1837

@@ -26,13 +45,16 @@ const EnsureAuth: React.FC<IEnsureAuth> = ({ children, className }) => {
2645
return isVerified ? (
2746
children
2847
) : (
29-
<Button
30-
text="Sign In"
31-
onClick={handleClick}
32-
disabled={isSigningIn || !address}
33-
isLoading={isSigningIn}
34-
{...{ className }}
35-
/>
48+
<Container>
49+
{message ? <StyledInfo>{message}</StyledInfo> : null}
50+
<Button
51+
text={buttonText ?? "Sign In"}
52+
onClick={handleClick}
53+
disabled={isSigningIn || !address}
54+
isLoading={isSigningIn}
55+
{...{ className }}
56+
/>
57+
</Container>
3658
);
3759
};
3860

‎web/src/components/EvidenceCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
258258
</BottomLeftContent>
259259
{fileURI && fileURI !== "-" ? (
260260
<FileLinkContainer>
261-
<StyledInternalLink to={`attachment/?url=${getIpfsUrl(fileURI)}`}>
261+
<StyledInternalLink to={`/attachment/?title=${"Evidence File"}&url=${getIpfsUrl(fileURI)}`}>
262262
<AttachmentIcon />
263263
<AttachedFileText />
264264
</StyledInternalLink>

‎web/src/components/PlusMinusField.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const IconContainer = styled.button`
2222

2323
const StyledEllipseIcon = styled(Ellipse)<{ isDisabled?: boolean }>`
2424
circle {
25+
fill: ${({ theme }) => theme.primaryBlue};
2526
${({ isDisabled }) =>
2627
isDisabled &&
2728
css`

‎web/src/context/NewDisputeContext.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import React, { createContext, useState, useContext, useMemo, useCallback } from "react";
1+
import React, { createContext, useState, useContext, useMemo, useCallback, useEffect } from "react";
22

3+
import { useLocation } from "react-router-dom";
34
import { Address } from "viem";
45

56
import { DEFAULT_CHAIN } from "consts/chains";
67
import { klerosCoreAddress } from "hooks/contracts/generated";
78
import { useLocalStorage } from "hooks/useLocalStorage";
89
import { isEmpty, isUndefined } from "utils/index";
910

11+
export const MIN_DISPUTE_BATCH_SIZE = 2;
12+
1013
export type Answer = {
1114
id: string;
1215
title: string;
@@ -59,6 +62,10 @@ interface INewDisputeContext {
5962
setIsSubmittingCase: (isSubmittingCase: boolean) => void;
6063
isPolicyUploading: boolean;
6164
setIsPolicyUploading: (isPolicyUploading: boolean) => void;
65+
isBatchCreation: boolean;
66+
setIsBatchCreation: (isBatchCreation: boolean) => void;
67+
batchSize: number;
68+
setBatchSize: (batchSize?: number) => void;
6269
}
6370

6471
const getInitialDisputeData = (): IDisputeData => ({
@@ -92,13 +99,26 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch
9299
const [disputeData, setDisputeData] = useLocalStorage<IDisputeData>("disputeData", initialDisputeData);
93100
const [isSubmittingCase, setIsSubmittingCase] = useState<boolean>(false);
94101
const [isPolicyUploading, setIsPolicyUploading] = useState<boolean>(false);
102+
const [isBatchCreation, setIsBatchCreation] = useState<boolean>(false);
103+
const [batchSize, setBatchSize] = useLocalStorage<number>("disputeBatchSize", MIN_DISPUTE_BATCH_SIZE);
95104

96105
const disputeTemplate = useMemo(() => constructDisputeTemplate(disputeData), [disputeData]);
106+
const location = useLocation();
97107

98108
const resetDisputeData = useCallback(() => {
99109
const freshData = getInitialDisputeData();
100110
setDisputeData(freshData);
101-
}, [setDisputeData]);
111+
setBatchSize(MIN_DISPUTE_BATCH_SIZE);
112+
// eslint-disable-next-line react-hooks/exhaustive-deps
113+
}, []);
114+
115+
useEffect(() => {
116+
// Cleanup function to clear local storage when user leaves the route
117+
if (location.pathname.includes("/resolver") || location.pathname.includes("/attachment")) return;
118+
119+
resetDisputeData();
120+
// eslint-disable-next-line react-hooks/exhaustive-deps
121+
}, [location.pathname]);
102122

103123
const contextValues = useMemo(
104124
() => ({
@@ -110,8 +130,23 @@ export const NewDisputeProvider: React.FC<{ children: React.ReactNode }> = ({ ch
110130
setIsSubmittingCase,
111131
isPolicyUploading,
112132
setIsPolicyUploading,
133+
isBatchCreation,
134+
setIsBatchCreation,
135+
batchSize,
136+
setBatchSize,
113137
}),
114-
[disputeData, disputeTemplate, resetDisputeData, isSubmittingCase, isPolicyUploading, setDisputeData]
138+
[
139+
disputeData,
140+
disputeTemplate,
141+
resetDisputeData,
142+
isSubmittingCase,
143+
isPolicyUploading,
144+
setDisputeData,
145+
isBatchCreation,
146+
setIsBatchCreation,
147+
batchSize,
148+
setBatchSize,
149+
]
115150
);
116151

117152
return <NewDisputeContext.Provider value={contextValues}>{children}</NewDisputeContext.Provider>;

‎web/src/hooks/queries/usePopulatedDisputeData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { executeActions } from "@kleros/kleros-sdk/src/dataMappings/executeActio
55
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
66
import { populateTemplate } from "@kleros/kleros-sdk/src/dataMappings/utils/populateTemplate";
77

8-
import { useGraphqlBatcher } from "context/GraphqlBatcher";
98
import { DEFAULT_CHAIN } from "consts/chains";
9+
import { useGraphqlBatcher } from "context/GraphqlBatcher";
1010
import { debounceErrorToast } from "utils/debounceErrorToast";
1111
import { isUndefined } from "utils/index";
1212

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
3+
import { REFETCH_INTERVAL, STALE_TIME } from "consts/index";
4+
import { useGraphqlBatcher } from "context/GraphqlBatcher";
5+
6+
import { graphql } from "src/graphql";
7+
import { RoundDetailsQuery } from "src/graphql/graphql";
8+
import { isUndefined } from "src/utils";
9+
10+
const roundDetailsQuery = graphql(`
11+
query RoundDetails($roundID: ID!) {
12+
round(id: $roundID) {
13+
court {
14+
id
15+
}
16+
nbVotes
17+
disputeKit {
18+
id
19+
}
20+
}
21+
}
22+
`);
23+
24+
export const useRoundDetailsQuery = (disputeId?: string, roundIndex?: number) => {
25+
const isEnabled = !isUndefined(disputeId) && !isUndefined(roundIndex);
26+
const { graphqlBatcher } = useGraphqlBatcher();
27+
28+
return useQuery<RoundDetailsQuery>({
29+
queryKey: [`roundDetailsQuery${disputeId}-${roundIndex}`],
30+
enabled: isEnabled,
31+
refetchInterval: REFETCH_INTERVAL,
32+
staleTime: STALE_TIME,
33+
queryFn: async () =>
34+
await graphqlBatcher.fetch({
35+
id: crypto.randomUUID(),
36+
document: roundDetailsQuery,
37+
variables: { roundID: `${disputeId}-${roundIndex}` },
38+
}),
39+
});
40+
};

‎web/src/hooks/useTransactionBatcher.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ const useTransactionBatcher = (
3737
options: TransactionBatcherOptions = { enabled: true }
3838
) => {
3939
const validatedConfigs = configs ?? [];
40+
const totalValue = validatedConfigs.reduce((sum, config) => {
41+
return sum + (config?.value ?? BigInt(0));
42+
}, BigInt(0));
43+
4044
const {
4145
data: batchConfig,
4246
isLoading,
4347
isError,
48+
error,
4449
} = useSimulateTransactionBatcherBatchSend({
4550
query: {
4651
enabled: !isUndefined(configs) && options.enabled,
@@ -50,6 +55,7 @@ const useTransactionBatcher = (
5055
validatedConfigs.map((config) => config?.value ?? BigInt(0)),
5156
validatedConfigs.map((config) => encodeFunctionData(config)),
5257
],
58+
value: totalValue,
5359
});
5460
const { writeContractAsync } = useWriteTransactionBatcherBatchSend();
5561

@@ -58,7 +64,7 @@ const useTransactionBatcher = (
5864
[writeContractAsync]
5965
);
6066

61-
return { executeBatch, batchConfig, isError, isLoading };
67+
return { executeBatch, batchConfig, isError, isLoading, error };
6268
};
6369

6470
export default useTransactionBatcher;

‎web/src/pages/Cases/AttachmentDisplay/Header.tsx renamed to ‎web/src/pages/AttachmentDisplay/Header.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import styled from "styled-components";
33

4-
import { useNavigate, useLocation, useParams } from "react-router-dom";
4+
import { useNavigate } from "react-router-dom";
55

66
import { Button } from "@kleros/ui-components-library";
77

@@ -64,24 +64,13 @@ const StyledButton = styled(Button)`
6464
}
6565
`;
6666

67-
const Header: React.FC = () => {
67+
const Header: React.FC<{ title: string }> = ({ title }) => {
6868
const navigate = useNavigate();
69-
const { id } = useParams();
70-
const location = useLocation();
7169

7270
const handleReturn = () => {
7371
navigate(-1);
7472
};
7573

76-
let title = "";
77-
if (location.pathname.includes("policy")) {
78-
title = `Policy - Case #${id}`;
79-
} else if (location.pathname.includes("evidence")) {
80-
title = "Attached File";
81-
} else if (location.pathname.includes("attachment")) {
82-
title = `Attachment - Case #${id}`;
83-
}
84-
8574
return (
8675
<Container>
8776
<TitleContainer>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { lazy, Suspense } from "react";
2+
import styled from "styled-components";
3+
4+
import { useSearchParams } from "react-router-dom";
5+
6+
import NewTabIcon from "svgs/icons/new-tab.svg";
7+
8+
import { MAX_WIDTH_LANDSCAPE } from "styles/landscapeStyle";
9+
10+
import { ExternalLink } from "components/ExternalLink";
11+
import Loader from "components/Loader";
12+
13+
import Header from "./Header";
14+
15+
const FileViewer = lazy(() => import("components/FileViewer"));
16+
17+
const Container = styled.div`
18+
width: 100%;
19+
background-color: ${({ theme }) => theme.lightBackground};
20+
padding: calc(24px + (136 - 24) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
21+
padding-top: calc(32px + (80 - 32) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
22+
padding-bottom: calc(76px + (96 - 76) * (min(max(100vw, 375px), 1250px) - 375px) / 875);
23+
max-width: ${MAX_WIDTH_LANDSCAPE};
24+
margin: 0 auto;
25+
`;
26+
27+
const AttachmentContainer = styled.div`
28+
width: 100%;
29+
display: flex;
30+
flex-direction: column;
31+
gap: 8px;
32+
`;
33+
34+
const LoaderContainer = styled.div`
35+
width: 100%;
36+
display: flex;
37+
justify-content: center;
38+
`;
39+
40+
const StyledExternalLink = styled(ExternalLink)`
41+
display: flex;
42+
align-items: center;
43+
align-self: flex-end;
44+
gap: 8px;
45+
`;
46+
47+
const StyledNewTabIcon = styled(NewTabIcon)`
48+
path {
49+
fill: ${({ theme }) => theme.primaryBlue};
50+
}
51+
`;
52+
53+
const AttachmentDisplay: React.FC = () => {
54+
const [searchParams] = useSearchParams();
55+
56+
const url = searchParams.get("url");
57+
const title = searchParams.get("title") ?? "Attachment";
58+
return (
59+
<Container>
60+
<AttachmentContainer>
61+
<Header {...{ title }} />
62+
{url ? (
63+
<>
64+
<StyledExternalLink to={url} rel="noreferrer" target="_blank">
65+
Open in new tab <StyledNewTabIcon />
66+
</StyledExternalLink>
67+
<Suspense
68+
fallback={
69+
<LoaderContainer>
70+
<Loader width={"48px"} height={"48px"} />
71+
</LoaderContainer>
72+
}
73+
>
74+
<FileViewer url={url} />
75+
</Suspense>
76+
</>
77+
) : null}
78+
</AttachmentContainer>
79+
</Container>
80+
);
81+
};
82+
83+
export default AttachmentDisplay;

‎web/src/pages/Cases/AttachmentDisplay/index.tsx

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

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

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

4+
import { Routes, Route } from "react-router-dom";
5+
46
import { MAX_WIDTH_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle";
57
import { responsiveSize } from "styles/responsiveSize";
68

7-
import { Routes, Route } from "react-router-dom";
8-
9-
import AttachmentDisplay from "./AttachmentDisplay";
109
import CaseDetails from "./CaseDetails";
1110
import CasesFetcher from "./CasesFetcher";
1211

@@ -28,9 +27,6 @@ const Cases: React.FC = () => (
2827
<Container>
2928
<Routes>
3029
<Route path="/display/:page/:order/:filter" element={<CasesFetcher />} />
31-
<Route path="/:id/evidence/attachment/*" element={<AttachmentDisplay />} />
32-
<Route path="/:id/overview/policy/attachment/*" element={<AttachmentDisplay />} />
33-
<Route path="/:id/overview/attachment/*" element={<AttachmentDisplay />} />
3430
<Route path="/:id/*" element={<CaseDetails />} />
3531
</Routes>
3632
</Container>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { Card, Radio } from "@kleros/ui-components-library";
5+
6+
import CaseFromScratchIcon from "svgs/icons/caseFromScratch.svg";
7+
import DuplicateCaseIcon from "svgs/icons/duplicateCase.svg";
8+
9+
import { responsiveSize } from "styles/responsiveSize";
10+
11+
import { Divider } from "components/Divider";
12+
import { NumberInputField } from "components/NumberInputField";
13+
import WithHelpTooltip from "components/WithHelpTooltip";
14+
15+
export enum CreationMethod {
16+
Scratch,
17+
Duplicate,
18+
}
19+
20+
const StyledCard = styled(Card)<{ selected?: boolean }>`
21+
height: fit-content;
22+
width: 100%;
23+
background: ${({ theme, selected }) => (selected ? theme.whiteBackground : theme.lightBackground)};
24+
`;
25+
26+
const CardTopContent = styled.div`
27+
width: 100%;
28+
padding: 8px ${responsiveSize(16, 24)};
29+
display: flex;
30+
align-items: center;
31+
gap: ${responsiveSize(8, 16)};
32+
`;
33+
34+
const CardBottomContent = styled.div`
35+
width: 100%;
36+
padding: 16px ${responsiveSize(16, 24)};
37+
display: flex;
38+
align-items: center;
39+
flex-wrap: wrap;
40+
gap: ${responsiveSize(8, 16)};
41+
`;
42+
43+
const Icon = styled.svg`
44+
width: 48px;
45+
height: 48px;
46+
circle {
47+
fill: ${({ theme }) => theme.lightBlue};
48+
stroke: ${({ theme }) => theme.primaryBlue};
49+
}
50+
path {
51+
fill: ${({ theme }) => theme.primaryBlue};
52+
}
53+
`;
54+
55+
const StyledP = styled.p`
56+
padding: 0;
57+
font-size: 16px;
58+
flex: 1;
59+
color: ${({ theme }) => theme.primaryText};
60+
`;
61+
62+
const StyledRadio = styled(Radio)`
63+
align-self: center;
64+
padding-left: 16px;
65+
66+
> span {
67+
transform: translateY(-50%);
68+
}
69+
`;
70+
71+
const Label = styled.label`
72+
font-size: 14px;
73+
color: ${({ theme }) => theme.primaryText};
74+
`;
75+
76+
const StyledNumberField = styled(NumberInputField)`
77+
max-width: 128px;
78+
input {
79+
border: 1px solid ${({ theme, variant }) => (variant === "error" ? theme.error : theme.stroke)};
80+
}
81+
`;
82+
83+
const ErrorMsg = styled.small`
84+
font-size: 16px;
85+
font-weight: 400;
86+
color: ${({ theme }) => theme.error};
87+
`;
88+
89+
interface ICreationCard {
90+
cardMethod: CreationMethod;
91+
selectedMethod: CreationMethod;
92+
setCreationMethod: (method: CreationMethod) => void;
93+
disputeID?: string;
94+
setDisputeID?: (id?: string) => void;
95+
isInvalidDispute?: boolean;
96+
}
97+
98+
const CreationCard: React.FC<ICreationCard> = ({
99+
cardMethod,
100+
selectedMethod,
101+
setCreationMethod,
102+
disputeID,
103+
setDisputeID,
104+
isInvalidDispute,
105+
}) => {
106+
return (
107+
<StyledCard hover onClick={() => setCreationMethod(cardMethod)} selected={cardMethod === selectedMethod}>
108+
<CardTopContent>
109+
<Icon as={cardMethod === CreationMethod.Scratch ? CaseFromScratchIcon : DuplicateCaseIcon} />
110+
<StyledP>
111+
{cardMethod === CreationMethod.Scratch ? "Create a case from scratch" : "Duplicate an existing case"}
112+
</StyledP>
113+
<StyledRadio label="" checked={cardMethod === selectedMethod} onChange={() => setCreationMethod(cardMethod)} />
114+
</CardTopContent>
115+
{cardMethod === CreationMethod.Duplicate && selectedMethod === CreationMethod.Duplicate ? (
116+
<>
117+
<Divider />
118+
<CardBottomContent>
119+
<WithHelpTooltip tooltipMsg={'The case ID can be found on the top left of the Case page. eg. "Case #300".'}>
120+
<Label>{"Enter the cases's ID"}</Label>
121+
</WithHelpTooltip>
122+
<StyledNumberField
123+
placeholder="eg. 45"
124+
value={disputeID}
125+
onChange={(val) => {
126+
if (setDisputeID) setDisputeID(val.trim() !== "" ? val : undefined);
127+
}}
128+
variant={isInvalidDispute ? "error" : undefined}
129+
/>
130+
{isInvalidDispute ? <ErrorMsg>Invalid dispute</ErrorMsg> : null}
131+
</CardBottomContent>
132+
</>
133+
) : null}
134+
</StyledCard>
135+
);
136+
};
137+
138+
export default CreationCard;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React, { useEffect, useMemo, useState } from "react";
2+
import styled, { css } from "styled-components";
3+
4+
import { useNavigate } from "react-router-dom";
5+
import { useDebounce } from "react-use";
6+
7+
import { Button } from "@kleros/ui-components-library";
8+
9+
import { AliasArray, Answer, useNewDisputeContext } from "context/NewDisputeContext";
10+
11+
import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
12+
import { usePopulatedDisputeData } from "queries/usePopulatedDisputeData";
13+
import { useRoundDetailsQuery } from "queries/useRoundDetailsQuery";
14+
15+
import { isUndefined } from "src/utils";
16+
17+
import { landscapeStyle } from "styles/landscapeStyle";
18+
import { responsiveSize } from "styles/responsiveSize";
19+
20+
import Header from "../Header";
21+
22+
import CreationCard, { CreationMethod } from "./CreationCard";
23+
24+
const Container = styled.div`
25+
display: flex;
26+
flex-direction: column;
27+
align-items: center;
28+
width: 84vw;
29+
30+
${landscapeStyle(
31+
() => css`
32+
width: ${responsiveSize(442, 700, 900)};
33+
34+
padding-bottom: 240px;
35+
`
36+
)}
37+
`;
38+
39+
const CardContainer = styled.div`
40+
width: 100%;
41+
max-width: 720px;
42+
display: flex;
43+
flex-direction: column;
44+
gap: 16px;
45+
margin-bottom: 32px;
46+
`;
47+
48+
const Landing: React.FC = () => {
49+
const navigate = useNavigate();
50+
const [creationMethod, setCreationMethod] = useState<CreationMethod>(CreationMethod.Scratch);
51+
52+
const [disputeID, setDisputeID] = useState<string>();
53+
const [debouncedDisputeID, setDebouncedDisputeID] = useState<string>();
54+
const { disputeData, setDisputeData } = useNewDisputeContext();
55+
useDebounce(() => setDebouncedDisputeID(disputeID), 500, [disputeID]);
56+
57+
const { data: dispute, isLoading: isLoadingDispute } = useDisputeDetailsQuery(debouncedDisputeID);
58+
const {
59+
data: populatedDispute,
60+
isError: isErrorPopulatedDisputeQuery,
61+
isLoading: isPopulatingDispute,
62+
} = usePopulatedDisputeData(debouncedDisputeID, dispute?.dispute?.arbitrated.id as `0x${string}`);
63+
64+
// we want the genesis round's court and numberOfJurors
65+
const {
66+
data: roundData,
67+
isError: isErrorRoundQuery,
68+
isLoading: isLoadingRound,
69+
} = useRoundDetailsQuery(debouncedDisputeID, 0);
70+
71+
const isLoading = useMemo(
72+
() => isLoadingDispute || isPopulatingDispute || isLoadingRound,
73+
[isLoadingDispute, isPopulatingDispute, isLoadingRound]
74+
);
75+
76+
const isInvalidDispute = useMemo(() => {
77+
if (isUndefined(debouncedDisputeID) || isLoading) return false;
78+
if (dispute?.dispute === null) return true;
79+
if (!isUndefined(populatedDispute)) {
80+
return isErrorRoundQuery || isErrorPopulatedDisputeQuery || Object.keys(populatedDispute).length === 0;
81+
}
82+
return false;
83+
}, [debouncedDisputeID, isLoading, populatedDispute, isErrorRoundQuery, isErrorPopulatedDisputeQuery, dispute]);
84+
85+
useEffect(() => {
86+
if (isUndefined(populatedDispute) || isUndefined(roundData) || isInvalidDispute) return;
87+
88+
const answers = populatedDispute.answers.reduce<Answer[]>((acc, val) => {
89+
acc.push({ ...val, id: parseInt(val.id, 16).toString() });
90+
return acc;
91+
}, []);
92+
93+
let aliasesArray: AliasArray[] | undefined;
94+
if (!isUndefined(populatedDispute.aliases)) {
95+
aliasesArray = Object.entries(populatedDispute.aliases).map(([key, value], index) => ({
96+
name: key,
97+
address: value,
98+
id: (index + 1).toString(),
99+
}));
100+
}
101+
102+
setDisputeData({
103+
...disputeData,
104+
title: populatedDispute.title,
105+
description: populatedDispute.description,
106+
category: populatedDispute.category,
107+
policyURI: populatedDispute.policyURI,
108+
question: populatedDispute.question,
109+
courtId: roundData.round?.court.id,
110+
numberOfJurors: roundData.round?.nbVotes,
111+
disputeKitId: parseInt(roundData.round?.disputeKit.id ?? "1", 10),
112+
answers,
113+
aliasesArray: aliasesArray ?? disputeData.aliasesArray,
114+
});
115+
// eslint-disable-next-line react-hooks/exhaustive-deps
116+
}, [populatedDispute, roundData, isInvalidDispute]);
117+
118+
return (
119+
<Container>
120+
<Header text="Create a case" />
121+
<CardContainer>
122+
<CreationCard
123+
cardMethod={CreationMethod.Scratch}
124+
selectedMethod={creationMethod}
125+
{...{ disputeID, setDisputeID, setCreationMethod, isInvalidDispute }}
126+
/>
127+
<CreationCard
128+
cardMethod={CreationMethod.Duplicate}
129+
selectedMethod={creationMethod}
130+
{...{ disputeID, setDisputeID, setCreationMethod, isInvalidDispute }}
131+
/>
132+
</CardContainer>
133+
134+
<Button
135+
text="Next"
136+
isLoading={isLoading}
137+
disabled={
138+
isLoading ||
139+
isInvalidDispute ||
140+
(creationMethod === CreationMethod.Duplicate && isUndefined(debouncedDisputeID))
141+
}
142+
onClick={() => navigate("/resolver/title")}
143+
/>
144+
</Container>
145+
);
146+
};
147+
148+
export default Landing;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React, { useMemo } from "react";
2+
import styled from "styled-components";
3+
4+
import { useNavigate } from "react-router-dom";
5+
import { useAccount, useBalance, usePublicClient } from "wagmi";
6+
7+
import { Button } from "@kleros/ui-components-library";
8+
9+
import { DEFAULT_CHAIN } from "consts/chains";
10+
import { MIN_DISPUTE_BATCH_SIZE, useNewDisputeContext } from "context/NewDisputeContext";
11+
import { disputeResolverAbi, disputeResolverAddress } from "hooks/contracts/generated";
12+
import useTransactionBatcher from "hooks/useTransactionBatcher";
13+
import { isUndefined } from "utils/index";
14+
import { parseWagmiError } from "utils/parseWagmiError";
15+
import { prepareArbitratorExtradata } from "utils/prepareArbitratorExtradata";
16+
import { wrapWithToast } from "utils/wrapWithToast";
17+
18+
import { EnsureChain } from "components/EnsureChain";
19+
import { ErrorButtonMessage } from "components/ErrorButtonMessage";
20+
import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon";
21+
22+
import { isTemplateValid } from "./SubmitDisputeButton";
23+
24+
const StyledButton = styled(Button)``;
25+
26+
const SubmitBatchDisputesButton: React.FC = () => {
27+
const publicClient = usePublicClient();
28+
const navigate = useNavigate();
29+
const { disputeTemplate, disputeData, resetDisputeData, isSubmittingCase, setIsSubmittingCase, batchSize } =
30+
useNewDisputeContext();
31+
32+
const { address, chainId } = useAccount();
33+
const { data: userBalance, isLoading: isBalanceLoading } = useBalance({ address });
34+
35+
const insufficientBalance = useMemo(() => {
36+
const arbitrationCost = disputeData.arbitrationCost ? BigInt(disputeData.arbitrationCost) : BigInt(0);
37+
return userBalance && userBalance.value < arbitrationCost * BigInt(batchSize ?? MIN_DISPUTE_BATCH_SIZE);
38+
}, [userBalance, disputeData, batchSize]);
39+
40+
const {
41+
executeBatch,
42+
batchConfig,
43+
isLoading: isLoadingConfig,
44+
error,
45+
isError,
46+
} = useTransactionBatcher(
47+
Array.from({ length: batchSize }, () => ({
48+
abi: disputeResolverAbi,
49+
address: disputeResolverAddress[chainId ?? DEFAULT_CHAIN],
50+
functionName: "createDisputeForTemplate",
51+
args: [
52+
prepareArbitratorExtradata(
53+
disputeData.courtId ?? "1",
54+
disputeData.numberOfJurors ?? 3,
55+
disputeData.disputeKitId ?? 1
56+
),
57+
JSON.stringify(disputeTemplate),
58+
"",
59+
BigInt(disputeTemplate.answers.length),
60+
],
61+
value: BigInt(disputeData.arbitrationCost ?? 0),
62+
})),
63+
{
64+
enabled: !insufficientBalance && isTemplateValid(disputeTemplate),
65+
}
66+
);
67+
68+
const isButtonDisabled = useMemo(
69+
() =>
70+
isError ||
71+
isSubmittingCase ||
72+
!isTemplateValid(disputeTemplate) ||
73+
isBalanceLoading ||
74+
insufficientBalance ||
75+
isLoadingConfig,
76+
[isSubmittingCase, insufficientBalance, isBalanceLoading, disputeTemplate, isLoadingConfig, isError]
77+
);
78+
79+
const errorMsg = useMemo(() => {
80+
if (insufficientBalance) return "Insufficient balance";
81+
else if (error) {
82+
return parseWagmiError(error);
83+
}
84+
return null;
85+
}, [error, insufficientBalance]);
86+
87+
return (
88+
<EnsureChain>
89+
<div>
90+
<StyledButton
91+
text="Create cases"
92+
disabled={isButtonDisabled}
93+
isLoading={(isSubmittingCase || isBalanceLoading || isLoadingConfig) && !insufficientBalance}
94+
onClick={() => {
95+
if (batchConfig && publicClient) {
96+
setIsSubmittingCase(true);
97+
wrapWithToast(async () => await executeBatch(batchConfig), publicClient)
98+
.then((res) => {
99+
if (res.status && !isUndefined(res.result)) {
100+
resetDisputeData();
101+
navigate("/cases/display/1/desc/all");
102+
}
103+
})
104+
.finally(() => {
105+
setIsSubmittingCase(false);
106+
});
107+
}
108+
}}
109+
/>
110+
{errorMsg && (
111+
<ErrorButtonMessage>
112+
<ClosedCircleIcon /> {errorMsg}
113+
</ErrorButtonMessage>
114+
)}
115+
</div>
116+
</EnsureChain>
117+
);
118+
};
119+
120+
export default SubmitBatchDisputesButton;

‎web/src/pages/Resolver/NavigationButtons/SubmitDisputeButton.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ const SubmitDisputeButton: React.FC = () => {
4242
return userBalance && userBalance.value < arbitrationCost;
4343
}, [userBalance, disputeData]);
4444

45-
const { data: submitCaseConfig, error } = useSimulateDisputeResolverCreateDisputeForTemplate({
45+
const {
46+
data: submitCaseConfig,
47+
error,
48+
isLoading: isLoadingConfig,
49+
isError,
50+
} = useSimulateDisputeResolverCreateDisputeForTemplate({
4651
query: {
4752
enabled: !insufficientBalance && isTemplateValid(disputeTemplate),
4853
},
@@ -62,8 +67,14 @@ const SubmitDisputeButton: React.FC = () => {
6267
const { writeContractAsync: submitCase } = useWriteDisputeResolverCreateDisputeForTemplate();
6368

6469
const isButtonDisabled = useMemo(
65-
() => isSubmittingCase || !isTemplateValid(disputeTemplate) || isBalanceLoading || insufficientBalance,
66-
[isSubmittingCase, insufficientBalance, isBalanceLoading, disputeTemplate]
70+
() =>
71+
isError ||
72+
isSubmittingCase ||
73+
!isTemplateValid(disputeTemplate) ||
74+
isBalanceLoading ||
75+
insufficientBalance ||
76+
isLoadingConfig,
77+
[isSubmittingCase, insufficientBalance, isBalanceLoading, disputeTemplate, isLoadingConfig, isError]
6778
);
6879

6980
const errorMsg = useMemo(() => {
@@ -82,9 +93,9 @@ const SubmitDisputeButton: React.FC = () => {
8293
<StyledButton
8394
text="Submit the case"
8495
disabled={isButtonDisabled}
85-
isLoading={(isSubmittingCase || isBalanceLoading) && !insufficientBalance}
96+
isLoading={(isSubmittingCase || isBalanceLoading || isLoadingConfig) && !insufficientBalance}
8697
onClick={() => {
87-
if (submitCaseConfig) {
98+
if (submitCaseConfig && publicClient) {
8899
setIsSubmittingCase(true);
89100
wrapWithToast(async () => await submitCase(submitCaseConfig.request), publicClient)
90101
.then((res) => {
@@ -123,7 +134,7 @@ const SubmitDisputeButton: React.FC = () => {
123134
);
124135
};
125136

126-
const isTemplateValid = (disputeTemplate: IDisputeTemplate) => {
137+
export const isTemplateValid = (disputeTemplate: IDisputeTemplate) => {
127138
const areVotingOptionsFilled =
128139
disputeTemplate.question !== "" &&
129140
disputeTemplate.answers.every((answer) => answer.title !== "" && answer.description !== "");

‎web/src/pages/Resolver/NavigationButtons/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import React from "react";
22
import styled from "styled-components";
33

4+
import { useNewDisputeContext } from "context/NewDisputeContext";
5+
6+
import { isUndefined } from "src/utils";
7+
48
import { responsiveSize } from "styles/responsiveSize";
59

610
import NextButton from "./NextButton";
711
import PreviousButton from "./PreviousButton";
12+
import SubmitBatchDisputesButton from "./SubmitBatchDisputesButton";
813
import SubmitDisputeButton from "./SubmitDisputeButton";
914

1015
const Container = styled.div`
@@ -21,10 +26,13 @@ interface NavigationButtonsProps {
2126
}
2227

2328
const NavigationButtons: React.FC<NavigationButtonsProps> = ({ prevRoute, nextRoute }) => {
29+
const { isBatchCreation } = useNewDisputeContext();
30+
31+
const SubmitButton = isBatchCreation ? SubmitBatchDisputesButton : SubmitDisputeButton;
2432
return (
2533
<Container>
2634
<PreviousButton prevRoute={prevRoute} />
27-
{prevRoute === "/resolver/policy" ? <SubmitDisputeButton /> : <NextButton nextRoute={nextRoute} />}
35+
{isUndefined(nextRoute) ? <SubmitButton /> : <NextButton nextRoute={nextRoute} />}
2836
</Container>
2937
);
3038
};

‎web/src/pages/Resolver/Parameters/NotablePersons/PersonFields.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef } from "react";
1+
import React, { useEffect, useRef } from "react";
22
import styled, { css } from "styled-components";
33

44
import { usePublicClient } from "wagmi";
@@ -44,6 +44,7 @@ const PersonFields: React.FC = () => {
4444
const publicClient = usePublicClient({ chainId: 1 });
4545

4646
const debounceValidateAddress = (address: string, key: number) => {
47+
if (isUndefined(publicClient)) return;
4748
// Clear the existing timer
4849
if (validationTimerRef.current) {
4950
clearTimeout(validationTimerRef.current);
@@ -59,6 +60,22 @@ const PersonFields: React.FC = () => {
5960
setDisputeData({ ...disputeData, aliasesArray: updatedAliases });
6061
}, 500);
6162
};
63+
64+
// in case of duplicate creation flow, aliasesArray will already be populated.
65+
// validating addresses in case it is
66+
useEffect(() => {
67+
if (disputeData.aliasesArray && publicClient) {
68+
disputeData.aliasesArray.map(async (alias, key) => {
69+
const isValid = await validateAddress(alias.address, publicClient);
70+
const updatedAliases = disputeData.aliasesArray;
71+
if (isUndefined(updatedAliases) || isUndefined(updatedAliases[key])) return;
72+
updatedAliases[key].isValid = isValid;
73+
74+
setDisputeData({ ...disputeData, aliasesArray: updatedAliases });
75+
});
76+
}
77+
}, []);
78+
6279
const handleAliasesWrite = (event: React.ChangeEvent<HTMLInputElement>) => {
6380
const key = parseInt(event.target.id.replace(/\D/g, ""), 10) - 1;
6481
const aliases = disputeData.aliasesArray;
@@ -68,7 +85,7 @@ const PersonFields: React.FC = () => {
6885
setDisputeData({ ...disputeData, aliasesArray: aliases });
6986

7087
//since resolving ens is async, we update asynchronously too with debounce
71-
event.target.name === "address" && debounceValidateAddress(event.target.value, key);
88+
if (event.target.name === "address") debounceValidateAddress(event.target.value, key);
7289
};
7390

7491
const showError = (alias: AliasArray) => {

‎web/src/pages/Resolver/Policy/index.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@ import styled, { css } from "styled-components";
44
import { useAtlasProvider, Roles } from "@kleros/kleros-app";
55
import { FileUploader } from "@kleros/ui-components-library";
66

7+
import PolicyIcon from "svgs/icons/policy.svg";
8+
79
import { useNewDisputeContext } from "context/NewDisputeContext";
810
import useIsDesktop from "hooks/useIsDesktop";
11+
import { getIpfsUrl } from "utils/getIpfsUrl";
912
import { errorToast, infoToast, successToast } from "utils/wrapWithToast";
1013

11-
import { getFileUploaderMsg } from "src/utils";
14+
import { getFileUploaderMsg, isUndefined } from "src/utils";
1215

16+
import { hoverShortTransitionTiming } from "styles/commonStyles";
1317
import { landscapeStyle } from "styles/landscapeStyle";
1418
import { responsiveSize } from "styles/responsiveSize";
1519

20+
import { InternalLink } from "components/InternalLink";
1621
import Header from "pages/Resolver/Header";
1722

1823
import NavigationButtons from "../NavigationButtons";
@@ -54,6 +59,25 @@ const StyledFileUploader = styled(FileUploader)`
5459
}
5560
`;
5661

62+
const StyledPolicyIcon = styled(PolicyIcon)`
63+
width: 16px;
64+
fill: ${({ theme }) => theme.primaryBlue};
65+
`;
66+
67+
const StyledInternalLink = styled(InternalLink)`
68+
${hoverShortTransitionTiming}
69+
display: flex;
70+
gap: 4px;
71+
align-self: flex-start;
72+
margin-bottom: 32px;
73+
margin-top: 32px;
74+
&:hover {
75+
svg {
76+
fill: ${({ theme }) => theme.secondaryBlue};
77+
}
78+
}
79+
`;
80+
5781
const Policy: React.FC = () => {
5882
const { disputeData, setDisputeData, setIsPolicyUploading } = useNewDisputeContext();
5983
const { uploadFile, roleRestrictions } = useAtlasProvider();
@@ -83,12 +107,18 @@ const Policy: React.FC = () => {
83107
criteria, a contract stating the rights and duties of the parties, or any set of pre-defined rules that are
84108
relevant to jurors' decision-making.
85109
</StyledLabel>
110+
86111
<StyledFileUploader
87112
callback={handleFileUpload}
88113
variant={isDesktop ? "info" : undefined}
89114
msg={`You can attach additional information here. Important: the above description must reference the relevant parts of the file content.\n${getFileUploaderMsg(Roles.Policy, roleRestrictions)}`}
90115
/>
91-
116+
{!isUndefined(disputeData.policyURI) ? (
117+
<StyledInternalLink to={`/attachment/?title=${"Policy File"}&url=${getIpfsUrl(disputeData.policyURI)}`}>
118+
<StyledPolicyIcon />
119+
Inspect the uploaded policy
120+
</StyledInternalLink>
121+
) : null}
92122
<NavigationButtons prevRoute="/resolver/notable-persons" nextRoute="/resolver/preview" />
93123
</Container>
94124
);
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import React, { useState } from "react";
2+
import styled, { css } from "styled-components";
3+
4+
import { useDebounce } from "react-use";
5+
6+
import { Card, Switch } from "@kleros/ui-components-library";
7+
8+
import { CoinIds } from "consts/coingecko";
9+
import { useNewDisputeContext } from "context/NewDisputeContext";
10+
import { useCoinPrice } from "hooks/useCoinPrice";
11+
import { formatETH, formatUnitsWei, formatUSD } from "utils/format";
12+
13+
import { isUndefined } from "src/utils";
14+
15+
import { landscapeStyle } from "styles/landscapeStyle";
16+
17+
import { Divider } from "components/Divider";
18+
import PlusMinusField from "components/PlusMinusField";
19+
import WithHelpTooltip from "components/WithHelpTooltip";
20+
21+
const Container = styled(Card)`
22+
width: 100%;
23+
height: fit-content;
24+
`;
25+
26+
const TopContent = styled.div`
27+
width: 100%;
28+
min-height: 64px;
29+
display: flex;
30+
align-items: center;
31+
flex-wrap: wrap;
32+
gap: 16px;
33+
padding: 16px;
34+
35+
span::before {
36+
background-color: ${({ theme }) => theme.whiteBackground} !important;
37+
}
38+
${landscapeStyle(
39+
() => css`
40+
padding: 0px 32px;
41+
`
42+
)}
43+
`;
44+
45+
const BottomContent = styled.div`
46+
width: 100%;
47+
min-height: 64px;
48+
display: flex;
49+
flex-wrap: wrap;
50+
align-items: center;
51+
justify-content: start;
52+
gap: 16px;
53+
padding: 16px;
54+
55+
${landscapeStyle(
56+
() => css`
57+
justify-content: space-between;
58+
padding: 16px 32px;
59+
`
60+
)}
61+
`;
62+
63+
const NumberDisplayContainer = styled.div`
64+
display: flex;
65+
align-items: center;
66+
flex-wrap: wrap;
67+
gap: 16px;
68+
${landscapeStyle(
69+
() => css`
70+
gap: 32px;
71+
`
72+
)}
73+
`;
74+
75+
const NumberDisplay = styled.div`
76+
min-width: 64px;
77+
min-height: 64px;
78+
background-color: ${({ theme }) => theme.lightBackground};
79+
border: 1px solid ${({ theme }) => theme.stroke};
80+
border-radius: 3px;
81+
font-size: 32px;
82+
color: ${({ theme }) => theme.primaryBlue};
83+
text-align: center;
84+
align-content: center;
85+
`;
86+
87+
const Label = styled.p`
88+
padding: 0;
89+
margin: 0;
90+
font-size: 16px;
91+
color: ${({ theme }) => theme.secondaryText};
92+
`;
93+
94+
const Value = styled(Label)`
95+
font-weight: 600;
96+
color: ${({ theme }) => theme.primaryText};
97+
`;
98+
99+
const StyledPlusMinusField = styled(PlusMinusField)`
100+
margin: 0;
101+
path {
102+
fill: ${({ theme }) => theme.whiteBackground};
103+
}
104+
`;
105+
106+
const StyledP = styled.p`
107+
padding: 0;
108+
margin: 0;
109+
font-size: 16px;
110+
color: ${({ theme }) => theme.primaryText};
111+
`;
112+
113+
const InfosContainer = styled.div`
114+
display: flex;
115+
align-items: center;
116+
gap: 16px;
117+
flex-wrap: wrap;
118+
`;
119+
120+
const Info = styled.div`
121+
display: flex;
122+
gap: 8px;
123+
`;
124+
125+
const BatchCreationCard: React.FC = () => {
126+
const { disputeData, isBatchCreation, setIsBatchCreation, batchSize, setBatchSize } = useNewDisputeContext();
127+
const [localBatchSize, setLocalBatchSize] = useState(batchSize);
128+
useDebounce(() => setBatchSize(localBatchSize), 500, [localBatchSize]);
129+
130+
const { prices: pricesData } = useCoinPrice([CoinIds.ETH]);
131+
132+
const coinPrice = !isUndefined(pricesData) ? pricesData[CoinIds.ETH]?.price : undefined;
133+
134+
return (
135+
<Container>
136+
<TopContent>
137+
<Switch checked={isBatchCreation} onChange={() => setIsBatchCreation(!isBatchCreation)} />
138+
<WithHelpTooltip tooltipMsg="Batch Cases: You can create multiple copies of the case. ">
139+
<StyledP>Create multiple cases at once</StyledP>
140+
</WithHelpTooltip>
141+
</TopContent>
142+
{isBatchCreation ? (
143+
<>
144+
<Divider />
145+
<BottomContent>
146+
<NumberDisplayContainer>
147+
<NumberDisplay>{localBatchSize}</NumberDisplay>
148+
<StyledPlusMinusField
149+
minValue={2}
150+
currentValue={localBatchSize}
151+
updateValue={(val) => setLocalBatchSize(val)}
152+
/>
153+
<Label>(Number of cases to be created)</Label>
154+
</NumberDisplayContainer>
155+
<InfosContainer>
156+
<Info>
157+
<Label>Jurors per case:</Label>
158+
<Value>{disputeData.numberOfJurors}</Value>
159+
</Info>
160+
<Info>
161+
<Label>Total:</Label>
162+
<Value>{disputeData.numberOfJurors * localBatchSize}</Value>
163+
</Info>
164+
<Info>
165+
<Label>Total cost:</Label>
166+
<Value>{formatETH(BigInt(disputeData.arbitrationCost ?? 0) * BigInt(localBatchSize))} ETH </Value>
167+
{!isUndefined(coinPrice) ? (
168+
<Label>
169+
~
170+
{formatUSD(
171+
Number(formatUnitsWei(BigInt(disputeData.arbitrationCost ?? 0) * BigInt(localBatchSize))) *
172+
coinPrice
173+
)}
174+
</Label>
175+
) : null}
176+
</Info>
177+
</InfosContainer>
178+
</BottomContent>
179+
</>
180+
) : null}
181+
</Container>
182+
);
183+
};
184+
185+
export default BatchCreationCard;

‎web/src/pages/Resolver/Preview/index.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@ import { Divider } from "components/Divider";
1717

1818
import NavigationButtons from "../NavigationButtons";
1919

20+
import BatchCreationCard from "./BatchCreationCard";
21+
2022
const Container = styled.div`
2123
width: 100%;
2224
padding: 0px ${responsiveSize(10, 130)};
2325
display: flex;
2426
flex-direction: column;
2527
align-items: center;
28+
gap: 16px;
2629
`;
2730

2831
const StyledCard = styled(Card)`
2932
width: 100%;
3033
height: auto;
3134
min-height: 100px;
32-
margin-bottom: ${responsiveSize(130, 70)};
35+
position: relative;
3336
`;
37+
3438
const PreviewContainer = styled.div`
3539
width: 100%;
3640
height: auto;
@@ -52,6 +56,15 @@ const Header = styled.h2`
5256
)}
5357
`;
5458

59+
const Overlay = styled.div`
60+
width: 100%;
61+
height: 100%;
62+
position: absolute;
63+
top: 0;
64+
left: 0;
65+
z-index: 2;
66+
`;
67+
5568
const Preview: React.FC = () => {
5669
const { disputeData, disputeTemplate } = useNewDisputeContext();
5770
const { data: courtPolicy } = useCourtPolicy(disputeData.courtId);
@@ -61,6 +74,7 @@ const Preview: React.FC = () => {
6174
<Container>
6275
<Header>Preview</Header>
6376
<StyledCard>
77+
<Overlay />
6478
<PreviewContainer>
6579
<DisputeContext disputeDetails={disputeTemplate} />
6680
<Divider />
@@ -76,6 +90,7 @@ const Preview: React.FC = () => {
7690
</PreviewContainer>
7791
<Policies disputePolicyURI={disputeTemplate.policyURI} courtId={disputeData.courtId} />
7892
</StyledCard>
93+
<BatchCreationCard />
7994
<NavigationButtons prevRoute="/resolver/policy" />
8095
</Container>
8196
);

‎web/src/pages/Resolver/index.tsx

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

44
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
55
import { useToggle } from "react-use";
66
import { useAccount } from "wagmi";
77

8-
import { useNewDisputeContext } from "context/NewDisputeContext";
8+
import { useAtlasProvider } from "@kleros/kleros-app";
99

1010
import { MAX_WIDTH_LANDSCAPE, landscapeStyle } from "styles/landscapeStyle";
1111
import { responsiveSize } from "styles/responsiveSize";
@@ -19,6 +19,7 @@ import ScrollTop from "components/ScrollTop";
1919

2020
import Description from "./Briefing/Description";
2121
import Title from "./Briefing/Title";
22+
import Landing from "./Landing";
2223
import Category from "./Parameters/Category";
2324
import Court from "./Parameters/Court";
2425
import Jurors from "./Parameters/Jurors";
@@ -35,6 +36,7 @@ const Wrapper = styled.div`
3536
const Container = styled.div`
3637
display: flex;
3738
flex-direction: column;
39+
gap: 32px;
3840
width: 100%;
3941
background-color: ${({ theme }) => theme.lightBackground};
4042
padding: ${responsiveSize(24, 32)};
@@ -76,20 +78,41 @@ const MiddleContentContainer = styled.div`
7678
position: relative;
7779
`;
7880

81+
const Heading = styled.h1`
82+
margin: 0;
83+
font-size: 24px;
84+
font-weight: 600;
85+
color: ${({ theme }) => theme.primaryText};
86+
text-align: center;
87+
`;
88+
89+
const Paragraph = styled.p`
90+
padding: 0;
91+
margin: 0;
92+
font-size: 16px;
93+
text-align: center;
94+
color: ${({ theme }) => theme.secondaryText};
95+
`;
96+
7997
const DisputeResolver: React.FC = () => {
8098
const location = useLocation();
8199
const [isDisputeResolverMiniGuideOpen, toggleDisputeResolverMiniGuide] = useToggle(false);
100+
const { isVerified } = useAtlasProvider();
82101
const { isConnected } = useAccount();
83102
const isPreviewPage = location.pathname.includes("/preview");
84-
const { resetDisputeData } = useNewDisputeContext();
85103

86-
useEffect(() => resetDisputeData(), []);
87104
return (
88105
<Wrapper>
89106
<HeroImage />
90107
<Container>
108+
{!isConnected || !isVerified ? (
109+
<>
110+
<Heading>Justise as a Service</Heading>
111+
<Paragraph>You send your disputes. Kleros sends back decisions.</Paragraph>
112+
</>
113+
) : null}
91114
{isConnected ? (
92-
<StyledEnsureAuth>
115+
<StyledEnsureAuth buttonText="Sign in to start">
93116
<MiddleContentContainer>
94117
{isConnected && !isPreviewPage ? (
95118
<HowItWorksAndTimeline>
@@ -102,7 +125,8 @@ const DisputeResolver: React.FC = () => {
102125
</HowItWorksAndTimeline>
103126
) : null}
104127
<Routes>
105-
<Route index element={<Navigate to="title" replace />} />
128+
<Route index element={<Navigate to="create" replace />} />
129+
<Route path="/create/*" element={<Landing />} />
106130
<Route path="/title/*" element={<Title />} />
107131
<Route path="/description/*" element={<Description />} />
108132
<Route path="/court/*" element={<Court />} />

0 commit comments

Comments
 (0)
Please sign in to comment.