Skip to content

Commit

Permalink
Merge pull request #1964 from zeitgeistpm/#1954-court-case-page
Browse files Browse the repository at this point in the history
#1954 Court
  • Loading branch information
yornaath authored Nov 17, 2023
2 parents 705abd7 + e81be3c commit 4a9f929
Show file tree
Hide file tree
Showing 69 changed files with 4,039 additions and 2,759 deletions.
2 changes: 1 addition & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ NEXT_PUBLIC_AVATAR_COLLECTION_ID="2e55d4bf2e85715b63-ZEITASTAGE"
NEXT_PUBLIC_SINGULAR_URL="https://singular-rmrk2-dev.vercel.app"
NEXT_PUBLIC_RMRK_INDEXER_API="https://gql2.rmrk.dev/v1/graphql"
NEXT_PUBLIC_IPFS_NODE="http://ipfs.zeitgeist.pm:5001"
NEXT_PUBLIC_RMRK_CHAIN_RPC_NODE="wss://kusama-node-staging.rmrk.link/"
NEXT_PUBLIC_RMRK_CHAIN_RPC_NODE="wss://kusama-node-staging.rmrk.link"
NEXT_PUBLIC_AVATAR_API_HOST="https://avatar-bsr.zeitgeist.pm/"

#enable in dev/staging to inspect react-query cache and query handling.
Expand Down
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ NEXT_PUBLIC_AVATAR_COLLECTION_ID="2e55d4bf2e85715b63-ZEITASTAGE"
NEXT_PUBLIC_SINGULAR_URL="https://singular-rmrk2-dev.vercel.app"
NEXT_PUBLIC_RMRK_INDEXER_API="https://gql2.rmrk.dev/v1/graphql"
NEXT_PUBLIC_IPFS_NODE="http://ipfs.zeitgeist.pm:5001"
NEXT_PUBLIC_RMRK_CHAIN_RPC_NODE="wss://kusama-node-staging.rmrk.link/"
NEXT_PUBLIC_RMRK_CHAIN_RPC_NODE="wss://kusama-node-staging.rmrk.link"
NEXT_PUBLIC_AVATAR_API_HOST="https://avatar-bsr.zeitgeist.pm/"
NEXT_PUBLIC_MIGRATION_IN_PROGRESS=false

Expand Down
4 changes: 2 additions & 2 deletions components/confirmation/ConfirmationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export const ConfirmationProvider = () => {
className="rounded-md px-4 py-2 text-gray-400"
onClick={() => dismiss(id)}
>
Cancel
{value.cancelLabel ?? "Cancel"}
</button>
<button
className="rounded-md bg-ztg-blue px-4 py-2 text-white"
onClick={() => confirm(id)}
>
Confirm
{value.confirmLabel ?? "Confirm"}
</button>
</div>
</Dialog.Panel>
Expand Down
56 changes: 56 additions & 0 deletions components/court/CourtAppealForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useQueryClient } from "@tanstack/react-query";
import { isRpcSdk } from "@zeitgeistpm/sdk";
import TransactionButton from "components/ui/TransactionButton";
import { courtCasesRootKey } from "lib/hooks/queries/court/useCourtCases";
import { useExtrinsic } from "lib/hooks/useExtrinsic";
import { useSdkv2 } from "lib/hooks/useSdkv2";

export const CourtAppealForm = ({ caseId }: { caseId: number }) => {
const [sdk, id] = useSdkv2();
const queryClient = useQueryClient();

const { send, isReady, isLoading, isBroadcasting } = useExtrinsic(
() => {
if (isRpcSdk(sdk)) {
return sdk.api.tx.court.appeal(caseId);
}
return undefined;
},
{
onSuccess: () => {
queryClient.invalidateQueries([id, courtCasesRootKey]);
},
},
);

return (
<div className="overflow-hidden rounded-xl shadow-lg">
<div className="center flex bg-fog-of-war py-3">
<h3 className="text-gray-300 text-opacity-50">Appeal Court</h3>
</div>

<div className="px-2 py-6 text-center">
<div className="mb-4">
<div className="mb-3 text-sm text-gray-700">
If you think the court has made a mistake, you can appeal the
decision. This will start a new round of voting.
</div>
</div>

<TransactionButton
disabled={!isReady || isLoading || isBroadcasting}
className={`relative h-[56px] ${
isLoading && "animate-pulse"
} !bg-orange-400`}
type="submit"
loading={isLoading || isBroadcasting}
onClick={() => send()}
>
<div>
<div className="center h-[20px] font-normal">Submit Appeal</div>
</div>
</TransactionButton>
</div>
</div>
);
};
248 changes: 248 additions & 0 deletions components/court/CourtCasesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { ZrmlCourtCourtInfo } from "@polkadot/types/lookup";
import { isInfinity } from "@zeitgeistpm/utility/dist/infinity";
import { blockDate } from "@zeitgeistpm/utility/dist/time";
import InfoPopover from "components/ui/InfoPopover";
import Skeleton from "components/ui/Skeleton";
import Table, { TableColumn, TableData } from "components/ui/Table";
import { useCaseMarketId } from "lib/hooks/queries/court/useCaseMarketId";
import { CourtCaseInfo } from "lib/hooks/queries/court/useCourtCase";
import { useCourtCases } from "lib/hooks/queries/court/useCourtCases";
import { useVoteDrawsForCase } from "lib/hooks/queries/court/useVoteDraws";
import { useMarket } from "lib/hooks/queries/useMarket";
import { useChainTime } from "lib/state/chaintime";
import { CourtStage, getCourtStage } from "lib/state/court/get-stage";
import { useWallet } from "lib/state/wallet";
import Link from "next/link";
import { useMemo } from "react";
import { AiOutlineEye } from "react-icons/ai";
import { LuVote } from "react-icons/lu";
import { courtStageCopy } from "./CourtStageTimer";

const columns: TableColumn[] = [
{
header: "#",
accessor: "id",
type: "text",
},
{
header: "Case",
accessor: "case",
type: "component",
},
{
header: "Status",
accessor: "status",
type: "component",
},
{
header: "Voting Ends",
accessor: "ends",
type: "text",
},
{
header: "",
accessor: "actions",
type: "component",
},
];

export const CourtCasesTable = () => {
const { data: cases } = useCourtCases();
const time = useChainTime();

cases?.sort((a, b) => {
if (b.case.status.type === "Reassigned") return -1;
return a.case.roundEnds.vote.toNumber() > b.case.roundEnds.vote.toNumber()
? 1
: 0;
});

const tableData: TableData[] | undefined = cases?.map((courtCase) => {
return {
id: `${courtCase.id}`,
case: <CaseNameForCaseId id={courtCase.id} />,
status: <CaseStatus courtCase={courtCase} />,
ends:
time &&
new Intl.DateTimeFormat("default", {
dateStyle: "medium",
timeStyle: "short",
}).format(blockDate(time, courtCase.case.roundEnds.vote.toNumber())),
actions: <CaseActions caseId={courtCase.id} courtCase={courtCase.case} />,
};
});

return (
<div className="relative">
<Table columns={columns} data={tableData} />
</div>
);
};

const CaseNameForCaseId = (props: { id: number }) => {
const { data: marketId } = useCaseMarketId(props.id);
const { data: market } = useMarket({ marketId: marketId! });
return (
<>
{market ? (
<div className="text-sm">{market?.question}</div>
) : (
<Skeleton />
)}
</>
);
};

const CaseStatus = ({ courtCase }: { courtCase: CourtCaseInfo }) => {
const { data: marketId } = useCaseMarketId(courtCase.id);
const { data: market } = useMarket({ marketId: marketId! });
const chainTime = useChainTime();

const stage = useMemo(() => {
if (market && chainTime) {
return getCourtStage(chainTime, market, courtCase.case);
}
}, [chainTime, market]);

const percentage =
stage && isInfinity(stage.remainingBlocks)
? 100
: stage
? ((stage.totalTime - stage.remainingBlocks) / stage.totalTime) * 100
: 0;

return (
<div className="">
{stage ? (
<>
<div className="mb-1 flex items-center gap-2">
<div className={`${caseStatusCopy[stage.type].color}`}>
{caseStatusCopy[stage.type].title}
</div>
<InfoPopover position="top">
{caseStatusCopy[stage.type].description}
</InfoPopover>
</div>

<div className="w-full">
<div className="h-1 w-full rounded-lg bg-gray-100">
<div
className={`h-full rounded-lg transition-all ${
courtStageCopy[stage.type].color
}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
</>
) : (
<Skeleton />
)}
</div>
);
};

const CaseActions = ({
caseId,
courtCase,
}: {
caseId: number;
courtCase: ZrmlCourtCourtInfo;
}) => {
const wallet = useWallet();

const { data: marketId } = useCaseMarketId(caseId);
const { data: market } = useMarket({ marketId: marketId! });
const chainTime = useChainTime();

const stage = useMemo(() => {
if (market && chainTime) {
return getCourtStage(chainTime, market, courtCase);
}
}, [chainTime, market]);

const { data: draws } = useVoteDrawsForCase(caseId);

const connectedParticipantDraw = draws?.find(
(draw) => draw.courtParticipant.toString() === wallet.realAddress,
);

const canVote = useMemo(() => {
return stage?.type === "vote" && connectedParticipantDraw?.vote.isDrawn;
}, [stage, connectedParticipantDraw]);

const canReveal = useMemo(() => {
return (
stage?.type === "aggregation" && connectedParticipantDraw?.vote.isSecret
);
}, [stage, connectedParticipantDraw]);

return (
<div className="flex w-full items-center justify-center">
<Link href={`/court/${caseId}`}>
<button
className={`
center line-clamp-1 gap-3 self-end rounded-full border-2 border-gray-300 px-5 py-1.5 text-xs hover:border-gray-400 disabled:opacity-50 md:min-w-[220px]
${canVote && "animate-pulse border-ztg-blue bg-ztg-blue text-white"}
${
canReveal &&
"animate-pulse border-purple-500 bg-purple-500 text-white"
}
`}
>
{canVote ? (
<>
<LuVote size={18} /> <span>Vote</span>
</>
) : canReveal ? (
<>
<AiOutlineEye size={18} /> <span>Reveal Vote</span>
</>
) : (
"View Case"
)}
</button>
</Link>
</div>
);
};

const caseStatusCopy: Record<
CourtStage["type"],
{
title: string;
description: string;
color: string;
}
> = {
"pre-vote": {
title: "Pre-Vote",
description: "Waiting for the vote period to start.",
color: "text-gray-400",
},
vote: {
title: "Vote",
description: "Case is now open for voting by jurors.",
color: "text-blue-400",
},
aggregation: {
title: "Aggregation",
description: "Votes can now be revealed by jurors.",
color: "text-purple-400",
},
appeal: {
title: "Appeal",
description: "Jurors can now appeal the voted outcome.",
color: "text-orange-400",
},
reassigned: {
title: "Reassigned",
description: "Case has been reassigned and winners paid out.",
color: "text-gray-400",
},
closed: {
title: "Closed",
description: "Case has been closed. Waiting to be reassigned.",
color: "text-gray-400",
},
};
Loading

0 comments on commit 4a9f929

Please sign in to comment.