Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add multi-round UI in interface #360

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/coordinator/scripts/uploadRoundMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface RoundMetadata {
registrationEndsAt: Date;
votingStartsAt: Date;
votingEndsAt: Date;
tallyFile: string;
}

interface IUploadMetadataProps {
Expand All @@ -29,9 +30,11 @@ function isValidDate(formattedDateStr: string) {
}

export async function uploadRoundMetadata({ data, name }: IUploadMetadataProps): Promise<string> {
// NOTICE! this is when you use vercel storage, if you're using another tool, please change this part.
const blob = await put(name, JSON.stringify(data), {
access: "public",
token: process.env.BLOB_READ_WRITE_TOKEN,
addRandomSuffix: false,
});

return blob.url;
Expand Down Expand Up @@ -139,13 +142,17 @@ export async function collectMetadata(): Promise<RoundMetadata> {

rl.close();

// NOTICE! this is when you use vercel blob storage, if you're using another tool, please change this part.
const vercelStoragePrefix = `https://${process.env.BLOB_READ_WRITE_TOKEN?.split("_")[3]}.public.blob.vercel-storage.com`;

return {
roundId,
description,
startsAt,
registrationEndsAt,
votingStartsAt,
votingEndsAt,
tallyFile: `${vercelStoragePrefix}/tally-${roundId}.json`,
};
}

Expand Down
19 changes: 6 additions & 13 deletions packages/interface/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,12 @@ NEXT_PUBLIC_WALLETCONNECT_ID="21fef48091f12692cad574a6f7753643"
# Event title for the round, just for display
NEXT_PUBLIC_EVENT_NAME="Devcon"

# Unique identifier for your applications and lists - your app will group attestations by this id
NEXT_PUBLIC_ROUND_ID="open-rpgf-1"
# Event title for the round, just for display
NEXT_PUBLIC_ROUND_ORGANIZER="PSE"
# Event description, just for display
NEXT_PUBLIC_EVENT_DESCRIPTION="Write a descripion about your community"

# Name of the token you want to allocate (only updates UI)
NEXT_PUBLIC_TOKEN_NAME="Votes"

# Voting periods
# Determine when users can register applications, admins review them, voters vote, and results are published
NEXT_PUBLIC_START_DATE=2024-01-01T00:00:00.000Z
NEXT_PUBLIC_REGISTRATION_END_DATE=2024-01-01T00:00:00.000Z
NEXT_PUBLIC_RESULTS_DATE=2024-01-01T00:00:00.000Z

# Collect user feedback. Is shown as a link when user has voted
NEXT_PUBLIC_FEEDBACK_URL=https://github.com/privacy-scaling-explorations/maci-platform/issues/new?title=Feedback

Expand Down Expand Up @@ -80,7 +72,8 @@ NEXT_PUBLIC_MACI_START_BLOCK=

NEXT_PUBLIC_MACI_SUBGRAPH_URL=

# URL with tally-{pollId}.json hosted
NEXT_PUBLIC_TALLY_URL=https://upblxu2duoxmkobt.public.blob.vercel-storage.com

NEXT_PUBLIC_ROUND_LOGO="round-logo.png"

NEXT_PUBLIC_START_DATE=2024-01-01T00:00:00.000Z
NEXT_PUBLIC_REGISTRATION_END_DATE=2024-01-01T00:00:00.000Z
NEXT_PUBLIC_RESULTS_DATE=2024-01-01T00:00:00.000Z
8 changes: 6 additions & 2 deletions packages/interface/src/components/AddedProjects.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useBallot } from "~/contexts/Ballot";
import { useProjectCount } from "~/features/projects/hooks/useProjects";

export const AddedProjects = (): JSX.Element => {
interface IAddedProjectsProps {
roundId: string;
}

export const AddedProjects = ({ roundId }: IAddedProjectsProps): JSX.Element => {
const { ballot } = useBallot();
const allocations = ballot.votes;
const { data: projectCount } = useProjectCount();
const { data: projectCount } = useProjectCount(roundId);

return (
<div className="border-b border-gray-200 py-2">
Expand Down
45 changes: 24 additions & 21 deletions packages/interface/src/components/BallotOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,43 @@ import Link from "next/link";

import { Heading } from "~/components/ui/Heading";
import { useBallot } from "~/contexts/Ballot";
import { useAppState } from "~/utils/state";
import { EAppState } from "~/utils/types";
import { useRoundState } from "~/utils/state";
import { ERoundState } from "~/utils/types";

import { AddedProjects } from "./AddedProjects";
import { VotingUsage } from "./VotingUsage";

interface IBallotOverviewProps {
roundId: string;
title?: string;
}

export const BallotOverview = ({ title = undefined }: IBallotOverviewProps): JSX.Element => {
export const BallotOverview = ({ title = undefined, roundId }: IBallotOverviewProps): JSX.Element => {
const { ballot } = useBallot();

const appState = useAppState();
const roundState = useRoundState(roundId);

return (
<div className="w-full">
<Link
href={
ballot.published && (appState === EAppState.TALLYING || appState === EAppState.RESULTS)
? "/ballot/confirmation"
: "/ballot"
}
>
{title && (
<Heading as="h3" size="3xl">
{title}
</Heading>
)}

<AddedProjects />
<Link
href={
ballot.published && (roundState === ERoundState.TALLYING || roundState === ERoundState.RESULTS)
? `/rounds/${roundId}/ballot/confirmation`
: `/rounds/${roundId}/ballot`
}
>
<div className="dark:bg-lightBlack my-8 flex-col items-center gap-2 rounded-lg bg-white p-5 uppercase shadow-lg dark:text-white">
<Heading as="h3" size="3xl">
{title && (
<Heading as="h3" size="3xl">
{title}
</Heading>
)}
</Heading>

<AddedProjects roundId={roundId} />

<VotingUsage />
</Link>
</div>
</div>
</Link>
);
};
61 changes: 30 additions & 31 deletions packages/interface/src/components/EligibilityDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,46 @@ import { ZKEdDSAEventTicketPCDPackage } from "@pcd/zk-eddsa-event-ticket-pcd";
import { zuAuthPopup } from "@pcd/zuauth";
import { GatekeeperTrait, getZupassGatekeeperData } from "maci-cli/sdk";
import { useRouter } from "next/router";
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useEffect, useMemo } from "react";
import { toast } from "sonner";
import { useAccount, useDisconnect } from "wagmi";

import { zupass, config } from "~/config";
import { useMaci } from "~/contexts/Maci";
import { useRound } from "~/contexts/Round";
import { useEthersSigner } from "~/hooks/useEthersSigner";
import { useAppState } from "~/utils/state";
import { EAppState, jsonPCD } from "~/utils/types";
import { useRoundState } from "~/utils/state";
import { ERoundState, jsonPCD } from "~/utils/types";

import type { EdDSAPublicKey } from "@pcd/eddsa-pcd";

import { Dialog } from "./ui/Dialog";

export const EligibilityDialog = (): JSX.Element | null => {
const signer = useEthersSigner();
interface IEligibilityDialogProps {
roundId?: string;
}

export const EligibilityDialog = ({ roundId = "" }: IEligibilityDialogProps): JSX.Element | null => {
const { address } = useAccount();
const { disconnect } = useDisconnect();
const { getRoundByRoundId } = useRound();

const [openDialog, setOpenDialog] = useState<boolean>(!!address);
const {
onSignup,
isEligibleToVote,
isRegistered,
initialVoiceCredits,
votingEndsAt,
gatekeeperTrait,
storeZupassProof,
} = useMaci();
const { onSignup, isEligibleToVote, isRegistered, initialVoiceCredits, gatekeeperTrait, storeZupassProof } =
useMaci();
const router = useRouter();

const appState = useAppState();
const roundState = useRoundState(roundId);

const votingEndsAt = useMemo(() => {
const round = getRoundByRoundId(roundId);
return round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date();
}, [roundId, getRoundByRoundId]);

const onError = useCallback(() => toast.error("Signup error"), []);

const signer = useEthersSigner();

const handleSignup = useCallback(async () => {
await onSignup(onError);
setOpenDialog(false);
Expand Down Expand Up @@ -87,15 +92,11 @@ export const EligibilityDialog = (): JSX.Element | null => {
disconnect();
}, [disconnect]);

const handleGoToProjects = useCallback(() => {
router.push("/projects");
}, [router]);

const handleGoToCreateApp = useCallback(() => {
router.push("/applications/new");
}, [router]);

if (appState === EAppState.APPLICATION) {
if (roundState === ERoundState.APPLICATION) {
return (
<Dialog
button="secondary"
Expand All @@ -114,12 +115,10 @@ export const EligibilityDialog = (): JSX.Element | null => {
);
}

if (appState === EAppState.VOTING && isRegistered) {
if (roundState === ERoundState.VOTING && isRegistered) {
return (
<Dialog
button="secondary"
buttonAction={handleGoToProjects}
buttonName="See all projects"
description={
<div className="flex flex-col gap-4">
<p>You have {initialVoiceCredits} voice credits to vote with.</p>
Expand All @@ -134,21 +133,21 @@ export const EligibilityDialog = (): JSX.Element | null => {
}
isOpen={openDialog}
size="sm"
title="You're all set to vote"
title="You're all set to vote for rounds!"
onOpenChange={handleCloseDialog}
/>
);
}

if (appState === EAppState.VOTING && !isRegistered && isEligibleToVote) {
if (roundState === ERoundState.VOTING && !isRegistered && isEligibleToVote) {
return (
<Dialog
button="secondary"
buttonAction={handleSignup}
buttonName="Join voting round"
buttonName="Join voting rounds"
description={
<div className="flex flex-col gap-6">
<p>Next, you will need to join the voting round.</p>
<p>Next, you will need to register to the event to join the voting rounds.</p>

<i>
<span>Learn more about this process </span>
Expand All @@ -169,7 +168,7 @@ export const EligibilityDialog = (): JSX.Element | null => {
);
}

if (appState === EAppState.VOTING && !isEligibleToVote && gatekeeperTrait === GatekeeperTrait.Zupass) {
if (roundState === ERoundState.VOTING && !isEligibleToVote && gatekeeperTrait === GatekeeperTrait.Zupass) {
return (
<Dialog
button="secondary"
Expand All @@ -184,13 +183,13 @@ export const EligibilityDialog = (): JSX.Element | null => {
);
}

if (appState === EAppState.VOTING && !isEligibleToVote) {
if (roundState === ERoundState.VOTING && !isEligibleToVote) {
return (
<Dialog
button="secondary"
buttonAction={handleDisconnect}
buttonName="Disconnect"
description="To participate in this round, you must be in the voter's registry. Contact the round organizers to get access as a voter."
description="To participate in the event, you must be in the voter's registry. Contact the round organizers to get access as a voter."
isOpen={openDialog}
size="sm"
title="Sorry, this account does not have the credentials to be verified."
Expand All @@ -199,7 +198,7 @@ export const EligibilityDialog = (): JSX.Element | null => {
);
}

if (appState === EAppState.TALLYING) {
if (roundState === ERoundState.TALLYING) {
return (
<Dialog
description="The result is under tallying, please come back to check the result later."
Expand Down
22 changes: 14 additions & 8 deletions packages/interface/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { type ComponentPropsWithRef, useState, useCallback } from "react";
import { type ComponentPropsWithRef, useState, useCallback, useMemo } from "react";

import { useBallot } from "~/contexts/Ballot";
import { useAppState } from "~/utils/state";
import { EAppState } from "~/utils/types";
import { useRoundState } from "~/utils/state";
import { ERoundState } from "~/utils/types";

import { ConnectButton } from "./ConnectButton";
import { IconButton } from "./ui/Button";
Expand Down Expand Up @@ -52,19 +52,23 @@ interface INavLink {

interface IHeaderProps {
navLinks: INavLink[];
roundId?: string;
}

const Header = ({ navLinks }: IHeaderProps) => {
const Header = ({ navLinks, roundId = "" }: IHeaderProps) => {
const { asPath } = useRouter();
const [isOpen, setOpen] = useState(false);
const { ballot } = useBallot();
const appState = useAppState();
const roundState = useRoundState(roundId);
const { theme, setTheme } = useTheme();

const handleChangeTheme = useCallback(() => {
setTheme(theme === "light" ? "dark" : "light");
}, [theme, setTheme]);

// the URI of round index page looks like: /rounds/:roundId, without anything else, which is the reason why the length is 3
const isRoundIndexPage = useMemo(() => asPath.includes("rounds") && asPath.split("/").length === 3, [asPath]);

return (
<header className="dark:border-lighterBlack dark:bg-lightBlack relative z-[100] border-b border-gray-200 bg-white dark:text-white">
<div className="container mx-auto flex h-[72px] max-w-screen-2xl items-center px-2">
Expand All @@ -85,12 +89,14 @@ const Header = ({ navLinks }: IHeaderProps) => {

<div className="hidden h-full items-center gap-4 overflow-x-auto uppercase md:flex">
{navLinks.map((link) => {
const pageName = `/${link.href.split("/")[1]}`;
const isActive =
asPath.includes(link.children.toLowerCase()) || (link.children === "Projects" && isRoundIndexPage);

return (
<NavLink key={link.href} href={link.href} isActive={asPath.startsWith(pageName)}>
<NavLink key={link.href} href={link.href} isActive={isActive}>
{link.children}

{appState === EAppState.VOTING && pageName === "/ballot" && ballot.votes.length > 0 && (
{roundState === ERoundState.VOTING && link.href.includes("/ballot") && ballot.votes.length > 0 && (
<div className="ml-2 h-5 w-5 rounded-full border-2 border-blue-400 bg-blue-50 text-center text-sm leading-4 text-blue-400">
{ballot.votes.length}
</div>
Expand Down
Loading
Loading