Skip to content

Commit e827f63

Browse files
authored
Merge pull request #2117 from kleros/feat/court-features
Court features selection
2 parents 0e96a87 + 2842949 commit e827f63

File tree

12 files changed

+951
-356
lines changed

12 files changed

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

0 commit comments

Comments
 (0)