Skip to content

Commit 58cc149

Browse files
committed
feat(web): arbitrator-lvl-dev-buttons
1 parent 097bbaa commit 58cc149

File tree

7 files changed

+347
-3
lines changed

7 files changed

+347
-3
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from "react";
2+
import styled, { css, keyframes } from "styled-components";
3+
4+
import DottedMenu from "svgs/icons/dotted-menu.svg";
5+
6+
const ripple = keyframes`
7+
0% {
8+
opacity: 0;
9+
transform: scale3d(0.5, 0.5, 1);
10+
}
11+
10% {
12+
opacity: 0.5;
13+
transform: scale3d(0.75, 0.75, 1);
14+
}
15+
16+
100% {
17+
opacity: 0;
18+
transform: scale3d(1.75, 1.75, 1);
19+
}
20+
`;
21+
22+
const ring = (duration: string, delay: string) => css`
23+
opacity: 0;
24+
position: absolute;
25+
top: 0;
26+
left: 0;
27+
transform: translate(50%);
28+
content: "";
29+
height: 100%;
30+
width: 100%;
31+
border: 3px solid ${({ theme }) => theme.primaryBlue};
32+
border-radius: 100%;
33+
animation-name: ${ripple};
34+
animation-duration: ${duration};
35+
animation-delay: ${delay};
36+
animation-iteration-count: infinite;
37+
animation-timing-function: cubic-bezier(0.65, 0, 0.34, 1);
38+
z-index: 0;
39+
`;
40+
41+
const Container = styled.div<{ displayRipple: boolean }>`
42+
display: flex;
43+
justify-content: center;
44+
align-items: center;
45+
width: 36px;
46+
height: 36px;
47+
${({ displayRipple }) =>
48+
displayRipple &&
49+
css`
50+
&::after {
51+
${ring("3s", "0s")}
52+
}
53+
&::before {
54+
${ring("3s", "0.5s")}
55+
}
56+
`}
57+
`;
58+
59+
const ButtonContainer = styled.div`
60+
border-radius: 50%;
61+
z-index: 1;
62+
background-color: ${({ theme }) => theme.lightBackground};
63+
`;
64+
65+
const StyledDottedMenu = styled(DottedMenu)`
66+
cursor: pointer;
67+
width: 100%;
68+
height: 100%;
69+
fill: ${({ theme }) => theme.primaryBlue};
70+
`;
71+
72+
interface IMenuButton {
73+
toggle: () => void;
74+
displayRipple: boolean;
75+
className?: string;
76+
}
77+
78+
const DottedMenuButton: React.FC<IMenuButton> = ({ toggle, displayRipple, className }) => {
79+
return (
80+
<Container {...{ displayRipple, className }}>
81+
<ButtonContainer className="button-container">
82+
<StyledDottedMenu onClick={toggle} className="menu-icon" />
83+
</ButtonContainer>
84+
</Container>
85+
);
86+
};
87+
88+
export default DottedMenuButton;

web/src/layout/Header/navbar/Debug.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ enum Phases {
5757
drawing,
5858
}
5959

60-
const Phase = () => {
60+
export const Phase = () => {
6161
const { data: phase } = useSortitionModulePhase();
6262
return <>{isUndefined(phase) ? null : <StyledLabel>Phase: {Phases[phase as number]}</StyledLabel>}</>;
6363
};

web/src/pages/Cases/CaseDetails/MaintenanceButtons/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery";
88
import { Periods } from "src/consts/periods";
99
import { Period } from "src/graphql/graphql";
1010

11+
import DottedMenuButton from "components/DottedMenuButton";
1112
import { EnsureChain } from "components/EnsureChain";
1213
import { Overlay } from "components/Overlay";
1314

1415
import DistributeRewards from "./DistributeRewards";
1516
import DrawButton from "./DrawButton";
1617
import ExecuteRulingButton from "./ExecuteRuling";
17-
import MenuButton from "./MenuButton";
1818
import PassPeriodButton from "./PassPeriodButton";
1919
import WithdrawAppealFees from "./WithdrawAppealFees";
2020

@@ -128,7 +128,7 @@ const MaintenanceButtons: React.FC = () => {
128128
</PopupContainer>
129129
</>
130130
) : null}
131-
<MenuButton {...{ toggle, displayRipple }} />
131+
<DottedMenuButton {...{ toggle, displayRipple }} />
132132
</Container>
133133
);
134134
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { usePublicClient } from "wagmi";
5+
6+
import { Button } from "@kleros/ui-components-library";
7+
8+
import {
9+
useReadSortitionModuleDelayedStakeReadIndex,
10+
useReadSortitionModuleDelayedStakeWriteIndex,
11+
useSimulateSortitionModuleExecuteDelayedStakes,
12+
useWriteSortitionModuleExecuteDelayedStakes,
13+
} from "hooks/contracts/generated";
14+
import { useSortitionModulePhase } from "hooks/useSortitionModulePhase";
15+
import { wrapWithToast } from "utils/wrapWithToast";
16+
17+
import { isUndefined } from "src/utils";
18+
19+
import { IBaseStakeMaintenanceButton, Phases } from ".";
20+
21+
const StyledButton = styled(Button)`
22+
width: 100%;
23+
`;
24+
25+
type IExecuteStakeDelayedButton = IBaseStakeMaintenanceButton;
26+
27+
const ExecuteDelayedStakeButton: React.FC<IExecuteStakeDelayedButton> = ({ setIsOpen }) => {
28+
const [isSending, setIsSending] = useState(false);
29+
const publicClient = usePublicClient();
30+
const { data: phase } = useSortitionModulePhase();
31+
const { data: delayedStakeWriteIndex } = useReadSortitionModuleDelayedStakeWriteIndex();
32+
const { data: delayedStakeReadIndex } = useReadSortitionModuleDelayedStakeReadIndex();
33+
34+
const canExecute = useMemo(() => {
35+
if (isUndefined(phase) || isUndefined(delayedStakeReadIndex) || isUndefined(delayedStakeWriteIndex)) return false;
36+
return phase === Phases.staking && delayedStakeWriteIndex >= delayedStakeReadIndex;
37+
}, [phase, delayedStakeReadIndex, delayedStakeWriteIndex]);
38+
39+
const {
40+
data: executeDelayedStakeConfig,
41+
isLoading: isLoadingConfig,
42+
isError,
43+
} = useSimulateSortitionModuleExecuteDelayedStakes({
44+
query: {
45+
enabled: canExecute,
46+
},
47+
args: [1n + (delayedStakeWriteIndex ?? 0n) - (delayedStakeReadIndex ?? 0n)],
48+
});
49+
50+
const { writeContractAsync: executeDelayedStake } = useWriteSortitionModuleExecuteDelayedStakes();
51+
52+
const isLoading = useMemo(() => isLoadingConfig || isSending, [isLoadingConfig, isSending]);
53+
const isDisabled = useMemo(() => isError || isLoading || !canExecute, [isError, isLoading, canExecute]);
54+
const handleClick = () => {
55+
if (!executeDelayedStakeConfig || !publicClient || !executeDelayedStake) return;
56+
57+
setIsSending(true);
58+
59+
wrapWithToast(async () => await executeDelayedStake(executeDelayedStakeConfig.request), publicClient).finally(
60+
() => {
61+
setIsSending(false);
62+
setIsOpen(false);
63+
}
64+
);
65+
};
66+
return (
67+
<StyledButton
68+
text="Execute Delayed Stakes"
69+
small
70+
isLoading={isLoading}
71+
disabled={isDisabled}
72+
onClick={handleClick}
73+
/>
74+
);
75+
};
76+
77+
export default ExecuteDelayedStakeButton;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useMemo, useState } from "react";
2+
import styled from "styled-components";
3+
4+
import { usePublicClient } from "wagmi";
5+
6+
import { Button } from "@kleros/ui-components-library";
7+
8+
import {
9+
useReadSortitionModuleDisputesWithoutJurors,
10+
useReadSortitionModuleLastPhaseChange,
11+
useReadSortitionModuleMaxDrawingTime,
12+
useReadSortitionModuleMinStakingTime,
13+
useSimulateSortitionModulePassPhase,
14+
useWriteSortitionModulePassPhase,
15+
} from "hooks/contracts/generated";
16+
import { useSortitionModulePhase } from "hooks/useSortitionModulePhase";
17+
import { wrapWithToast } from "utils/wrapWithToast";
18+
19+
import { isUndefined } from "src/utils";
20+
21+
import { IBaseStakeMaintenanceButton, Phases } from ".";
22+
23+
const StyledButton = styled(Button)`
24+
width: 100%;
25+
`;
26+
27+
type IPassPhaseButton = IBaseStakeMaintenanceButton;
28+
29+
const PassPhaseButton: React.FC<IPassPhaseButton> = ({ setIsOpen }) => {
30+
const [isSending, setIsSending] = useState(false);
31+
const publicClient = usePublicClient();
32+
const { data: phase } = useSortitionModulePhase();
33+
const { data: lastPhaseChange } = useReadSortitionModuleLastPhaseChange();
34+
const { data: minStakingTime } = useReadSortitionModuleMinStakingTime();
35+
const { data: maxDrawingTime } = useReadSortitionModuleMaxDrawingTime();
36+
const { data: disputeWithoutJurors } = useReadSortitionModuleDisputesWithoutJurors();
37+
38+
const canChangePhase = useMemo(() => {
39+
if (
40+
isUndefined(phase) ||
41+
isUndefined(lastPhaseChange) ||
42+
isUndefined(minStakingTime) ||
43+
isUndefined(maxDrawingTime) ||
44+
isUndefined(disputeWithoutJurors)
45+
)
46+
return false;
47+
48+
const now = Math.floor(Date.now() / 1000);
49+
switch (phase) {
50+
case Phases.staking:
51+
return BigInt(now) - lastPhaseChange >= minStakingTime;
52+
case Phases.drawing:
53+
return disputeWithoutJurors === 0n || BigInt(now) - lastPhaseChange >= maxDrawingTime;
54+
default:
55+
return true;
56+
}
57+
}, [phase, lastPhaseChange, minStakingTime, maxDrawingTime, disputeWithoutJurors]);
58+
59+
const {
60+
data: passPhaseConfig,
61+
isLoading: isLoadingConfig,
62+
isError,
63+
} = useSimulateSortitionModulePassPhase({
64+
query: {
65+
enabled: canChangePhase,
66+
},
67+
});
68+
69+
const { writeContractAsync: passPhase } = useWriteSortitionModulePassPhase();
70+
71+
const isLoading = useMemo(() => isLoadingConfig || isSending, [isLoadingConfig, isSending]);
72+
const isDisabled = useMemo(() => isError || isLoading || !canChangePhase, [isError, isLoading, canChangePhase]);
73+
const handleClick = () => {
74+
if (!passPhaseConfig || !publicClient || !passPhase) return;
75+
76+
setIsSending(true);
77+
78+
wrapWithToast(async () => await passPhase(passPhaseConfig.request), publicClient).finally(() => {
79+
setIsSending(false);
80+
setIsOpen(false);
81+
});
82+
};
83+
return <StyledButton text="Pass Phase" small isLoading={isLoading} disabled={isDisabled} onClick={handleClick} />;
84+
};
85+
86+
export default PassPhaseButton;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useState } from "react";
2+
import styled from "styled-components";
3+
4+
import DottedMenuButton from "components/DottedMenuButton";
5+
import { EnsureChain } from "components/EnsureChain";
6+
import { Overlay } from "components/Overlay";
7+
import { Phase } from "layout/Header/navbar/Debug";
8+
9+
import ExecuteDelayedStakeButton from "./ExecuteDelayedStakeButton";
10+
import PassPhaseButton from "./PassPhaseButton";
11+
12+
const Container = styled.div`
13+
width: 36px;
14+
height: 36px;
15+
display: flex;
16+
justify-content: center;
17+
align-items: center;
18+
position: relative;
19+
`;
20+
21+
const PopupContainer = styled.div`
22+
display: flex;
23+
flex-direction: column;
24+
position: absolute;
25+
height: fit-content;
26+
overflow-y: auto;
27+
z-index: 31;
28+
padding: 27px;
29+
gap: 16px;
30+
border: 1px solid ${({ theme }) => theme.stroke};
31+
background-color: ${({ theme }) => theme.whiteBackground};
32+
border-radius: 3px;
33+
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.06);
34+
35+
bottom: 0;
36+
left: 0;
37+
transform: translate(-100%, 100%);
38+
`;
39+
40+
const StyledDottedMenu = styled(DottedMenuButton)`
41+
.button-container {
42+
background-color: ${({ theme }) => theme.whiteBackground};
43+
}
44+
`;
45+
46+
export enum Phases {
47+
staking,
48+
generating,
49+
drawing,
50+
}
51+
52+
export interface IBaseStakeMaintenanceButton {
53+
setIsOpen: (open: boolean) => void;
54+
}
55+
56+
interface IStakeMaintenanceButtons {
57+
className?: string;
58+
}
59+
const StakeMaintenanceButtons: React.FC<IStakeMaintenanceButtons> = ({ className }) => {
60+
const [isOpen, setIsOpen] = useState(false);
61+
62+
const toggle = () => setIsOpen((prevValue) => !prevValue);
63+
return (
64+
<Container {...{ className }}>
65+
{isOpen ? (
66+
<>
67+
<Overlay onClick={() => setIsOpen(false)} />
68+
<PopupContainer>
69+
<EnsureChain>
70+
<>
71+
<Phase />
72+
<PassPhaseButton {...{ setIsOpen }} />
73+
<ExecuteDelayedStakeButton {...{ setIsOpen }} />
74+
</>
75+
</EnsureChain>
76+
</PopupContainer>
77+
</>
78+
) : null}
79+
<StyledDottedMenu {...{ toggle }} displayRipple={false} />
80+
</Container>
81+
);
82+
};
83+
84+
export default StakeMaintenanceButtons;

0 commit comments

Comments
 (0)