Skip to content

Commit 0da521f

Browse files
authored
Merge pull request #1420 from kleros/fix(web)--appeal-tab-issues
Fixes appeal tab issues
2 parents aaa07c5 + a6beea5 commit 0da521f

File tree

7 files changed

+168
-84
lines changed

7 files changed

+168
-84
lines changed

web/src/hooks/useClassicAppealContext.tsx

+25-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { useClassicAppealQuery, ClassicAppealQuery } from "queries/useClassicApp
99
import { useCountdown } from "hooks/useCountdown";
1010
import { getLocalRounds } from "utils/getLocalRounds";
1111

12-
const LoserSideCountdownContext = createContext<number | undefined>(undefined);
12+
interface ICountdownContext {
13+
loserSideCountdown?: number;
14+
winnerSideCountdown?: number;
15+
}
16+
const CountdownContext = createContext<ICountdownContext>({});
1317

1418
const OptionsContext = createContext<string[] | undefined>(undefined);
1519

@@ -60,6 +64,12 @@ export const ClassicAppealProvider: React.FC<{
6064
dispute?.court.timesPerPeriod[Periods.appeal],
6165
multipliers?.loser_appeal_period_multiplier.toString()
6266
);
67+
68+
const winnerSideCountdown = useWinnerSideCountdown(
69+
dispute?.lastPeriodChange,
70+
dispute?.court.timesPerPeriod[Periods.appeal]
71+
);
72+
6373
const { loserRequiredFunding, winnerRequiredFunding } = useMemo(
6474
() => ({
6575
loserRequiredFunding: getRequiredFunding(appealCost, multipliers?.loser_stake_multiplier),
@@ -69,8 +79,11 @@ export const ClassicAppealProvider: React.FC<{
6979
);
7080
const fundedChoices = getFundedChoices(data?.dispute);
7181
const [selectedOption, setSelectedOption] = useState<number | undefined>();
82+
7283
return (
73-
<LoserSideCountdownContext.Provider value={loserSideCountdown}>
84+
<CountdownContext.Provider
85+
value={useMemo(() => ({ loserSideCountdown, winnerSideCountdown }), [loserSideCountdown, winnerSideCountdown])}
86+
>
7487
<SelectedOptionContext.Provider
7588
value={useMemo(() => ({ selectedOption, setSelectedOption }), [selectedOption, setSelectedOption])}
7689
>
@@ -89,11 +102,11 @@ export const ClassicAppealProvider: React.FC<{
89102
<OptionsContext.Provider value={options}>{children}</OptionsContext.Provider>
90103
</FundingContext.Provider>
91104
</SelectedOptionContext.Provider>
92-
</LoserSideCountdownContext.Provider>
105+
</CountdownContext.Provider>
93106
);
94107
};
95108

96-
export const useLoserSideCountdownContext = () => useContext(LoserSideCountdownContext);
109+
export const useCountdownContext = () => useContext(CountdownContext);
97110
export const useSelectedOptionContext = () => useContext(SelectedOptionContext);
98111
export const useFundingContext = () => useContext(FundingContext);
99112
export const useOptionsContext = () => useContext(OptionsContext);
@@ -136,6 +149,14 @@ function useLoserSideCountdown(lastPeriodChange = "0", appealPeriodDuration = "0
136149
return useCountdown(deadline);
137150
}
138151

152+
function useWinnerSideCountdown(lastPeriodChange = "0", appealPeriodDuration = "0") {
153+
const deadline = useMemo(
154+
() => Number(BigInt(lastPeriodChange) + BigInt(appealPeriodDuration)),
155+
[lastPeriodChange, appealPeriodDuration]
156+
);
157+
return useCountdown(deadline);
158+
}
159+
139160
const getDeadline = (lastPeriodChange: string, appealPeriodDuration: string, loserTimeMultiplier: string): number => {
140161
const parsedLastPeriodChange = BigInt(lastPeriodChange);
141162
const parsedAppealPeriodDuration = BigInt(appealPeriodDuration);

web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx

+43-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import React, { useMemo, useState } from "react";
22
import styled from "styled-components";
33
import { useParams } from "react-router-dom";
44
import { useAccount, useBalance, usePublicClient } from "wagmi";
@@ -9,15 +9,12 @@ import { isUndefined } from "utils/index";
99
import { EnsureChain } from "components/EnsureChain";
1010
import { usePrepareDisputeKitClassicFundAppeal, useDisputeKitClassicFundAppeal } from "hooks/contracts/generated";
1111
import { useParsedAmount } from "hooks/useParsedAmount";
12-
import {
13-
useLoserSideCountdownContext,
14-
useSelectedOptionContext,
15-
useFundingContext,
16-
} from "hooks/useClassicAppealContext";
12+
import { useSelectedOptionContext, useFundingContext, useCountdownContext } from "hooks/useClassicAppealContext";
1713

1814
const Container = styled.div`
1915
display: flex;
2016
flex-direction: column;
17+
align-items: center;
2118
gap: 8px;
2219
`;
2320

@@ -38,11 +35,14 @@ const StyledField = styled(Field)`
3835

3936
const StyledButton = styled(Button)`
4037
margin: auto;
41-
margin-top: 12px;
38+
margin-top: 4px;
4239
`;
4340

41+
const StyledLabel = styled.label`
42+
align-self: flex-start;
43+
`;
4444
const useNeedFund = () => {
45-
const loserSideCountdown = useLoserSideCountdownContext();
45+
const { loserSideCountdown } = useCountdownContext();
4646
const { fundedChoices, winningChoice } = useFundingContext();
4747
const needFund =
4848
(loserSideCountdown ?? 0) > 0 ||
@@ -57,15 +57,15 @@ const useNeedFund = () => {
5757
const useFundAppeal = (parsedAmount) => {
5858
const { id } = useParams();
5959
const { selectedOption } = useSelectedOptionContext();
60-
const { config: fundAppealConfig } = usePrepareDisputeKitClassicFundAppeal({
60+
const { config: fundAppealConfig, isError } = usePrepareDisputeKitClassicFundAppeal({
6161
enabled: !isUndefined(id) && !isUndefined(selectedOption),
6262
args: [BigInt(id ?? 0), BigInt(selectedOption ?? 0)],
6363
value: parsedAmount,
6464
});
6565

6666
const { writeAsync: fundAppeal } = useDisputeKitClassicFundAppeal(fundAppealConfig);
6767

68-
return fundAppeal;
68+
return { fundAppeal, isError };
6969
};
7070

7171
interface IFund {
@@ -89,39 +89,44 @@ const Fund: React.FC<IFund> = ({ amount, setAmount, setIsOpen }) => {
8989
const parsedAmount = useParsedAmount(debouncedAmount);
9090

9191
const [isSending, setIsSending] = useState(false);
92-
const fundAppeal = useFundAppeal(parsedAmount);
92+
const { fundAppeal, isError } = useFundAppeal(parsedAmount);
93+
94+
const isFundDisabled = useMemo(
95+
() =>
96+
isDisconnected || isSending || !balance || parsedAmount > balance.value || Number(parsedAmount) <= 0 || isError,
97+
[isDisconnected, isSending, balance, parsedAmount, isError]
98+
);
9399

94100
return needFund ? (
95101
<Container>
96-
<label>How much ETH do you want to contribute?</label>
97-
<div>
98-
<StyledField
99-
type="number"
100-
value={amount}
101-
onChange={(e) => {
102-
setAmount(e.target.value);
102+
<StyledLabel>How much ETH do you want to contribute?</StyledLabel>
103+
<StyledField
104+
type="number"
105+
value={amount}
106+
onChange={(e) => {
107+
setAmount(e.target.value);
108+
}}
109+
placeholder="Amount to fund"
110+
/>
111+
<EnsureChain>
112+
<StyledButton
113+
disabled={isFundDisabled}
114+
isLoading={isSending}
115+
text={isDisconnected ? "Connect to Fund" : "Fund"}
116+
onClick={() => {
117+
if (fundAppeal) {
118+
setIsSending(true);
119+
wrapWithToast(async () => await fundAppeal().then((response) => response.hash), publicClient)
120+
.then((res) => {
121+
res.status && setIsOpen(true);
122+
})
123+
.finally(() => {
124+
setIsSending(false);
125+
});
126+
}
103127
}}
104-
placeholder="Amount to fund"
105128
/>
106-
<EnsureChain>
107-
<StyledButton
108-
disabled={isDisconnected || isSending || !balance || parsedAmount > balance.value}
109-
text={isDisconnected ? "Connect to Fund" : "Fund"}
110-
onClick={() => {
111-
if (fundAppeal) {
112-
setIsSending(true);
113-
wrapWithToast(async () => await fundAppeal().then((response) => response.hash), publicClient)
114-
.then(() => {
115-
setIsOpen(true);
116-
})
117-
.finally(() => {
118-
setIsSending(false);
119-
});
120-
}
121-
}}
122-
/>
123-
</EnsureChain>
124-
</div>
129+
</EnsureChain>
125130
</Container>
126131
) : (
127132
<></>

web/src/pages/Cases/CaseDetails/Appeal/Classic/Options/StageOne.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import styled from "styled-components";
33
import StageExplainer from "../StageExplainer";
44
import OptionCard from "../../OptionCard";
55
import {
6+
useCountdownContext,
67
useFundingContext,
7-
useLoserSideCountdownContext,
88
useOptionsContext,
99
useSelectedOptionContext,
1010
} from "hooks/useClassicAppealContext";
@@ -27,14 +27,14 @@ interface IStageOne {
2727
}
2828

2929
const StageOne: React.FC<IStageOne> = ({ setAmount }) => {
30-
const { paidFees, winningChoice, loserRequiredFunding, winnerRequiredFunding } = useFundingContext();
30+
const { paidFees, winningChoice, loserRequiredFunding, winnerRequiredFunding, fundedChoices } = useFundingContext();
3131
const options = useOptionsContext();
32-
const loserSideCountdown = useLoserSideCountdownContext();
32+
const { loserSideCountdown } = useCountdownContext();
3333
const { selectedOption, setSelectedOption } = useSelectedOptionContext();
3434

3535
return (
3636
<Container>
37-
<StageExplainer {...{ loserSideCountdown }} />
37+
<StageExplainer countdown={loserSideCountdown} stage={1} />
3838
<label> Which option do you want to fund? </label>
3939
<OptionsContainer>
4040
{!isUndefined(paidFees) &&
@@ -50,6 +50,7 @@ const StageOne: React.FC<IStageOne> = ({ setAmount }) => {
5050
winner={i.toString() === winningChoice}
5151
funding={paidFees[i] ? BigInt(paidFees[i]) : 0n}
5252
required={requiredFunding}
53+
canBeSelected={!fundedChoices?.includes(i.toString())}
5354
onClick={() => {
5455
setSelectedOption(i);
5556
setAmount(formatUnitsWei(requiredFunding));

web/src/pages/Cases/CaseDetails/Appeal/Classic/Options/StageTwo.tsx

+31-20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import React, { useEffect } from "react";
22
import styled from "styled-components";
33
import OptionCard from "../../OptionCard";
4-
import { useFundingContext, useOptionsContext, useSelectedOptionContext } from "hooks/useClassicAppealContext";
4+
import {
5+
useCountdownContext,
6+
useFundingContext,
7+
useOptionsContext,
8+
useSelectedOptionContext,
9+
} from "hooks/useClassicAppealContext";
510
import { isUndefined } from "utils/index";
611
import { formatUnitsWei } from "utils/format";
12+
import StageExplainer from "../StageExplainer";
13+
import Skeleton from "react-loading-skeleton";
714

815
const Container = styled.div`
916
margin: 24px 0;
@@ -22,35 +29,39 @@ interface IStageTwo {
2229

2330
const StageTwo: React.FC<IStageTwo> = ({ setAmount }) => {
2431
const { paidFees, winningChoice, winnerRequiredFunding, fundedChoices } = useFundingContext();
32+
const { winnerSideCountdown } = useCountdownContext();
2533
const options = useOptionsContext();
2634
const { selectedOption, setSelectedOption } = useSelectedOptionContext();
2735
useEffect(() => {
2836
if (!isUndefined(winningChoice)) setSelectedOption(parseInt(winningChoice));
2937
if (!isUndefined(winnerRequiredFunding)) setAmount(formatUnitsWei(winnerRequiredFunding));
30-
});
38+
}, [winnerRequiredFunding, winningChoice]);
39+
3140
return (
3241
<Container>
33-
{!isUndefined(winningChoice) &&
34-
!isUndefined(fundedChoices) &&
35-
!isUndefined(paidFees) &&
36-
fundedChoices.length > 0 &&
37-
!fundedChoices.includes(winningChoice) ? (
42+
{!isUndefined(winningChoice) && !isUndefined(fundedChoices) && !isUndefined(paidFees) ? (
3843
<>
39-
<label>Loser deadline has finalized, you can only fund the current winner.</label>
40-
<OptionsContainer>
41-
<OptionCard
42-
text={options![winningChoice!]}
43-
selected={winningChoice === selectedOption}
44-
winner={true}
45-
funding={paidFees![winningChoice!] ? BigInt(paidFees![winningChoice!]) : 0n}
46-
required={winnerRequiredFunding!}
47-
canBeSelected={false}
48-
onClick={() => setSelectedOption(parseInt(winningChoice!, 10))}
49-
/>
50-
</OptionsContainer>
44+
{fundedChoices.length > 0 && !fundedChoices.includes(winningChoice) ? (
45+
<>
46+
<StageExplainer stage={2} countdown={winnerSideCountdown} />
47+
<OptionsContainer>
48+
<OptionCard
49+
text={options![winningChoice!]}
50+
selected={parseInt(winningChoice) === selectedOption}
51+
winner={true}
52+
funding={paidFees![winningChoice!] ? BigInt(paidFees![winningChoice!]) : 0n}
53+
required={winnerRequiredFunding!}
54+
canBeSelected={false}
55+
onClick={() => setSelectedOption(parseInt(winningChoice!, 10))}
56+
/>
57+
</OptionsContainer>
58+
</>
59+
) : (
60+
<label>No losing option has been funded in time, winner is maintained.</label>
61+
)}
5162
</>
5263
) : (
53-
<label>No losing option has been funded in time, winner is maintained.</label>
64+
<Skeleton height={140} />
5465
)}
5566
</Container>
5667
);

web/src/pages/Cases/CaseDetails/Appeal/Classic/Options/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22
import styled from "styled-components";
3-
import { useLoserSideCountdownContext } from "hooks/useClassicAppealContext";
3+
import { useCountdownContext } from "hooks/useClassicAppealContext";
44
import { StyledSkeleton } from "components/StyledSkeleton";
55
import StageOne from "./StageOne";
66
import StageTwo from "./StageTwo";
@@ -15,7 +15,7 @@ interface IOptions {
1515
}
1616

1717
const Options: React.FC<IOptions> = ({ setAmount }) => {
18-
const loserSideCountdown = useLoserSideCountdownContext();
18+
const { loserSideCountdown } = useCountdownContext();
1919
return !isUndefined(loserSideCountdown) ? (
2020
<Container>
2121
{loserSideCountdown > 0 ? <StageOne setAmount={setAmount} /> : <StageTwo setAmount={setAmount} />}

0 commit comments

Comments
 (0)