diff --git a/bot-pinner/README.md b/bot-pinner/README.md index 76704e415..7f0edf6ec 100644 --- a/bot-pinner/README.md +++ b/bot-pinner/README.md @@ -17,6 +17,7 @@ $ docker-compose up -d 2. Evidence container that awaits new events and then scrapes the latest hashes and submits it to IPFS. 3. `src/peers.txt` contains a list of peers, by default it will add connects to Estuary & Kleros IPFS nodes. This should make it easier to find content by creating a data network around Kleros Court V2. 1. Adding these peers will make it easier to find and replicate content. + ## DappNode :warning: For the following steps, you need access to [a DappNode](https://dappnode.io) system with the IPFS service running. diff --git a/web/.parcelrc b/web/.parcelrc index b812a5214..8b6b0a40c 100644 --- a/web/.parcelrc +++ b/web/.parcelrc @@ -1,6 +1,8 @@ { "extends": "@parcel/config-default", "transformers": { - "*.svg": ["...", "@parcel/transformer-svg-react"] + "*.svg": ["...", "@parcel/transformer-svg-react"], + "tsx:*.svg": ["@parcel/transformer-svg-react"], + "tsx:*": ["..."] } } diff --git a/web/src/assets/svgs/icons/appeal.png b/web/src/assets/svgs/icons/appeal.png new file mode 100644 index 000000000..d0faae682 Binary files /dev/null and b/web/src/assets/svgs/icons/appeal.png differ diff --git a/web/src/assets/svgs/icons/appeal.svg b/web/src/assets/svgs/icons/appeal.svg new file mode 100644 index 000000000..a5afa1e66 --- /dev/null +++ b/web/src/assets/svgs/icons/appeal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/assets/svgs/icons/balance.svg b/web/src/assets/svgs/icons/balance.svg new file mode 100644 index 000000000..25c5a37f9 --- /dev/null +++ b/web/src/assets/svgs/icons/balance.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/svgs/icons/info-circle.svg b/web/src/assets/svgs/icons/info-circle.svg new file mode 100644 index 000000000..bcb80ff1e --- /dev/null +++ b/web/src/assets/svgs/icons/info-circle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/src/assets/svgs/icons/kleros.svg b/web/src/assets/svgs/icons/kleros.svg index 532aaf497..e0dc1a0fd 100644 --- a/web/src/assets/svgs/icons/kleros.svg +++ b/web/src/assets/svgs/icons/kleros.svg @@ -1,3 +1,3 @@ - + diff --git a/web/src/assets/svgs/icons/voted.svg b/web/src/assets/svgs/icons/voted.svg new file mode 100644 index 000000000..d43f24ad6 --- /dev/null +++ b/web/src/assets/svgs/icons/voted.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/src/components/DisputeCard/index.tsx b/web/src/components/DisputeCard/index.tsx index c6ba8f6cb..5470894c3 100644 --- a/web/src/components/DisputeCard/index.tsx +++ b/web/src/components/DisputeCard/index.tsx @@ -34,7 +34,11 @@ const Container = styled.div` } `; -const getPeriodEndTimestamp = (lastPeriodChange: string, currentPeriodIndex: number, timesPerPeriod: string[]) => { +export const getPeriodEndTimestamp = ( + lastPeriodChange: string, + currentPeriodIndex: number, + timesPerPeriod: string[] +) => { const durationCurrentPeriod = parseInt(timesPerPeriod[currentPeriodIndex]); return parseInt(lastPeriodChange) + durationCurrentPeriod; }; diff --git a/web/src/components/Popup/Description/Appeal.tsx b/web/src/components/Popup/Description/Appeal.tsx new file mode 100644 index 000000000..e994b96c1 --- /dev/null +++ b/web/src/components/Popup/Description/Appeal.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledAmountFunded = styled.div` + display: flex; + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; +`; + +const StyledOptionFunded = styled.div` + display: flex; + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; +`; + +const AmountContainer = styled.div` + color: ${({ theme }) => theme.primaryText}; +`; + +const OptionContainer = styled.div` + color: ${({ theme }) => theme.primaryText}; +`; + +interface IAppeal { + amount: string; + option: string; +} + +const Appeal: React.FC = ({ amount, option }) => { + return ( + + + You have funded:  {amount} ETH + + + Option funded:  {option} + + + ); +}; +export default Appeal; diff --git a/web/src/components/Popup/Description/StakeWithdraw.tsx b/web/src/components/Popup/Description/StakeWithdraw.tsx new file mode 100644 index 000000000..960d817b8 --- /dev/null +++ b/web/src/components/Popup/Description/StakeWithdraw.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import styled from "styled-components"; +import { isUndefined } from "utils/index"; +import { useAccount } from "wagmi"; +import { useKlerosCoreGetJurorBalance } from "hooks/contracts/generated"; +import { format } from "src/pages/Dashboard/Courts/CourtCard"; +import KlerosLogo from "tsx:svgs/icons/kleros.svg"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const StyledKlerosLogo = styled(KlerosLogo)` + width: 14px; + height: 14px; +`; + +const StyledTitle = styled.div` + display: flex; + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; +`; + +const AmountStakedOrWithdrawnContainer = styled.div` + font-size: 24px; + font-weight: 600; + color: ${({ theme }) => theme.secondaryPurple}; + margin-bottom: calc(0px + (4 - 0) * ((100vw - 300px) / (1250 - 300))); +`; + +const TotalStakeContainer = styled.div` + display: flex; + font-size: 14px; + align-items: center; + justify-content: center; + margin-bottom: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); +`; + +const MyStakeContainer = styled.div` + display: flex; + margin: 0px calc(4px + (8 - 4) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; +`; + +const AmountContainer = styled.div` + font-weight: 600; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IStakeWithdraw { + pnkStaked: string; + courtName: string; + isStake: boolean; + courtId: string; +} + +interface IAmountStakedOrWithdrawn { + pnkStaked: string; + isStake: boolean; +} + +const AmountStakedOrWithdrawn: React.FC = ({ pnkStaked, isStake }) => { + return isStake ?
+ {pnkStaked} PNK
:
- {pnkStaked} PNK
; +}; + +const StakeWithdraw: React.FC = ({ pnkStaked, courtName, isStake, courtId }) => { + const { address } = useAccount(); + + const { data: jurorBalance } = useKlerosCoreGetJurorBalance({ + enabled: !isUndefined(address) && !isUndefined(courtId), + args: [address, BigInt(courtId)], + watch: true, + }); + + return ( + + 🎉 Your stake in the {courtName} court was successful! 🎉 + + + + + + My Stake:{" "} + {`${format(jurorBalance?.[0])} PNK`} + + + ); +}; +export default StakeWithdraw; diff --git a/web/src/components/Popup/Description/VoteWithCommit.tsx b/web/src/components/Popup/Description/VoteWithCommit.tsx new file mode 100644 index 000000000..789840385 --- /dev/null +++ b/web/src/components/Popup/Description/VoteWithCommit.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledDescription = styled.div` + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; + line-height: 21.8px; +`; + +const EmphasizedDate = styled.span` + font-size: 16px; + font-weight: 400; + line-height: 21.8px; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IVoteWithCommit { + date: string; +} + +const VoteWithCommit: React.FC = ({ date }) => { + return ( + + + Your vote is confirmed. It's kept secret until all jurors have cast their votes. + You'll need to justify and reveal your vote on {date} + + + ); +}; +export default VoteWithCommit; diff --git a/web/src/components/Popup/Description/VoteWithoutCommit.tsx b/web/src/components/Popup/Description/VoteWithoutCommit.tsx new file mode 100644 index 000000000..a5b445130 --- /dev/null +++ b/web/src/components/Popup/Description/VoteWithoutCommit.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledDescription = styled.div` + margin-bottom: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); + margin-left: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + color: ${({ theme }) => theme.secondaryText}; + text-align: center; + line-height: 21.8px; +`; + +const EmphasizedDate = styled.span` + font-size: 16px; + font-weight: 400; + line-height: 21.8px; + color: ${({ theme }) => theme.primaryText}; +`; + +interface IVoteWithoutCommit { + date: string; +} + +const VoteWithoutCommit: React.FC = ({ date }) => { + return ( + + + The decision date is {date} with the possibility for appeals. After that time + you will be informed about the jury decision. + + + ); +}; +export default VoteWithoutCommit; diff --git a/web/src/components/Popup/ExtraInfo/StakeWithdrawExtraInfo.tsx b/web/src/components/Popup/ExtraInfo/StakeWithdrawExtraInfo.tsx new file mode 100644 index 000000000..5ca4b4169 --- /dev/null +++ b/web/src/components/Popup/ExtraInfo/StakeWithdrawExtraInfo.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + color: ${({ theme }) => theme.secondaryText}; + text-align: center; + margin-left: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-right: calc(8px + (44 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-top: calc(8px + (24 - 8) * ((100vw - 300px) / (1250 - 300))); +`; + +const StakeWithdrawExtraInfo: React.FC = () => { + return ( + + { + "In order not to miss when you're drawn for cases, make sure to subscribe to notifications: Settings > Notifications" + } + + ); +}; +export default StakeWithdrawExtraInfo; diff --git a/web/src/components/Popup/ExtraInfo/VoteWithCommitExtraInfo.tsx b/web/src/components/Popup/ExtraInfo/VoteWithCommitExtraInfo.tsx new file mode 100644 index 000000000..199264b3d --- /dev/null +++ b/web/src/components/Popup/ExtraInfo/VoteWithCommitExtraInfo.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import styled from "styled-components"; +import InfoCircle from "tsx:svgs/icons/info-circle.svg"; + +const Container = styled.div` + display: flex; + gap: 8px; + text-align: center; + align-items: center; + margin: 0 calc(8px + (32 - 8) * ((100vw - 300px) / (1250 - 300))); + margin-top: calc(8px + (24 - 8) * ((100vw - 300px) / (1250 - 300))); + small { + font-size: 14px; + font-weight: 400; + color: ${({ theme }) => theme.secondaryText}; + line-height: 19px; + } +`; + +const StyledInfoCircle = styled(InfoCircle)` + min-width: 16px; + min-height: 16px; +`; + +const VoteWithCommitExtraInfo: React.FC = () => { + return ( + + + Subscribe to receive notifications to be reminded when the reveal time comes. + + ); +}; +export default VoteWithCommitExtraInfo; diff --git a/web/src/components/Popup/index.tsx b/web/src/components/Popup/index.tsx new file mode 100644 index 000000000..f9433f1a2 --- /dev/null +++ b/web/src/components/Popup/index.tsx @@ -0,0 +1,171 @@ +import React, { useRef } from "react"; +import styled from "styled-components"; +import { Button } from "@kleros/ui-components-library"; +import { Overlay } from "components/Overlay"; +import StakeWithdraw from "./Description/StakeWithdraw"; +import VoteWithCommit from "./Description/VoteWithCommit"; +import VoteWithoutCommit from "./Description/VoteWithoutCommit"; +import Appeal from "./Description/Appeal"; +import VoteWithCommitExtraInfo from "./ExtraInfo/VoteWithCommitExtraInfo"; +import StakeWithdrawExtraInfo from "./ExtraInfo/StakeWithdrawExtraInfo"; + +export enum PopupType { + STAKE_WITHDRAW = "STAKE_WITHDRAW", + APPEAL = "APPEAL", + VOTE_WITHOUT_COMMIT = "VOTE_WITHOUT_COMMIT", + VOTE_WITH_COMMIT = "VOTE_WITH_COMMIT", +} + +interface IStakeWithdraw { + popupType: PopupType.STAKE_WITHDRAW; + pnkStaked: string; + courtName: string; + isStake: boolean; + courtId: string; +} + +interface IVoteWithoutCommit { + popupType: PopupType.VOTE_WITHOUT_COMMIT; + date: string; +} + +interface IVoteWithCommit { + popupType: PopupType.VOTE_WITH_COMMIT; + date: string; +} + +interface IAppeal { + popupType: PopupType.APPEAL; + amount: string; + option: string; +} +interface IPopup { + title: string; + icon: React.FC>; + popupType: PopupType; + setIsOpen: (val: boolean) => void; + setAmount?: (val: string) => void; + isCommit?: boolean; +} + +type PopupProps = IStakeWithdraw | IVoteWithoutCommit | IVoteWithCommit | IAppeal; + +const Header = styled.h1` + display: flex; + margin-top: calc(12px + (32 - 12) * ((100vw - 300px) / (1250 - 300))); + margin-bottom: calc(12px + (24 - 12) * ((100vw - 300px) / (1250 - 300))); + font-size: 24px; + font-weight: 600; + line-height: 32.68px; +`; + +const IconContainer = styled.div` + width: calc(150px + (350 - 150) * (100vw - 375px) / (1250 - 375)); + display: flex; + align-items: center; + justify-content: center; + + svg { + display: inline-block; + width: calc(150px + (350 - 150) * (100vw - 375px) / (1250 - 375)); + height: calc(150px + (350 - 150) * (100vw - 375px) / (1250 - 375)); + } +`; + +const StyledButton = styled(Button)` + margin: calc(16px + (32 - 16) * ((100vw - 300px) / (1250 - 300))); +`; + +const Container = styled.div` + display: flex; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + z-index: 10; + flex-direction: column; + align-items: center; + justify-content: center; + width: calc(300px + (600 - 300) * (100vw - 375px) / (1250 - 375)); + max-width: 600px; + border-radius: 3px; + border: 1px solid ${({ theme }) => theme.stroke}; + background-color: ${({ theme }) => theme.whiteBackground}; + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.06); + + svg { + visibility: visible; + } +`; + +const Popup: React.FC = ({ + title, + icon: Icon, + popupType, + setIsOpen, + setAmount, + isCommit, + ...props +}) => { + const containerRef = useRef(null); + + const resetValue = () => { + if (setAmount) { + setAmount(""); + } + }; + + let PopupComponent: JSX.Element | null = null; + + switch (popupType) { + case PopupType.STAKE_WITHDRAW: { + const { pnkStaked, courtName, isStake, courtId } = props as IStakeWithdraw; + PopupComponent = ( + + ); + break; + } + case PopupType.VOTE_WITHOUT_COMMIT: { + const { date } = props as IVoteWithoutCommit; + PopupComponent = ; + break; + } + case PopupType.VOTE_WITH_COMMIT: { + const { date } = props as IVoteWithCommit; + PopupComponent = ; + break; + } + case PopupType.APPEAL: { + const { amount, option } = props as IAppeal; + PopupComponent = ; + break; + } + default: + break; + } + + return ( + <> + + +
{title}
+ {PopupComponent} + + + + {popupType === PopupType.STAKE_WITHDRAW && } + {popupType === PopupType.VOTE_WITH_COMMIT && } + { + setIsOpen(false); + resetValue(); + }} + /> +
+ + ); +}; +export default Popup; diff --git a/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx b/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx index 3888b2b25..12ab41015 100644 --- a/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx +++ b/web/src/pages/Cases/CaseDetails/Appeal/Classic/Fund.tsx @@ -42,7 +42,13 @@ const useFundAppeal = (parsedAmount) => { return fundAppeal; }; -const Fund: React.FC = () => { +interface IFund { + amount: string; + setAmount: (val: string) => void; + setIsOpen: (val: boolean) => void; +} + +const Fund: React.FC = ({ amount, setAmount, setIsOpen }) => { const needFund = useNeedFund(); const { address, isDisconnected } = useAccount(); const { data: balance } = useBalance({ @@ -51,7 +57,6 @@ const Fund: React.FC = () => { }); const publicClient = usePublicClient(); - const [amount, setAmount] = useState(""); const [debouncedAmount, setDebouncedAmount] = useState(""); useDebounce(() => setDebouncedAmount(amount), 500, [amount]); @@ -79,12 +84,12 @@ const Fund: React.FC = () => { onClick={() => { if (fundAppeal) { setIsSending(true); - wrapWithToast(async () => await fundAppeal().then((response) => response.hash), publicClient) - .then(() => { - setAmount(""); - close(); - }) - .finally(() => setIsSending(false)); + wrapWithToast(async () => await fundAppeal().then((response) => response.hash), publicClient).finally( + () => { + setIsSending(false); + setIsOpen(true); + } + ); } }} /> diff --git a/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx b/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx index 657f6748d..d5020a627 100644 --- a/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Appeal/Classic/index.tsx @@ -1,15 +1,44 @@ -import React from "react"; -import { ClassicAppealProvider } from "hooks/useClassicAppealContext"; +import React, { useState } from "react"; +import { ClassicAppealProvider, useOptionsContext, useSelectedOptionContext } from "hooks/useClassicAppealContext"; import Options from "./Options"; import Fund from "./Fund"; +import Popup, { PopupType } from "components/Popup"; +import AppealIcon from "svgs/icons/appeal.svg"; +import { isUndefined } from "utils/index"; -const Classic: React.FC = () => ( - -

Appeal crowdfunding

- - - -
-); +const Classic: React.FC = () => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + const [amount, setAmount] = useState(""); + const { selectedOption } = useSelectedOptionContext(); + const options = useOptionsContext(); -export default Classic; + return ( + <> + {isPopupOpen && ( + + )} +

Appeal crowdfunding

+ + + + + ); +}; + +const ClassicWrapper: React.FC = () => { + return ( + + + + ); +}; + +export default ClassicWrapper; diff --git a/web/src/pages/Cases/CaseDetails/Appeal/index.tsx b/web/src/pages/Cases/CaseDetails/Appeal/index.tsx index d81c37167..b30c6a24f 100644 --- a/web/src/pages/Cases/CaseDetails/Appeal/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Appeal/index.tsx @@ -1,8 +1,8 @@ import React from "react"; -import Classic from "./Classic"; +import ClassicWrapper from "./Classic"; import { Periods } from "consts/periods"; const Appeal: React.FC<{ currentPeriodIndex: number }> = ({ currentPeriodIndex }) => - Periods.appeal === currentPeriodIndex ? :

Not in appeal period

; + Periods.appeal === currentPeriodIndex ? :

Not in appeal period

; export default Appeal; diff --git a/web/src/pages/Cases/CaseDetails/Voting/Classic.tsx b/web/src/pages/Cases/CaseDetails/Voting/Classic.tsx index 3d44bab92..e4e878792 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/Classic.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/Classic.tsx @@ -44,7 +44,13 @@ const RefuseToArbitrateContainer = styled.div` justify-content: center; `; -const Classic: React.FC<{ arbitrable: `0x${string}`; voteIDs: string[] }> = ({ arbitrable, voteIDs }) => { +interface IClassic { + arbitrable: `0x${string}`; + voteIDs: string[]; + setIsOpen: (val: boolean) => void; +} + +const Classic: React.FC = ({ arbitrable, voteIDs, setIsOpen }) => { const { id } = useParams(); const parsedDisputeID = BigInt(id ?? 0); const parsedVoteIDs = useMemo(() => voteIDs.map((voteID) => BigInt(voteID)), [voteIDs]); @@ -70,10 +76,14 @@ const Classic: React.FC<{ arbitrable: `0x${string}`; voteIDs: string[] }> = ({ a ], }); if (walletClient) { - wrapWithToast(async () => await walletClient.writeContract(request), publicClient).finally(() => { - setChosenOption(-1); - setIsSending(false); - }); + wrapWithToast(async () => await walletClient.writeContract(request), publicClient) + .then(() => { + setIsOpen(true); + }) + .finally(() => { + setChosenOption(-1); + setIsSending(false); + }); } }; diff --git a/web/src/pages/Cases/CaseDetails/Voting/index.tsx b/web/src/pages/Cases/CaseDetails/Voting/index.tsx index bbdaec0c9..d9c8ada77 100644 --- a/web/src/pages/Cases/CaseDetails/Voting/index.tsx +++ b/web/src/pages/Cases/CaseDetails/Voting/index.tsx @@ -1,13 +1,23 @@ -import React from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { useAccount } from "wagmi"; +import { useLockBodyScroll } from "react-use"; import { useDisputeDetailsQuery } from "queries/useDisputeDetailsQuery"; import { useDrawQuery } from "queries/useDrawQuery"; import Classic from "./Classic"; import VotingHistory from "./VotingHistory"; +import Popup, { PopupType } from "components/Popup"; import { Periods } from "consts/periods"; import { isUndefined } from "utils/index"; +import { getPeriodEndTimestamp } from "components/DisputeCard"; import { useDisputeKitClassicIsVoteActive } from "hooks/contracts/generated"; +import VoteIcon from "assets/svgs/icons/voted.svg"; + +function formatDate(unixTimestamp: number): string { + const date = new Date(unixTimestamp * 1000); + const options: Intl.DateTimeFormatOptions = { month: "long", day: "2-digit", year: "numeric" }; + return date.toLocaleDateString("en-US", options); +} const Voting: React.FC<{ arbitrable?: `0x${string}`; @@ -24,14 +34,37 @@ const Voting: React.FC<{ args: [BigInt(id ?? 0), roundId, voteId], watch: true, }); - return drawData && - !isUndefined(arbitrable) && - currentPeriodIndex === Periods.vote && - drawData.draws?.length > 0 && - !voted ? ( - draw.voteID)} /> - ) : ( - + const [isPopupOpen, setIsPopupOpen] = useState(false); + useLockBodyScroll(isPopupOpen); + const lastPeriodChange = disputeData?.dispute?.lastPeriodChange; + const timesPerPeriod = disputeData?.dispute?.court?.timesPerPeriod; + const finalDate = + !isUndefined(currentPeriodIndex) && + !isUndefined(timesPerPeriod) && + getPeriodEndTimestamp(lastPeriodChange, currentPeriodIndex, timesPerPeriod); + + return ( + <> + {isPopupOpen && ( + + )} + {drawData && + !isUndefined(arbitrable) && + currentPeriodIndex === Periods.vote && + drawData.draws?.length > 0 && + !voted ? ( + draw.voteID)} /> + ) : ( + + )} + ); }; diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx index 5451f9b91..b43e5aa7e 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/InputDisplay.tsx @@ -5,7 +5,6 @@ import { formatEther } from "viem"; import { useDebounce } from "react-use"; import { useAccount } from "wagmi"; import { Field } from "@kleros/ui-components-library"; - import { useParsedAmount } from "hooks/useParsedAmount"; import { useKlerosCoreGetJurorBalance, usePnkBalanceOf } from "hooks/contracts/generated"; import StakeWithdrawButton, { ActionType } from "./StakeWithdrawButton"; @@ -38,10 +37,19 @@ interface IInputDisplay { action: ActionType; isSending: boolean; setIsSending: (arg0: boolean) => void; + setIsPopupOpen: (arg0: boolean) => void; + amount: string; + setAmount: (arg0: string) => void; } -const InputDisplay: React.FC = ({ action, isSending, setIsSending }) => { - const [amount, setAmount] = useState(""); +const InputDisplay: React.FC = ({ + action, + isSending, + setIsSending, + setIsPopupOpen, + amount, + setAmount, +}) => { const [debouncedAmount, setDebouncedAmount] = useState(""); useDebounce(() => setDebouncedAmount(amount), 500, [amount]); const parsedAmount = useParsedAmount(debouncedAmount); @@ -96,6 +104,7 @@ const InputDisplay: React.FC = ({ action, isSending, setIsSending setAmount, isSending, setIsSending, + setIsPopupOpen, }} /> diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx index 000d1d5aa..962c44184 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/StakeWithdrawButton.tsx @@ -28,9 +28,17 @@ interface IActionButton { action: ActionType; setIsSending: (arg0: boolean) => void; setAmount: (arg0: string) => void; + setIsPopupOpen: (arg0: boolean) => void; } -const StakeWithdrawButton: React.FC = ({ parsedAmount, action, setAmount, isSending, setIsSending }) => { +const StakeWithdrawButton: React.FC = ({ + parsedAmount, + action, + setAmount, + isSending, + setIsSending, + setIsPopupOpen, +}) => { const { id } = useParams(); const { address } = useAccount(); const klerosCore = getKlerosCore({}); @@ -91,11 +99,10 @@ const StakeWithdrawButton: React.FC = ({ parsedAmount, action, se const handleStake = () => { if (typeof setStake !== "undefined") { setIsSending(true); - wrapWithToast(async () => await setStake().then((response) => response.hash), publicClient) - .then(() => { - setAmount(""); - }) - .finally(() => setIsSending(false)); + wrapWithToast(async () => await setStake().then((response) => response.hash), publicClient).finally(() => { + setIsSending(false); + setIsPopupOpen(true); + }); } }; diff --git a/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx b/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx index 640d2c146..d814e9e56 100644 --- a/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx +++ b/web/src/pages/Courts/CourtDetails/StakePanel/index.tsx @@ -1,18 +1,22 @@ import React, { useState } from "react"; import styled from "styled-components"; - +import { useLockBodyScroll } from "react-use"; import Tag from "components/Tag"; import JurorBalanceDisplay from "./JurorStakeDisplay"; import InputDisplay from "./InputDisplay"; import { ActionType } from "./StakeWithdrawButton"; +import Popup, { PopupType } from "components/Popup/index"; +import BalanceIcon from "assets/svgs/icons/balance.svg"; -const StakePanel: React.FC<{ courtName: string }> = ({ - courtName = "General Court", -}) => { +const StakePanel: React.FC<{ courtName: string; id: string }> = ({ courtName = "General Court", id }) => { + const [amount, setAmount] = useState(""); const [isSending, setIsSending] = useState(false); + const [isPopupOpen, setIsPopupOpen] = useState(false); const [isActive, setIsActive] = useState(true); const [action, setAction] = useState(ActionType.stake); + useLockBodyScroll(isPopupOpen); + const handleClick = (action: ActionType) => { setIsActive(action === ActionType.stake); setAction(action); @@ -22,25 +26,30 @@ const StakePanel: React.FC<{ courtName: string }> = ({ return ( - handleClick(ActionType.stake)} - /> - handleClick(ActionType.withdraw)} - /> + handleClick(ActionType.stake)} /> + handleClick(ActionType.withdraw)} /> - + + {isPopupOpen && ( + + )} ); }; @@ -48,6 +57,7 @@ const StakePanel: React.FC<{ courtName: string }> = ({ export default StakePanel; const Container = styled.div` + position: relative; width: 100%; margin-top: 32px; display: flex; diff --git a/web/src/pages/Courts/CourtDetails/index.tsx b/web/src/pages/Courts/CourtDetails/index.tsx index a0fb7e591..82abe751d 100644 --- a/web/src/pages/Courts/CourtDetails/index.tsx +++ b/web/src/pages/Courts/CourtDetails/index.tsx @@ -5,7 +5,7 @@ import { useParams } from "react-router-dom"; import { Card, Breadcrumb } from "@kleros/ui-components-library"; import { useCourtPolicy } from "queries/useCourtPolicy"; import { useCourtTree, CourtTreeQuery } from "queries/useCourtTree"; - +import { isUndefined } from "utils/index"; import Stats from "./Stats"; import Description from "./Description"; import StakePanel from "./StakePanel"; @@ -30,7 +30,7 @@ const CourtDetails: React.FC = () => {

- + @@ -60,11 +60,7 @@ interface IItem { id: string; } -const getCourtsPath = ( - node: CourtTreeQuery["court"], - id: string | undefined, - path: IItem[] = [] -): IItem[] | null => { +const getCourtsPath = (node: CourtTreeQuery["court"], id: string | undefined, path: IItem[] = []): IItem[] | null => { if (!node || !id) return null; if (node.id === id) { diff --git a/web/src/pages/Dashboard/Courts/CourtCard.tsx b/web/src/pages/Dashboard/Courts/CourtCard.tsx index 822d898a1..b7d385d57 100644 --- a/web/src/pages/Dashboard/Courts/CourtCard.tsx +++ b/web/src/pages/Dashboard/Courts/CourtCard.tsx @@ -33,7 +33,7 @@ const tooltipMsg = "The locked stake of incoherent jurors is redistributed as incentives for " + "the coherent jurors."; -const format = (value: bigint | undefined): string => (value !== undefined ? formatEther(value) : "0"); +export const format = (value: bigint | undefined): string => (value !== undefined ? formatEther(value) : "0"); interface ICourtCard { id: string;