Skip to content

Commit 83a21bd

Browse files
committed
feat(web): dynamic-dispute-kit-feature-selection
1 parent baf9695 commit 83a21bd

File tree

13 files changed

+793
-414
lines changed

13 files changed

+793
-414
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from "react";
2+
3+
import { useNewDisputeContext } from "context/NewDisputeContext";
4+
5+
import { useCourtDetails } from "queries/useCourtDetails";
6+
7+
import WithHelpTooltip from "components/WithHelpTooltip";
8+
9+
import { RadioInput, StyledRadio } from ".";
10+
11+
const ClassicVote: React.FC<RadioInput> = (props) => {
12+
const { disputeData } = useNewDisputeContext();
13+
const { data: courtData } = useCourtDetails(disputeData.courtId);
14+
const isCommitEnabled = Boolean(courtData?.court?.hiddenVotes);
15+
return (
16+
<WithHelpTooltip
17+
tooltipMsg={
18+
isCommitEnabled
19+
? `The jurors' votes are hidden.
20+
Nobody can see them before the voting period completes.
21+
(It takes place in two steps commit-reveal)`
22+
: `The jurors' votes are not hidden.
23+
Everybody can see the justification and voted choice before the voting period completes.`
24+
}
25+
>
26+
<StyledRadio label={isCommitEnabled ? "Two-steps commit-reveal" : "Classic one step voting"} small {...props} />
27+
</WithHelpTooltip>
28+
);
29+
};
30+
31+
export default ClassicVote;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useEffect, useMemo } from "react";
2+
import styled from "styled-components";
3+
4+
import { Field } from "@kleros/ui-components-library";
5+
6+
import { IGatedDisputeData, useNewDisputeContext } from "context/NewDisputeContext";
7+
import { useERC1155Validation } from "hooks/useTokenAddressValidation";
8+
9+
import { isUndefined } from "src/utils";
10+
11+
import WithHelpTooltip from "components/WithHelpTooltip";
12+
13+
import { RadioInput, StyledRadio } from ".";
14+
15+
const FieldContainer = styled.div`
16+
width: 100%;
17+
padding-left: 32px;
18+
`;
19+
20+
const StyledField = styled(Field)`
21+
width: 100%;
22+
margin-top: 8px;
23+
margin-bottom: 32px;
24+
> small {
25+
margin-top: 16px;
26+
}
27+
`;
28+
29+
const GatedErc1155: React.FC<RadioInput> = (props) => {
30+
const { disputeData, setDisputeData } = useNewDisputeContext();
31+
32+
const tokenGateAddress = (disputeData.disputeKitData as IGatedDisputeData)?.tokenGate ?? "";
33+
const validationEnabled = !isUndefined(tokenGateAddress) && tokenGateAddress.trim() !== "";
34+
35+
const {
36+
isValidating,
37+
isValid,
38+
error: validationError,
39+
} = useERC1155Validation({
40+
address: tokenGateAddress,
41+
enabled: validationEnabled,
42+
});
43+
44+
const [validationMessage, variant] = useMemo(() => {
45+
if (isValidating) return [`Validating ERC-1155 token...`, "info"];
46+
else if (validationError) return [validationError, "error"];
47+
else if (isValid === true) return [`Valid ERC-1155 token`, "success"];
48+
else return [undefined, "info"];
49+
}, [isValidating, validationError, isValid]);
50+
51+
// Update validation state in dispute context
52+
useEffect(() => {
53+
// this can clash with erc20 check
54+
if (!props.checked) return;
55+
// Only update if isValid has actually changed
56+
if (disputeData.disputeKitData) {
57+
const currentData = disputeData.disputeKitData as IGatedDisputeData;
58+
59+
if (currentData.isTokenGateValid !== isValid) {
60+
setDisputeData({
61+
...disputeData,
62+
disputeKitData: { ...currentData, isTokenGateValid: isValid },
63+
});
64+
}
65+
}
66+
}, [isValid, setDisputeData, props.checked]);
67+
68+
const handleTokenAddressChange = (event: React.ChangeEvent<HTMLInputElement>) => {
69+
const currentData = disputeData.disputeKitData as IGatedDisputeData;
70+
71+
setDisputeData({
72+
...disputeData,
73+
disputeKitData: {
74+
...currentData,
75+
tokenGate: event.target.value,
76+
isTokenGateValid: null, // Reset validation state when address changes
77+
},
78+
});
79+
};
80+
81+
const handleTokenIdChange = (event: React.ChangeEvent<HTMLInputElement>) => {
82+
const currentData = disputeData.disputeKitData as IGatedDisputeData;
83+
// DEV: we only update the tokenGate value here, and the disputeKidID,
84+
// and type are still handled in Resolver/Court/FeatureSelection.tsx
85+
setDisputeData({
86+
...disputeData,
87+
disputeKitData: { ...currentData, tokenId: event.target.value },
88+
});
89+
};
90+
91+
return (
92+
<>
93+
<WithHelpTooltip
94+
tooltipMsg="Only jurors who possess the token or NFT indicated below can be selected as jurors for this case.
95+
Add the token address below."
96+
>
97+
<StyledRadio label="Jurors owning at least 1 token ERC-1155" small {...props} />
98+
</WithHelpTooltip>
99+
{props.checked ? (
100+
<FieldContainer>
101+
<StyledField
102+
dir="auto"
103+
onChange={handleTokenAddressChange}
104+
value={tokenGateAddress}
105+
placeholder="Eg. 0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"
106+
variant={variant}
107+
message={validationMessage}
108+
/>
109+
<StyledField
110+
dir="auto"
111+
onChange={handleTokenIdChange}
112+
value={(disputeData.disputeKitData as IGatedDisputeData)?.tokenId ?? "0"}
113+
placeholder="Eg. 1"
114+
/>
115+
</FieldContainer>
116+
) : null}
117+
</>
118+
);
119+
};
120+
121+
export default GatedErc1155;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { useEffect, useMemo } from "react";
2+
import styled from "styled-components";
3+
4+
import { Field } from "@kleros/ui-components-library";
5+
6+
import { IGatedDisputeData, useNewDisputeContext } from "context/NewDisputeContext";
7+
import { useERC20ERC721Validation } from "hooks/useTokenAddressValidation";
8+
9+
import { isUndefined } from "src/utils";
10+
11+
import WithHelpTooltip from "components/WithHelpTooltip";
12+
13+
import { RadioInput, StyledRadio } from ".";
14+
15+
const FieldContainer = styled.div`
16+
width: 100%;
17+
padding-left: 32px;
18+
`;
19+
20+
const StyledField = styled(Field)`
21+
width: 100%;
22+
margin-top: 8px;
23+
margin-bottom: 32px;
24+
> small {
25+
margin-top: 16px;
26+
}
27+
`;
28+
29+
const GatedErc20: React.FC<RadioInput> = (props) => {
30+
const { disputeData, setDisputeData } = useNewDisputeContext();
31+
32+
const tokenGateAddress = (disputeData.disputeKitData as IGatedDisputeData)?.tokenGate ?? "";
33+
const validationEnabled = !isUndefined(tokenGateAddress) && tokenGateAddress.trim() !== "";
34+
35+
const {
36+
isValidating,
37+
isValid,
38+
error: validationError,
39+
} = useERC20ERC721Validation({
40+
address: tokenGateAddress,
41+
enabled: validationEnabled && props.checked,
42+
});
43+
44+
const [validationMessage, variant] = useMemo(() => {
45+
if (isValidating) return [`Validating ERC-20 or ERC-721 token...`, "info"];
46+
else if (validationError) return [validationError, "error"];
47+
else if (isValid === true) return [`Valid ERC-20 or ERC-721 token`, "success"];
48+
else return [undefined, "info"];
49+
}, [isValidating, validationError, isValid]);
50+
51+
// Update validation state in dispute context
52+
useEffect(() => {
53+
// this can clash with erc1155 check
54+
if (!props.checked) return;
55+
if (disputeData.disputeKitData) {
56+
const currentData = disputeData.disputeKitData as IGatedDisputeData;
57+
58+
if (currentData.isTokenGateValid !== isValid) {
59+
setDisputeData({
60+
...disputeData,
61+
disputeKitData: { ...currentData, isTokenGateValid: isValid },
62+
});
63+
}
64+
}
65+
}, [isValid, setDisputeData, props.checked]);
66+
67+
const handleTokenAddressChange = (event: React.ChangeEvent<HTMLInputElement>) => {
68+
const currentData = disputeData.disputeKitData as IGatedDisputeData;
69+
// DEV: we only update the tokenGate value here, and the disputeKidID,
70+
// and type are still handled in Resolver/Court/FeatureSelection.tsx
71+
setDisputeData({
72+
...disputeData,
73+
disputeKitData: {
74+
...currentData,
75+
tokenGate: event.target.value,
76+
isTokenGateValid: null, // Reset validation state when address changes
77+
},
78+
});
79+
};
80+
81+
return (
82+
<>
83+
<WithHelpTooltip
84+
tooltipMsg="Only jurors who possess the token or NFT indicated below can be selected as jurors for this case.
85+
Add the token address below."
86+
>
87+
<StyledRadio label="Jurors owning at least 1 token ERC-20 or ERC-721" small {...props} />
88+
</WithHelpTooltip>
89+
{props.checked ? (
90+
<FieldContainer>
91+
<StyledField
92+
dir="auto"
93+
onChange={handleTokenAddressChange}
94+
value={tokenGateAddress}
95+
placeholder="Eg. 0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"
96+
variant={variant}
97+
message={validationMessage}
98+
/>
99+
</FieldContainer>
100+
) : null}
101+
</>
102+
);
103+
};
104+
105+
export default GatedErc20;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { Radio } from "@kleros/ui-components-library";
5+
6+
import { Features } from "consts/disputeFeature";
7+
8+
import WithHelpTooltip from "components/WithHelpTooltip";
9+
10+
import ClassicVote from "./ClassicVote";
11+
import GatedErc1155 from "./GatedErc1155";
12+
import GatedErc20 from "./GatedErc20";
13+
14+
export type RadioInput = {
15+
name: string;
16+
value: Features;
17+
checked: boolean;
18+
disabled: boolean;
19+
onClick: () => void;
20+
};
21+
22+
export type FeatureUI = (props: RadioInput) => JSX.Element;
23+
24+
export const StyledRadio = styled(Radio)`
25+
font-size: 14px;
26+
color: ${({ theme, disabled }) => (disabled ? theme.secondaryText : theme.primaryText)};
27+
opacity: ${({ disabled }) => (disabled ? "0.7" : 1)};
28+
`;
29+
30+
export const FeatureUIs: Record<Features, FeatureUI> = {
31+
[Features.ShieldedVote]: (props: RadioInput) => (
32+
<WithHelpTooltip
33+
tooltipMsg={`The jurors' votes are hidden.
34+
Nobody can see them before the voting period completes.
35+
(It takes place in one step via Shutter Network)`}
36+
>
37+
<StyledRadio label="Single-step via Shutter Network" small {...props} />
38+
</WithHelpTooltip>
39+
),
40+
41+
[Features.ClassicVote]: (props: RadioInput) => <ClassicVote {...props} />,
42+
43+
[Features.ClassicEligibility]: (props: RadioInput) => (
44+
<StyledRadio label="All the jurors in this court" small {...props} />
45+
),
46+
47+
[Features.GatedErc20]: (props: RadioInput) => <GatedErc20 {...props} />,
48+
49+
[Features.GatedErc1155]: (props: RadioInput) => <GatedErc1155 {...props} />,
50+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { Group } from "consts/disputeFeature";
5+
6+
const Container = styled.div`
7+
width: 100%;
8+
display: flex;
9+
flex-direction: column;
10+
gap: 16px;
11+
align-items: start;
12+
padding-bottom: 16px;
13+
`;
14+
15+
const HeaderContainer = styled.div`
16+
width: 100%;
17+
padding-top: 16px;
18+
`;
19+
20+
const Header = styled.h2`
21+
font-size: 16px;
22+
font-weight: 600;
23+
margin: 0;
24+
`;
25+
26+
const SubTitle = styled.p`
27+
font-size: 14px;
28+
color: ${({ theme }) => theme.secondaryText};
29+
padding: 0;
30+
margin: 0;
31+
`;
32+
33+
export type GroupUI = (props: { children: JSX.Element }) => JSX.Element;
34+
export const GroupsUI: Record<Group, GroupUI> = {
35+
[Group.Voting]: ({ children }) => (
36+
<Container key={Group.Voting}>
37+
<HeaderContainer>
38+
<Header>Shielded Voting</Header>
39+
<SubTitle>It hides the jurors&apos; votes until the end of the voting period.</SubTitle>
40+
</HeaderContainer>
41+
{children}
42+
</Container>
43+
),
44+
[Group.Eligibility]: ({ children }) => (
45+
<Container key={Group.Eligibility}>
46+
<HeaderContainer>
47+
<Header>Jurors Eligibility</Header>
48+
<SubTitle>Who can be selected as a juror?.</SubTitle>
49+
</HeaderContainer>
50+
{children}
51+
</Container>
52+
),
53+
};

0 commit comments

Comments
 (0)