From 7c509d5e1cf301f28612895122a8e367161ea05c Mon Sep 17 00:00:00 2001 From: yu-zhen Date: Thu, 15 Aug 2024 00:55:29 +0800 Subject: [PATCH 1/2] feat: add v2 frontend UI design --- packages/interface/.env.example | 12 +-- .../src/components/AddedProjects.tsx | 8 +- .../src/components/BallotOverview.tsx | 45 +++++----- .../src/components/EligibilityDialog.tsx | 41 +++++---- packages/interface/src/components/Header.tsx | 22 +++-- packages/interface/src/components/Info.tsx | 76 ++++++++-------- .../interface/src/components/InfoCard.tsx | 18 +--- .../interface/src/components/JoinButton.tsx | 15 +--- .../interface/src/components/RoundInfo.tsx | 8 +- .../src/components/ui/Navigation.tsx | 5 +- packages/interface/src/config.ts | 11 +-- packages/interface/src/contexts/Maci.tsx | 2 +- packages/interface/src/contexts/Round.tsx | 44 ++++++++++ packages/interface/src/contexts/types.ts | 10 +++ packages/interface/src/env.js | 28 +----- .../components/ApplicationForm.tsx | 20 +++-- .../components/ApplicationsToApprove.tsx | 33 ++++--- .../applications/components/ReviewBar.tsx | 9 +- .../applications/hooks/useApplications.ts | 4 +- .../hooks/useApproveApplication.ts | 9 +- .../hooks/useApprovedApplications.ts | 4 +- .../hooks/useCreateApplication.ts | 5 +- .../ballot/components/BallotConfirmation.tsx | 27 ++++-- .../ballot/components/SubmitBallotButton.tsx | 8 +- .../src/features/filter/types/index.ts | 1 + .../{signup => home}/components/FaqItem.tsx | 0 .../{signup => home}/components/FaqList.tsx | 0 .../src/features/{signup => home}/types.ts | 0 .../projects/components/ProjectAwarded.tsx | 5 +- .../projects/components/ProjectDetails.tsx | 12 +-- .../projects/components/ProjectItem.tsx | 12 +-- .../projects/components/ProjectsResults.tsx | 19 ++-- .../features/projects/hooks/useProjects.ts | 12 +-- .../components/Projects.tsx | 30 ++++--- .../features/rounds/components/RoundItem.tsx | 65 ++++++++++++++ .../features/rounds/components/RoundsList.tsx | 16 ++++ .../src/features/rounds/types/index.ts | 12 +++ .../voters/components/ApproveVoters.tsx | 31 +++---- .../features/voters/hooks/useApproveVoters.ts | 5 +- .../interface/src/features/voters/types.tsx | 6 ++ packages/interface/src/hooks/useResults.ts | 26 +++--- .../interface/src/layouts/AdminLayout.tsx | 13 +-- packages/interface/src/layouts/BaseLayout.tsx | 31 ++----- .../interface/src/layouts/DefaultLayout.tsx | 70 +++++++++------ packages/interface/src/layouts/types.ts | 30 +++++++ .../src/pages/applications/index.tsx | 10 --- .../interface/src/pages/coordinator/index.tsx | 5 ++ packages/interface/src/pages/index.tsx | 62 ++++++++++--- packages/interface/src/pages/info/index.tsx | 88 ------------------- .../interface/src/pages/projects/index.tsx | 10 --- .../interface/src/pages/projects/results.tsx | 10 --- .../[roundId]}/[projectId]/Project.tsx | 21 ++--- .../[roundId]}/[projectId]/index.tsx | 0 .../[roundId]}/applications/confirmation.tsx | 16 ++-- .../rounds/[roundId]/applications/index.tsx | 21 +++++ .../[roundId]}/applications/new.tsx | 16 ++-- .../[roundId]}/ballot/confirmation.tsx | 12 ++- .../{ => rounds/[roundId]}/ballot/index.tsx | 47 +++++++--- .../src/pages/rounds/[roundId]/index.tsx | 21 +++++ .../{ => rounds/[roundId]}/stats/index.tsx | 43 ++++++--- packages/interface/src/pages/signup/index.tsx | 70 --------------- packages/interface/src/providers/index.tsx | 11 ++- .../src/server/api/routers/applications.ts | 28 +++--- .../src/server/api/routers/projects.ts | 17 ++-- .../src/server/api/routers/results.ts | 53 ++++++----- .../src/server/api/routers/voters.ts | 5 +- .../interface/src/utils/fetchAttestations.ts | 6 +- .../utils/fetchAttestationsWithoutCache.ts | 10 +-- packages/interface/src/utils/state.ts | 32 ++++--- packages/interface/src/utils/time.ts | 14 +++ packages/interface/src/utils/types.ts | 3 +- 71 files changed, 826 insertions(+), 665 deletions(-) create mode 100644 packages/interface/src/contexts/Round.tsx rename packages/interface/src/features/{signup => home}/components/FaqItem.tsx (100%) rename packages/interface/src/features/{signup => home}/components/FaqList.tsx (100%) rename packages/interface/src/features/{signup => home}/types.ts (100%) rename packages/interface/src/features/{projects => rounds}/components/Projects.tsx (75%) create mode 100644 packages/interface/src/features/rounds/components/RoundItem.tsx create mode 100644 packages/interface/src/features/rounds/components/RoundsList.tsx create mode 100644 packages/interface/src/features/rounds/types/index.ts create mode 100644 packages/interface/src/layouts/types.ts delete mode 100644 packages/interface/src/pages/applications/index.tsx create mode 100644 packages/interface/src/pages/coordinator/index.tsx delete mode 100644 packages/interface/src/pages/info/index.tsx delete mode 100644 packages/interface/src/pages/projects/index.tsx delete mode 100644 packages/interface/src/pages/projects/results.tsx rename packages/interface/src/pages/{projects => rounds/[roundId]}/[projectId]/Project.tsx (60%) rename packages/interface/src/pages/{projects => rounds/[roundId]}/[projectId]/index.tsx (100%) rename packages/interface/src/pages/{ => rounds/[roundId]}/applications/confirmation.tsx (80%) create mode 100644 packages/interface/src/pages/rounds/[roundId]/applications/index.tsx rename packages/interface/src/pages/{ => rounds/[roundId]}/applications/new.tsx (79%) rename packages/interface/src/pages/{ => rounds/[roundId]}/ballot/confirmation.tsx (72%) rename packages/interface/src/pages/{ => rounds/[roundId]}/ballot/index.tsx (77%) create mode 100644 packages/interface/src/pages/rounds/[roundId]/index.tsx rename packages/interface/src/pages/{ => rounds/[roundId]}/stats/index.tsx (73%) delete mode 100644 packages/interface/src/pages/signup/index.tsx diff --git a/packages/interface/.env.example b/packages/interface/.env.example index 616bf8b1..d769c805 100644 --- a/packages/interface/.env.example +++ b/packages/interface/.env.example @@ -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 diff --git a/packages/interface/src/components/AddedProjects.tsx b/packages/interface/src/components/AddedProjects.tsx index bb1c2ac8..2be7e346 100644 --- a/packages/interface/src/components/AddedProjects.tsx +++ b/packages/interface/src/components/AddedProjects.tsx @@ -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 (
diff --git a/packages/interface/src/components/BallotOverview.tsx b/packages/interface/src/components/BallotOverview.tsx index bac24c4b..3e1452f8 100644 --- a/packages/interface/src/components/BallotOverview.tsx +++ b/packages/interface/src/components/BallotOverview.tsx @@ -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 ( -
- - {title && ( - - {title} - - )} - - + +
+ + {title && ( + + {title} + + )} + + + - -
+
+ ); }; diff --git a/packages/interface/src/components/EligibilityDialog.tsx b/packages/interface/src/components/EligibilityDialog.tsx index fe5ec20a..b27cd99b 100644 --- a/packages/interface/src/components/EligibilityDialog.tsx +++ b/packages/interface/src/components/EligibilityDialog.tsx @@ -11,15 +11,18 @@ import { useAccount, useDisconnect } from "wagmi"; import { zupass, config } from "~/config"; import { useMaci } from "~/contexts/Maci"; 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(); @@ -35,10 +38,12 @@ export const EligibilityDialog = (): JSX.Element | null => { } = useMaci(); const router = useRouter(); - const appState = useAppState(); + const roundState = useRoundState(roundId); const onError = useCallback(() => toast.error("Signup error"), []); + const signer = useEthersSigner(); + const handleSignup = useCallback(async () => { await onSignup(onError); setOpenDialog(false); @@ -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 ( { ); } - if (appState === EAppState.VOTING && isRegistered) { + if (roundState === ERoundState.VOTING && isRegistered) { return (

You have {initialVoiceCredits} voice credits to vote with.

@@ -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 ( -

Next, you will need to join the voting round.

+

Next, you will need to register to the event to join the voting rounds.

Learn more about this process @@ -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 ( { ); } - if (appState === EAppState.VOTING && !isEligibleToVote) { + if (roundState === ERoundState.VOTING && !isEligibleToVote) { return ( { ); } - if (appState === EAppState.TALLYING) { + if (roundState === ERoundState.TALLYING) { return ( { +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 (
@@ -85,12 +89,14 @@ const Header = ({ navLinks }: IHeaderProps) => {
{navLinks.map((link) => { - const pageName = `/${link.href.split("/")[1]}`; + const isActive = + asPath.includes(link.children.toLowerCase()) || (link.children === "Projects" && isRoundIndexPage); + return ( - + {link.children} - {appState === EAppState.VOTING && pageName === "/ballot" && ballot.votes.length > 0 && ( + {roundState === ERoundState.VOTING && link.href.includes("/ballot") && ballot.votes.length > 0 && (
{ballot.votes.length}
diff --git a/packages/interface/src/components/Info.tsx b/packages/interface/src/components/Info.tsx index fb952103..46ecd5e2 100644 --- a/packages/interface/src/components/Info.tsx +++ b/packages/interface/src/components/Info.tsx @@ -2,10 +2,9 @@ import { useRouter } from "next/router"; import { tv } from "tailwind-variants"; import { createComponent } from "~/components/ui"; -import { config } from "~/config"; -import { useMaci } from "~/contexts/Maci"; -import { useAppState } from "~/utils/state"; -import { EInfoCardState, EAppState } from "~/utils/types"; +import { useRound } from "~/contexts/Round"; +import { useRoundState } from "~/utils/state"; +import { EInfoCardState, ERoundState } from "~/utils/types"; import { BallotOverview } from "./BallotOverview"; import { InfoCard } from "./InfoCard"; @@ -25,8 +24,9 @@ const InfoContainer = createComponent( }), ); -interface InfoProps { +interface IInfoProps { size: string; + roundId: string; showRoundInfo?: boolean; showAppState?: boolean; showBallot?: boolean; @@ -34,73 +34,71 @@ interface InfoProps { export const Info = ({ size, + roundId, showRoundInfo = false, showAppState = false, showBallot = false, -}: InfoProps): JSX.Element => { - const { votingEndsAt } = useMaci(); - const appState = useAppState(); +}: IInfoProps): JSX.Element => { + const roundState = useRoundState(roundId); + const { getRound } = useRound(); + const round = getRound(roundId); const { asPath } = useRouter(); const steps = [ { label: "application", - state: EAppState.APPLICATION, - start: config.startsAt, - end: config.registrationEndsAt, + state: ERoundState.APPLICATION, + start: round?.startsAt ? new Date(round.startsAt) : new Date(), + end: round?.registrationEndsAt ? new Date(round.registrationEndsAt) : new Date(), }, { label: "voting", - state: EAppState.VOTING, - start: config.registrationEndsAt, - end: votingEndsAt, + state: ERoundState.VOTING, + start: round?.registrationEndsAt ? new Date(round.registrationEndsAt) : new Date(), + end: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(), }, { label: "tallying", - state: EAppState.TALLYING, - start: votingEndsAt, - end: config.resultsAt, + state: ERoundState.TALLYING, + start: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(), + end: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(), }, { label: "results", - state: EAppState.RESULTS, - start: config.resultsAt, - end: config.resultsAt, + state: ERoundState.RESULTS, + start: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(), + end: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(), }, ]; return (
- {showRoundInfo && } + {showRoundInfo && } - {showBallot && } + {showBallot && } - {showRoundInfo && appState === EAppState.VOTING && } + {showRoundInfo && roundState === ERoundState.VOTING && } {showAppState && - steps.map( - (step) => - step.start && - step.end && ( - - ), - )} + steps.map((step) => ( + + ))}
); }; -function defineState({ state, appState }: { state: EAppState; appState: EAppState }): EInfoCardState { - const statesOrder = [EAppState.APPLICATION, EAppState.VOTING, EAppState.TALLYING, EAppState.RESULTS]; +function defineState({ state, roundState }: { state: ERoundState; roundState: ERoundState }): EInfoCardState { + const statesOrder = [ERoundState.APPLICATION, ERoundState.VOTING, ERoundState.TALLYING, ERoundState.RESULTS]; const currentStateOrder = statesOrder.indexOf(state); - const appStateOrder = statesOrder.indexOf(appState); + const appStateOrder = statesOrder.indexOf(roundState); if (currentStateOrder < appStateOrder) { return EInfoCardState.PASSED; diff --git a/packages/interface/src/components/InfoCard.tsx b/packages/interface/src/components/InfoCard.tsx index a5adb23b..871affae 100644 --- a/packages/interface/src/components/InfoCard.tsx +++ b/packages/interface/src/components/InfoCard.tsx @@ -1,8 +1,8 @@ -import { format } from "date-fns"; import Image from "next/image"; import { tv } from "tailwind-variants"; import { createComponent } from "~/components/ui"; +import { formatPeriod } from "~/utils/time"; import { EInfoCardState } from "~/utils/types"; const InfoCardContainer = createComponent( @@ -45,20 +45,6 @@ export const InfoCard = ({ state, title, start, end }: InfoCardProps): JSX.Eleme )}
-

{formatDateString({ start, end })}

+

{formatPeriod({ start, end })}

); - -function formatDateString({ start, end }: { start: Date; end: Date }): string { - const fullFormat = "d MMM yyyy"; - - if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) { - return `${start.getDate()} - ${format(end, fullFormat)}`; - } - - if (start.getFullYear() === end.getFullYear()) { - return `${format(start, "d MMM")} - ${format(end, fullFormat)}`; - } - - return `${format(start, fullFormat)} - ${format(end, fullFormat)}`; -} diff --git a/packages/interface/src/components/JoinButton.tsx b/packages/interface/src/components/JoinButton.tsx index cddb616c..b488a347 100644 --- a/packages/interface/src/components/JoinButton.tsx +++ b/packages/interface/src/components/JoinButton.tsx @@ -2,35 +2,24 @@ import { useCallback } from "react"; import { toast } from "sonner"; import { useMaci } from "~/contexts/Maci"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; import { Button } from "./ui/Button"; export const JoinButton = (): JSX.Element => { const { isLoading, isRegistered, isEligibleToVote, onSignup } = useMaci(); - const appState = useAppState(); const onError = useCallback(() => toast.error("Signup error"), []); const handleSignup = useCallback(() => onSignup(onError), [onSignup, onError]); return (
- {appState === EAppState.VOTING && !isEligibleToVote && ( - - )} + {!isEligibleToVote && } - {appState === EAppState.VOTING && isEligibleToVote && !isRegistered && ( + {isEligibleToVote && !isRegistered && ( )} - - {appState === EAppState.TALLYING && ( - - )} - - {appState === EAppState.RESULTS && }
); }; diff --git a/packages/interface/src/components/RoundInfo.tsx b/packages/interface/src/components/RoundInfo.tsx index 75c9f06f..6281c17b 100644 --- a/packages/interface/src/components/RoundInfo.tsx +++ b/packages/interface/src/components/RoundInfo.tsx @@ -3,7 +3,11 @@ import Image from "next/image"; import { Heading } from "~/components/ui/Heading"; import { config } from "~/config"; -export const RoundInfo = (): JSX.Element => ( +interface IRoundInfoProps { + roundId: string; +} + +export const RoundInfo = ({ roundId }: IRoundInfoProps): JSX.Element => (

Round

@@ -11,7 +15,7 @@ export const RoundInfo = (): JSX.Element => ( {config.roundLogo && round logo} - {config.roundId} + {roundId}
diff --git a/packages/interface/src/components/ui/Navigation.tsx b/packages/interface/src/components/ui/Navigation.tsx index 6f50c170..e2999649 100644 --- a/packages/interface/src/components/ui/Navigation.tsx +++ b/packages/interface/src/components/ui/Navigation.tsx @@ -2,12 +2,13 @@ import Link from "next/link"; interface INavigationProps { projectName: string; + roundId: string; } -export const Navigation = ({ projectName }: INavigationProps): JSX.Element => ( +export const Navigation = ({ projectName, roundId }: INavigationProps): JSX.Element => (
- Projects + Projects {">"} diff --git a/packages/interface/src/config.ts b/packages/interface/src/config.ts index dd0b05c3..c93d938d 100644 --- a/packages/interface/src/config.ts +++ b/packages/interface/src/config.ts @@ -7,8 +7,6 @@ export const metadata = { image: "/api/og", }; -const parseDate = (env?: string) => (env ? new Date(env) : undefined); - // URLs for the EAS GraphQL endpoint for each chain const easScanUrl = { ethereum: "https://easscan.org/graphql", @@ -76,13 +74,9 @@ export const config = { pageSize: 3 * 4, // TODO: temp solution until we come up with solid one // https://github.com/privacy-scaling-explorations/maci-platform/issues/31 - voteLimit: 50, - startsAt: parseDate(process.env.NEXT_PUBLIC_START_DATE), - registrationEndsAt: parseDate(process.env.NEXT_PUBLIC_REGISTRATION_END_DATE), - resultsAt: parseDate(process.env.NEXT_PUBLIC_RESULTS_DATE), tokenName: process.env.NEXT_PUBLIC_TOKEN_NAME!, - eventName: process.env.NEXT_PUBLIC_EVENT_NAME ?? "MACI-PLATFORM", - roundId: process.env.NEXT_PUBLIC_ROUND_ID!, + eventName: process.env.NEXT_PUBLIC_EVENT_NAME ?? "Add your event name", + eventDescription: process.env.NEXT_PUBLIC_EVENT_DESCRIPTION ?? "Add your event description", admin: (process.env.NEXT_PUBLIC_ADMIN_ADDRESS ?? "") as `0x${string}`, network: wagmiChains[process.env.NEXT_PUBLIC_CHAIN_NAME as keyof typeof wagmiChains], maciAddress: process.env.NEXT_PUBLIC_MACI_ADDRESS, @@ -101,7 +95,6 @@ export const theme = { export const eas = { url: easScanUrl[process.env.NEXT_PUBLIC_CHAIN_NAME as keyof typeof easScanUrl], - attesterAddress: process.env.NEXT_PUBLIC_APPROVED_APPLICATIONS_ATTESTER ?? "", contracts: { eas: easContractAddresses[process.env.NEXT_PUBLIC_CHAIN_NAME as keyof typeof easContractAddresses], diff --git a/packages/interface/src/contexts/Maci.tsx b/packages/interface/src/contexts/Maci.tsx index 89c7ba9d..050292fd 100644 --- a/packages/interface/src/contexts/Maci.tsx +++ b/packages/interface/src/contexts/Maci.tsx @@ -263,7 +263,7 @@ export const MaciProvider: React.FC = ({ children }: MaciProv () => pollData && pollData.duration !== 0 ? new Date(Number(pollData.deployTime) * 1000 + Number(pollData.duration) * 1000) - : config.resultsAt, + : undefined, [pollData?.deployTime, pollData?.duration], ); diff --git a/packages/interface/src/contexts/Round.tsx b/packages/interface/src/contexts/Round.tsx new file mode 100644 index 00000000..a5415337 --- /dev/null +++ b/packages/interface/src/contexts/Round.tsx @@ -0,0 +1,44 @@ +import React, { createContext, useContext, useMemo, useCallback } from "react"; + +import type { RoundContextType, RoundProviderProps } from "./types"; +import type { Round } from "~/features/rounds/types"; + +export const RoundContext = createContext(undefined); + +export const RoundProvider: React.FC = ({ children }: RoundProviderProps) => { + const rounds = [ + { + roundId: "open-rpgf-1", + description: "This is the description of this round, please add your own description.", + startsAt: 1723477832000, + registrationEndsAt: 1723487832000, + votingEndsAt: 1724009826000, + tallyURL: "https://upblxu2duoxmkobt.public.blob.vercel-storage.com/tally.json", + }, + ]; + + const getRound = useCallback( + (roundId: string): Round | undefined => rounds.find((round) => round.roundId === roundId), + [rounds], + ); + + const value = useMemo( + () => ({ + rounds, + getRound, + }), + [rounds, getRound], + ); + + return {children}; +}; + +export const useRound = (): RoundContextType => { + const roundContext = useContext(RoundContext); + + if (!roundContext) { + throw new Error("Should use context inside provider."); + } + + return roundContext; +}; diff --git a/packages/interface/src/contexts/types.ts b/packages/interface/src/contexts/types.ts index 8ceb3ad8..6a84cb09 100644 --- a/packages/interface/src/contexts/types.ts +++ b/packages/interface/src/contexts/types.ts @@ -4,6 +4,7 @@ import { type ReactNode } from "react"; import type { PCD } from "@pcd/pcd-types"; import type { Ballot, Vote } from "~/features/ballot/types"; +import type { Round } from "~/features/rounds/types"; export interface IVoteArgs { voteOptionIndex: bigint; @@ -51,3 +52,12 @@ export interface BallotContextType { export interface BallotProviderProps { children: ReactNode; } + +export interface RoundContextType { + rounds: Round[]; + getRound: (roundId: string) => Round | undefined; +} + +export interface RoundProviderProps { + children: ReactNode; +} diff --git a/packages/interface/src/env.js b/packages/interface/src/env.js index cf7d3a20..46adb175 100644 --- a/packages/interface/src/env.js +++ b/packages/interface/src/env.js @@ -35,27 +35,12 @@ module.exports = createEnv({ NEXT_PUBLIC_FEEDBACK_URL: z.string().default("#"), // EAS Schemas - NEXT_PUBLIC_APPROVED_APPLICATIONS_SCHEMA: z - .string() - .default("0xebbf697d5d3ca4b53579917ffc3597fb8d1a85b8c6ca10ec10039709903b9277"), - NEXT_PUBLIC_APPROVED_APPLICATIONS_ATTESTER: z.string().default("0x621477dBA416E12df7FF0d48E14c4D20DC85D7D9"), - NEXT_PUBLIC_APPLICATIONS_SCHEMA: z - .string() - .default("0x76e98cce95f3ba992c2ee25cef25f756495147608a3da3aa2e5ca43109fe77cc"), - NEXT_PUBLIC_BADGEHOLDER_SCHEMA: z - .string() - .default("0xfdcfdad2dbe7489e0ce56b260348b7f14e8365a8a325aef9834818c00d46b31b"), - NEXT_PUBLIC_BADGEHOLDER_ATTESTER: z.string().default("0x621477dBA416E12df7FF0d48E14c4D20DC85D7D9"), - NEXT_PUBLIC_PROFILE_SCHEMA: z - .string() - .default("0xac4c92fc5c7babed88f78a917cdbcdc1c496a8f4ab2d5b2ec29402736b2cf929"), - NEXT_PUBLIC_ADMIN_ADDRESS: z.string().startsWith("0x"), NEXT_PUBLIC_APPROVAL_SCHEMA: z.string().startsWith("0x"), NEXT_PUBLIC_METADATA_SCHEMA: z.string().startsWith("0x"), - NEXT_PUBLIC_EVENT_NAME: z.string().optional(), - NEXT_PUBLIC_ROUND_ID: z.string(), + NEXT_PUBLIC_EVENT_NAME: z.string().default("Add your event name"), + NEXT_PUBLIC_EVENT_DESCRIPTION: z.string().default("Add your event description"), NEXT_PUBLIC_WALLETCONNECT_ID: z.string().optional(), NEXT_PUBLIC_ALCHEMY_ID: z.string().optional(), @@ -82,13 +67,6 @@ module.exports = createEnv({ NEXT_PUBLIC_FEEDBACK_URL: process.env.NEXT_PUBLIC_FEEDBACK_URL, - NEXT_PUBLIC_APPROVED_APPLICATIONS_SCHEMA: process.env.NEXT_PUBLIC_APPROVED_APPLICATIONS_SCHEMA, - NEXT_PUBLIC_APPROVED_APPLICATIONS_ATTESTER: process.env.NEXT_PUBLIC_APPROVED_APPLICATIONS_ATTESTER, - NEXT_PUBLIC_APPLICATIONS_SCHEMA: process.env.NEXT_PUBLIC_APPLICATIONS_SCHEMA, - NEXT_PUBLIC_BADGEHOLDER_SCHEMA: process.env.NEXT_PUBLIC_BADGEHOLDER_SCHEMA, - NEXT_PUBLIC_BADGEHOLDER_ATTESTER: process.env.NEXT_PUBLIC_BADGEHOLDER_ATTESTER, - NEXT_PUBLIC_PROFILE_SCHEMA: process.env.NEXT_PUBLIC_PROFILE_SCHEMA, - NEXT_PUBLIC_WALLETCONNECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_ID, NEXT_PUBLIC_ALCHEMY_ID: process.env.NEXT_PUBLIC_ALCHEMY_ID, @@ -97,7 +75,7 @@ module.exports = createEnv({ NEXT_PUBLIC_METADATA_SCHEMA: process.env.NEXT_PUBLIC_METADATA_SCHEMA, NEXT_PUBLIC_EVENT_NAME: process.env.NEXT_PUBLIC_EVENT_NAME, - NEXT_PUBLIC_ROUND_ID: process.env.NEXT_PUBLIC_ROUND_ID, + NEXT_PUBLIC_EVENT_DESCRIPTION: process.env.NEXT_PUBLIC_EVENT_DESCRIPTION, NEXT_PUBLIC_MACI_ADDRESS: process.env.NEXT_PUBLIC_MACI_ADDRESS, NEXT_PUBLIC_MACI_START_BLOCK: process.env.NEXT_PUBLIC_MACI_START_BLOCK, diff --git a/packages/interface/src/features/applications/components/ApplicationForm.tsx b/packages/interface/src/features/applications/components/ApplicationForm.tsx index 70b6bcff..58b12b12 100644 --- a/packages/interface/src/features/applications/components/ApplicationForm.tsx +++ b/packages/interface/src/features/applications/components/ApplicationForm.tsx @@ -11,14 +11,18 @@ import { Input } from "~/components/ui/Input"; import { useIsCorrectNetwork } from "~/hooks/useIsCorrectNetwork"; import { useCreateApplication } from "../hooks/useCreateApplication"; -import { ApplicationSchema, contributionTypes, fundingSourceTypes } from "../types"; +import { ApplicationSchema, contributionTypes, fundingSourceTypes, type Application } from "../types"; import { ApplicationButtons, EApplicationStep } from "./ApplicationButtons"; import { ApplicationSteps } from "./ApplicationSteps"; import { ImpactTags } from "./ImpactTags"; import { ReviewApplicationDetails } from "./ReviewApplicationDetails"; -export const ApplicationForm = (): JSX.Element => { +interface IApplicationFormProps { + roundId: string; +} + +export const ApplicationForm = ({ roundId }: IApplicationFormProps): JSX.Element => { const clearDraft = useLocalStorage("application-draft")[2]; const { isCorrectNetwork, correctNetwork } = useIsCorrectNetwork(); @@ -60,8 +64,16 @@ export const ApplicationForm = (): JSX.Element => { toast.error("Application create error", { description: err.reason ?? err.data?.message, }), + roundId, }); + const handleSubmit = useCallback( + (application: Application) => { + create.mutate(application); + }, + [create], + ); + const { error: createError } = create; return ( @@ -73,9 +85,7 @@ export const ApplicationForm = (): JSX.Element => { payoutAddress: address, }} schema={ApplicationSchema} - onSubmit={(application) => { - create.mutate(application); - }} + onSubmit={handleSubmit} > { - const applications = useApplications(); - const approved = useApprovedApplications(); - const approve = useApproveApplication(); +interface IApplicationsToApproveProps { + roundId: string; +} + +export const ApplicationsToApprove = ({ roundId }: IApplicationsToApproveProps): JSX.Element => { + const applications = useApplications(roundId); + const approved = useApprovedApplications(roundId); + const approve = useApproveApplication({ roundId }); const [refetchedData, setRefetchedData] = useState(); const approvedById = useMemo( @@ -39,7 +43,7 @@ export const ApplicationsToApprove = (): JSX.Element => { useEffect(() => { const fetchData = async () => { - const ret = await fetchApprovedApplications(); + const ret = await fetchApprovedApplications(roundId); setRefetchedData(ret); }; @@ -53,6 +57,13 @@ export const ApplicationsToApprove = (): JSX.Element => { }; }, [approve.isPending, approve.isSuccess]); + const handleSubmit = useCallback( + (values: TApplicationsToApprove) => { + approve.mutate(values.selected); + }, + [approve], + ); + return (
@@ -72,13 +83,7 @@ export const ApplicationsToApprove = (): JSX.Element => {
{`${applications.data?.length} applications found`}
-
{ - approve.mutate(values.selected); - }} - > + {applications.isLoading && (
diff --git a/packages/interface/src/features/applications/components/ReviewBar.tsx b/packages/interface/src/features/applications/components/ReviewBar.tsx index 9f3b1e6a..70cafc58 100644 --- a/packages/interface/src/features/applications/components/ReviewBar.tsx +++ b/packages/interface/src/features/applications/components/ReviewBar.tsx @@ -14,14 +14,15 @@ import { useApproveApplication } from "../hooks/useApproveApplication"; import { useApprovedApplications } from "../hooks/useApprovedApplications"; interface IReviewBarProps { + roundId: string; projectId: string; } -export const ReviewBar = ({ projectId }: IReviewBarProps): JSX.Element => { +export const ReviewBar = ({ roundId, projectId }: IReviewBarProps): JSX.Element => { const isAdmin = useIsAdmin(); const { isCorrectNetwork, correctNetwork } = useIsCorrectNetwork(); - const rawReturn = useApprovedApplications([projectId]); + const rawReturn = useApprovedApplications(roundId, [projectId]); const [refetchedData, setRefetchedData] = useState(); const approved = useMemo( @@ -29,7 +30,7 @@ export const ReviewBar = ({ projectId }: IReviewBarProps): JSX.Element => { [rawReturn.data, refetchedData], ); - const approve = useApproveApplication(); + const approve = useApproveApplication({ roundId }); const onClick = useCallback(() => { approve.mutate([projectId]); @@ -37,7 +38,7 @@ export const ReviewBar = ({ projectId }: IReviewBarProps): JSX.Element => { useEffect(() => { const fetchData = async () => { - const ret = await fetchApprovedApplications([projectId]); + const ret = await fetchApprovedApplications(roundId, [projectId]); setRefetchedData(ret); }; diff --git a/packages/interface/src/features/applications/hooks/useApplications.ts b/packages/interface/src/features/applications/hooks/useApplications.ts index bd92b59d..3ba0a3e0 100644 --- a/packages/interface/src/features/applications/hooks/useApplications.ts +++ b/packages/interface/src/features/applications/hooks/useApplications.ts @@ -3,6 +3,6 @@ import { api } from "~/utils/api"; import type { UseTRPCQueryResult } from "@trpc/react-query/shared"; import type { Attestation } from "~/utils/types"; -export function useApplications(): UseTRPCQueryResult { - return api.applications.list.useQuery({}); +export function useApplications(roundId: string): UseTRPCQueryResult { + return api.applications.list.useQuery({ roundId }); } diff --git a/packages/interface/src/features/applications/hooks/useApproveApplication.ts b/packages/interface/src/features/applications/hooks/useApproveApplication.ts index e1d052f7..fd8dd6b4 100644 --- a/packages/interface/src/features/applications/hooks/useApproveApplication.ts +++ b/packages/interface/src/features/applications/hooks/useApproveApplication.ts @@ -2,13 +2,14 @@ import { type Transaction } from "@ethereum-attestation-service/eas-sdk"; import { type UseMutationResult, useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; -import { config, eas } from "~/config"; +import { eas } from "~/config"; import { type TransactionError } from "~/features/voters/hooks/useApproveVoters"; import { useAttest } from "~/hooks/useEAS"; import { useEthersSigner } from "~/hooks/useEthersSigner"; import { createAttestation } from "~/lib/eas/createAttestation"; -export function useApproveApplication(opts?: { +export function useApproveApplication(opts: { + roundId: string; onSuccess?: () => void; }): UseMutationResult, Error | TransactionError, string[]> { const attest = useAttest(); @@ -24,7 +25,7 @@ export function useApproveApplication(opts?: { applicationIds.map((refUID) => createAttestation( { - values: { type: "application", round: config.roundId }, + values: { type: "application", round: opts.roundId }, schemaUID: eas.schemas.approval, refUID, }, @@ -36,7 +37,7 @@ export function useApproveApplication(opts?: { }, onSuccess: () => { toast.success("Application approved successfully!"); - opts?.onSuccess?.(); + opts.onSuccess?.(); }, onError: (err: { reason?: string; data?: { message: string } }) => toast.error("Application approve error", { diff --git a/packages/interface/src/features/applications/hooks/useApprovedApplications.ts b/packages/interface/src/features/applications/hooks/useApprovedApplications.ts index 6c3ed93b..9e0e1974 100644 --- a/packages/interface/src/features/applications/hooks/useApprovedApplications.ts +++ b/packages/interface/src/features/applications/hooks/useApprovedApplications.ts @@ -3,6 +3,6 @@ import { api } from "~/utils/api"; import type { UseTRPCQueryResult } from "@trpc/react-query/shared"; import type { Attestation } from "~/utils/types"; -export function useApprovedApplications(ids?: string[]): UseTRPCQueryResult { - return api.applications.approvals.useQuery({ ids }); +export function useApprovedApplications(roundId: string, ids?: string[]): UseTRPCQueryResult { + return api.applications.approvals.useQuery({ roundId, ids }); } diff --git a/packages/interface/src/features/applications/hooks/useCreateApplication.ts b/packages/interface/src/features/applications/hooks/useCreateApplication.ts index b7cefa9c..e701039c 100644 --- a/packages/interface/src/features/applications/hooks/useCreateApplication.ts +++ b/packages/interface/src/features/applications/hooks/useCreateApplication.ts @@ -1,6 +1,6 @@ import { type UseMutationResult, useMutation } from "@tanstack/react-query"; -import { config, eas } from "~/config"; +import { eas } from "~/config"; import { type TransactionError } from "~/features/voters/hooks/useApproveVoters"; import { useAttest, useCreateAttestation } from "~/hooks/useEAS"; import { useUploadMetadata } from "~/hooks/useMetadata"; @@ -20,6 +20,7 @@ export type TUseCreateApplicationReturn = Omit< export function useCreateApplication(options: { onSuccess: (data: Transaction) => void; onError: (err: TransactionError) => void; + roundId: string; }): TUseCreateApplicationReturn { const attestation = useCreateAttestation(); const attest = useAttest(); @@ -52,7 +53,7 @@ export function useCreateApplication(options: { metadataType: 0, // "http" metadataPtr, type: "application", - round: config.roundId, + round: options.roundId, }, }), ), diff --git a/packages/interface/src/features/ballot/components/BallotConfirmation.tsx b/packages/interface/src/features/ballot/components/BallotConfirmation.tsx index 6620d8e6..5beb6b29 100644 --- a/packages/interface/src/features/ballot/components/BallotConfirmation.tsx +++ b/packages/interface/src/features/ballot/components/BallotConfirmation.tsx @@ -9,10 +9,11 @@ import { Heading } from "~/components/ui/Heading"; import { Notice } from "~/components/ui/Notice"; import { config } from "~/config"; import { useBallot } from "~/contexts/Ballot"; +import { useRound } from "~/contexts/Round"; import { useProjectCount } from "~/features/projects/hooks/useProjects"; import { formatNumber } from "~/utils/formatNumber"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; import { ProjectAvatarWithName } from "./ProjectAvatarWithName"; @@ -25,11 +26,17 @@ const Card = createComponent( }), ); -export const BallotConfirmation = (): JSX.Element => { +interface IBallotConfirmationProps { + roundId: string; +} + +export const BallotConfirmation = ({ roundId }: IBallotConfirmationProps): JSX.Element => { const { ballot, sumBallot } = useBallot(); const allocations = ballot.votes; - const { data: projectCount } = useProjectCount(); - const appState = useAppState(); + const { data: projectCount } = useProjectCount(roundId); + const roundState = useRoundState(roundId); + const { getRound } = useRound(); + const round = getRound(roundId); const sum = useMemo(() => formatNumber(sumBallot(ballot.votes)), [ballot, sumBallot]); @@ -40,14 +47,14 @@ export const BallotConfirmation = (): JSX.Element => {

- {`Thank you for participating in ${config.eventName} ${config.roundId} round.`} + {`Thank you for participating in ${config.eventName} ${roundId} round.`}

Summary of your ballot

- {`Round you voted in: ${config.roundId}`} + {`Round you voted in: ${roundId}`}
@@ -70,12 +77,14 @@ export const BallotConfirmation = (): JSX.Element => {

- {appState === EAppState.VOTING && ( + {roundState === ERoundState.VOTING && (
Changed your mind? diff --git a/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx b/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx index 3961bc2d..cc69f24e 100644 --- a/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx +++ b/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx @@ -8,12 +8,16 @@ import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; import { useProjectIdMapping } from "~/features/projects/hooks/useProjects"; -export const SubmitBallotButton = (): JSX.Element => { +interface ISubmitBallotButtonProps { + roundId: string; +} + +export const SubmitBallotButton = ({ roundId }: ISubmitBallotButtonProps): JSX.Element => { const router = useRouter(); const [isOpen, setOpen] = useState(false); const { onVote, isLoading, initialVoiceCredits } = useMaci(); const { ballot, publishBallot, sumBallot } = useBallot(); - const projectIndices = useProjectIdMapping(ballot); + const projectIndices = useProjectIdMapping(ballot, roundId); const ableToSubmit = useMemo( () => sumBallot(ballot.votes) <= initialVoiceCredits, diff --git a/packages/interface/src/features/filter/types/index.ts b/packages/interface/src/features/filter/types/index.ts index dc0f3e30..b6cf2fab 100644 --- a/packages/interface/src/features/filter/types/index.ts +++ b/packages/interface/src/features/filter/types/index.ts @@ -17,6 +17,7 @@ export const FilterSchema = z.object({ sortOrder: z.nativeEnum(SortOrder).default(SortOrder.asc), search: z.preprocess((v) => (v === "null" || v === "undefined" ? null : v), z.string().nullish()), needApproval: z.boolean().optional().default(true), + roundId: z.string(), }); export type Filter = z.infer; diff --git a/packages/interface/src/features/signup/components/FaqItem.tsx b/packages/interface/src/features/home/components/FaqItem.tsx similarity index 100% rename from packages/interface/src/features/signup/components/FaqItem.tsx rename to packages/interface/src/features/home/components/FaqItem.tsx diff --git a/packages/interface/src/features/signup/components/FaqList.tsx b/packages/interface/src/features/home/components/FaqList.tsx similarity index 100% rename from packages/interface/src/features/signup/components/FaqList.tsx rename to packages/interface/src/features/home/components/FaqList.tsx diff --git a/packages/interface/src/features/signup/types.ts b/packages/interface/src/features/home/types.ts similarity index 100% rename from packages/interface/src/features/signup/types.ts rename to packages/interface/src/features/home/types.ts diff --git a/packages/interface/src/features/projects/components/ProjectAwarded.tsx b/packages/interface/src/features/projects/components/ProjectAwarded.tsx index e5351bd8..3da3170c 100644 --- a/packages/interface/src/features/projects/components/ProjectAwarded.tsx +++ b/packages/interface/src/features/projects/components/ProjectAwarded.tsx @@ -5,12 +5,13 @@ import { useProjectResults } from "~/hooks/useResults"; import { formatNumber } from "~/utils/formatNumber"; export interface IProjectAwardedProps { + roundId: string; id?: string; } -export const ProjectAwarded = ({ id = "" }: IProjectAwardedProps): JSX.Element | null => { +export const ProjectAwarded = ({ roundId, id = "" }: IProjectAwardedProps): JSX.Element | null => { const { pollData } = useMaci(); - const amount = useProjectResults(id, pollData); + const amount = useProjectResults(id, roundId, pollData); if (amount.isLoading) { return null; diff --git a/packages/interface/src/features/projects/components/ProjectDetails.tsx b/packages/interface/src/features/projects/components/ProjectDetails.tsx index f640620e..1e93c2bc 100644 --- a/packages/interface/src/features/projects/components/ProjectDetails.tsx +++ b/packages/interface/src/features/projects/components/ProjectDetails.tsx @@ -5,8 +5,8 @@ import { Navigation } from "~/components/ui/Navigation"; import { ProjectAvatar } from "~/features/projects/components/ProjectAvatar"; import { ProjectBanner } from "~/features/projects/components/ProjectBanner"; import { VotingWidget } from "~/features/projects/components/VotingWidget"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; import type { Attestation } from "~/utils/types"; @@ -16,12 +16,14 @@ import { ProjectContacts } from "./ProjectContacts"; import { ProjectDescriptionSection } from "./ProjectDescriptionSection"; export interface IProjectDetailsProps { + roundId: string; action?: ReactNode; projectId?: string; attestation?: Attestation; } const ProjectDetails = ({ + roundId, projectId = "", attestation = undefined, action = undefined, @@ -31,12 +33,12 @@ const ProjectDetails = ({ const { bio, websiteUrl, payoutAddress, github, twitter, fundingSources, profileImageUrl, bannerImageUrl } = metadata.data ?? {}; - const appState = useAppState(); + const roundState = useRoundState(roundId); return (
- +
@@ -52,7 +54,7 @@ const ProjectDetails = ({ {attestation?.name} - {appState === EAppState.VOTING && } + {roundState === ERoundState.VOTING && }
diff --git a/packages/interface/src/features/projects/components/ProjectItem.tsx b/packages/interface/src/features/projects/components/ProjectItem.tsx index 9af1dd0f..6bd8a483 100644 --- a/packages/interface/src/features/projects/components/ProjectItem.tsx +++ b/packages/interface/src/features/projects/components/ProjectItem.tsx @@ -5,8 +5,8 @@ import { Heading } from "~/components/ui/Heading"; import { Skeleton } from "~/components/ui/Skeleton"; import { config } from "~/config"; import { formatNumber } from "~/utils/formatNumber"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; import type { Attestation } from "~/utils/types"; @@ -18,6 +18,7 @@ import { ProjectAvatar } from "./ProjectAvatar"; import { ProjectBanner } from "./ProjectBanner"; export interface IProjectItemProps { + roundId: string; attestation: Attestation; isLoading: boolean; state?: EProjectState; @@ -25,13 +26,14 @@ export interface IProjectItemProps { } export const ProjectItem = ({ + roundId, attestation, isLoading, state = undefined, action = undefined, }: IProjectItemProps): JSX.Element => { const metadata = useProjectMetadata(attestation.metadataPtr); - const appState = useAppState(); + const roundState = useRoundState(roundId); return (
- {!isLoading && state !== undefined && action && appState === EAppState.VOTING && ( + {!isLoading && state !== undefined && action && roundState === ERoundState.VOTING && (
{state === EProjectState.DEFAULT && ( @@ -71,7 +73,7 @@ export const ProjectItem = ({ {state === EProjectState.ADDED && ( )} diff --git a/packages/interface/src/features/projects/components/ProjectsResults.tsx b/packages/interface/src/features/projects/components/ProjectsResults.tsx index 6e6ba5bc..cc12cf3e 100644 --- a/packages/interface/src/features/projects/components/ProjectsResults.tsx +++ b/packages/interface/src/features/projects/components/ProjectsResults.tsx @@ -6,19 +6,23 @@ import { useCallback } from "react"; import { InfiniteLoading } from "~/components/InfiniteLoading"; import { useMaci } from "~/contexts/Maci"; import { useResults, useProjectsResults } from "~/hooks/useResults"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; import { EProjectState } from "../types"; import { ProjectItem, ProjectItemAwarded } from "./ProjectItem"; -export const ProjectsResults = (): JSX.Element => { +interface IProjectsResultsProps { + roundId: string; +} + +export const ProjectsResults = ({ roundId }: IProjectsResultsProps): JSX.Element => { const router = useRouter(); const { pollData } = useMaci(); - const projects = useProjectsResults(pollData); - const results = useResults(); - const appState = useAppState(); + const projects = useProjectsResults(roundId, pollData); + const results = useResults(roundId); + const roundState = useRoundState(roundId); const handleAction = useCallback( (projectId: string) => (e: Event) => { @@ -33,7 +37,7 @@ export const ProjectsResults = (): JSX.Element => { {...projects} renderItem={(item, { isLoading }) => ( - {!results.isLoading && appState === EAppState.RESULTS ? ( + {!results.isLoading && roundState === ERoundState.RESULTS ? ( ) : null} @@ -41,6 +45,7 @@ export const ProjectsResults = (): JSX.Element => { action={handleAction(item.id)} attestation={item} isLoading={isLoading} + roundId={roundId} state={EProjectState.SUBMITTED} /> diff --git a/packages/interface/src/features/projects/hooks/useProjects.ts b/packages/interface/src/features/projects/hooks/useProjects.ts index 8dc1fdee..c338fada 100644 --- a/packages/interface/src/features/projects/hooks/useProjects.ts +++ b/packages/interface/src/features/projects/hooks/useProjects.ts @@ -11,6 +11,7 @@ import type { Ballot } from "~/features/ballot/types"; import type { Attestation } from "~/utils/types"; interface IUseSearchProjectsProps { + roundId: string; filterOverride?: Partial; needApproval?: boolean; } @@ -25,21 +26,22 @@ export function useProjectsById(ids: string[]): UseTRPCQueryResult { const { ...filter } = useFilter(); return api.projects.search.useInfiniteQuery( - { seed, ...filter, ...filterOverride, needApproval }, + { roundId, seed, ...filter, ...filterOverride, needApproval }, { getNextPageParam: (_, pages) => pages.length, }, ); } -export function useProjectIdMapping(ballot: Ballot): Record { - const { data } = api.projects.allApproved.useQuery(); +export function useProjectIdMapping(ballot: Ballot, roundId: string): Record { + const { data } = api.projects.allApproved.useQuery({ roundId }); const projectIndices = useMemo( () => @@ -59,6 +61,6 @@ export function useProjectMetadata(metadataPtr?: string): UseTRPCQueryResult(metadataPtr); } -export function useProjectCount(): UseTRPCQueryResult<{ count: number }, unknown> { - return api.projects.count.useQuery(); +export function useProjectCount(roundId: string): UseTRPCQueryResult<{ count: number }, unknown> { + return api.projects.count.useQuery({ roundId }); } diff --git a/packages/interface/src/features/projects/components/Projects.tsx b/packages/interface/src/features/rounds/components/Projects.tsx similarity index 75% rename from packages/interface/src/features/projects/components/Projects.tsx rename to packages/interface/src/features/rounds/components/Projects.tsx index d8af65d8..ebbd5fa6 100644 --- a/packages/interface/src/features/projects/components/Projects.tsx +++ b/packages/interface/src/features/rounds/components/Projects.tsx @@ -10,21 +10,24 @@ import { Heading } from "~/components/ui/Heading"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; import { useResults } from "~/hooks/useResults"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; -import { useSearchProjects } from "../hooks/useProjects"; -import { EProjectState } from "../types"; +import { ProjectItem, ProjectItemAwarded } from "../../projects/components/ProjectItem"; +import { useSearchProjects } from "../../projects/hooks/useProjects"; +import { EProjectState } from "../../projects/types"; -import { ProjectItem, ProjectItemAwarded } from "./ProjectItem"; +export interface IProjectsProps { + roundId?: string; +} -export const Projects = (): JSX.Element => { - const appState = useAppState(); - const projects = useSearchProjects({ needApproval: appState !== EAppState.APPLICATION }); +export const Projects = ({ roundId = "" }: IProjectsProps): JSX.Element => { + const appState = useRoundState(roundId); + const projects = useSearchProjects({ roundId, needApproval: appState !== ERoundState.APPLICATION }); const { pollData, pollId, isRegistered } = useMaci(); const { addToBallot, removeFromBallot, ballotContains, ballot } = useBallot(); - const results = useResults(pollData); + const results = useResults(roundId, pollData); const handleAction = useCallback( (projectId: string) => (e: Event) => { @@ -66,7 +69,7 @@ export const Projects = (): JSX.Element => { return (
- {appState === EAppState.APPLICATION && ( + {appState === ERoundState.APPLICATION && ( @@ -78,7 +81,7 @@ export const Projects = (): JSX.Element => { /> )} - {appState === EAppState.TALLYING && ( + {(appState === ERoundState.TALLYING || appState === ERoundState.RESULTS) && ( @@ -106,9 +109,9 @@ export const Projects = (): JSX.Element => { - {!results.isLoading && appState === EAppState.RESULTS ? ( + {!results.isLoading && appState === ERoundState.RESULTS ? ( ) : null} @@ -116,6 +119,7 @@ export const Projects = (): JSX.Element => { action={handleAction(item.id)} attestation={item} isLoading={isLoading} + roundId={roundId} state={defineState(item.id)} /> diff --git a/packages/interface/src/features/rounds/components/RoundItem.tsx b/packages/interface/src/features/rounds/components/RoundItem.tsx new file mode 100644 index 00000000..2033dc11 --- /dev/null +++ b/packages/interface/src/features/rounds/components/RoundItem.tsx @@ -0,0 +1,65 @@ +import clsx from "clsx"; +import Link from "next/link"; +import { useMemo } from "react"; +import { FiCalendar } from "react-icons/fi"; + +import { Heading } from "~/components/ui/Heading"; +import { useRoundState } from "~/utils/state"; +import { formatPeriod } from "~/utils/time"; +import { ERoundState } from "~/utils/types"; + +import type { Round } from "~/features/rounds/types"; + +interface ITimeBarProps { + start: Date; + end: Date; +} + +interface IRoundTagProps { + isOpen: boolean; +} + +interface IRoundItemProps { + round: Round; +} + +const TimeBar = ({ start, end }: ITimeBarProps): JSX.Element => { + const periodString = useMemo(() => formatPeriod({ start, end }), [start, end]); + + return ( +
+ + +

{periodString}

+
+ ); +}; + +const RoundTag = ({ isOpen }: IRoundTagProps): JSX.Element => ( +
+ {isOpen ? "Voting Open" : "Round Closed"} +
+); + +export const RoundItem = ({ round }: IRoundItemProps): JSX.Element => { + const roundState = useRoundState(round.roundId); + + return ( + +
+ + + {round.roundId} + +

{round.description}

+ + +
+ + ); +}; diff --git a/packages/interface/src/features/rounds/components/RoundsList.tsx b/packages/interface/src/features/rounds/components/RoundsList.tsx new file mode 100644 index 00000000..4d4b35a8 --- /dev/null +++ b/packages/interface/src/features/rounds/components/RoundsList.tsx @@ -0,0 +1,16 @@ +import { useRound } from "~/contexts/Round"; + +import { RoundItem } from "./RoundItem"; + +/// TODO: change to InfiniteLoading after loading rounds from registry contract and make search from trpc service +export const RoundsList = (): JSX.Element => { + const { rounds } = useRound(); + + return ( +
+ {rounds.map((round) => ( + + ))} +
+ ); +}; diff --git a/packages/interface/src/features/rounds/types/index.ts b/packages/interface/src/features/rounds/types/index.ts new file mode 100644 index 00000000..25a13431 --- /dev/null +++ b/packages/interface/src/features/rounds/types/index.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const RoundSchema = z.object({ + roundId: z.string(), + description: z.string(), + startsAt: z.number(), + registrationEndsAt: z.number(), + votingEndsAt: z.number(), + tallyURL: z.string().optional(), +}); + +export type Round = z.infer; diff --git a/packages/interface/src/features/voters/components/ApproveVoters.tsx b/packages/interface/src/features/voters/components/ApproveVoters.tsx index c879fe14..8eab5963 100644 --- a/packages/interface/src/features/voters/components/ApproveVoters.tsx +++ b/packages/interface/src/features/voters/components/ApproveVoters.tsx @@ -1,10 +1,9 @@ import { UserRoundPlus } from "lucide-react"; import dynamic from "next/dynamic"; -import { useMemo, useState } from "react"; +import { useMemo, useState, useCallback } from "react"; import { useFormContext } from "react-hook-form"; import { toast } from "sonner"; import { isAddress } from "viem"; -import { z } from "zod"; import { IconButton } from "~/components/ui/Button"; import { Dialog } from "~/components/ui/Dialog"; @@ -14,7 +13,7 @@ import { useIsAdmin } from "~/hooks/useIsAdmin"; import { useIsCorrectNetwork } from "~/hooks/useIsCorrectNetwork"; import { useApproveVoters } from "../hooks/useApproveVoters"; -import { EthAddressSchema } from "../types"; +import { ApproveVotersSchema, type TApproveVoters } from "../types"; function parseAddresses(addresses = ""): string[] { return addresses @@ -66,15 +65,25 @@ const ApproveVoters = () => { const buttonText = isAdmin ? `Add voters` : "You must be an admin"; + const handleSubmit = useCallback( + (values: TApproveVoters) => { + const voters = parseAddresses(values.voters); + approve.mutate(voters); + }, + [parseAddresses, approve], + ); + + const buttonOnClick = useCallback(() => { + setOpen(true); + }, [setOpen]); + return (
{ - setOpen(true); - }} + onClick={buttonOnClick} > {!isCorrectNetwork ? `Connect to ${correctNetwork.name}` : buttonText} @@ -86,15 +95,7 @@ const ApproveVoters = () => { title="Approve voters" onOpenChange={setOpen} > - { - const voters = parseAddresses(values.voters); - approve.mutate(voters); - }} - > +
diff --git a/packages/interface/src/features/voters/hooks/useApproveVoters.ts b/packages/interface/src/features/voters/hooks/useApproveVoters.ts index eaf7ff3c..b24daf14 100644 --- a/packages/interface/src/features/voters/hooks/useApproveVoters.ts +++ b/packages/interface/src/features/voters/hooks/useApproveVoters.ts @@ -1,7 +1,7 @@ import { type Transaction } from "@ethereum-attestation-service/eas-sdk"; import { type UseMutationResult, useMutation } from "@tanstack/react-query"; -import { config, eas } from "~/config"; +import { eas } from "~/config"; import { useAttest } from "~/hooks/useEAS"; import { useEthersSigner } from "~/hooks/useEthersSigner"; import { createAttestation } from "~/lib/eas/createAttestation"; @@ -25,11 +25,12 @@ export function useApproveVoters(options: { throw new Error("Connect wallet first"); } + /// TODO: should be changed to event name instead of roundId const attestations = await Promise.all( voters.map((recipient) => createAttestation( { - values: { type: "voter", round: config.roundId }, + values: { type: "voter" }, schemaUID: eas.schemas.approval, recipient, }, diff --git a/packages/interface/src/features/voters/types.tsx b/packages/interface/src/features/voters/types.tsx index 5a0a12ca..4b0a8eec 100644 --- a/packages/interface/src/features/voters/types.tsx +++ b/packages/interface/src/features/voters/types.tsx @@ -9,3 +9,9 @@ export const EthAddressSchema = z.custom( .every((address) => isAddress(address as Address)) || isAddress(val as Address), "Invalid address", ); + +export const ApproveVotersSchema = z.object({ + voters: EthAddressSchema, +}); + +export type TApproveVoters = z.infer; diff --git a/packages/interface/src/hooks/useResults.ts b/packages/interface/src/hooks/useResults.ts index a807cd25..a68027fe 100644 --- a/packages/interface/src/hooks/useResults.ts +++ b/packages/interface/src/hooks/useResults.ts @@ -1,44 +1,50 @@ import { config } from "~/config"; import { api } from "~/utils/api"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; import type { UseTRPCInfiniteQueryResult, UseTRPCQueryResult } from "@trpc/react-query/shared"; import type { IGetPollData } from "maci-cli/sdk"; import type { Attestation } from "~/utils/types"; export function useResults( + roundId: string, pollData?: IGetPollData, ): UseTRPCQueryResult<{ averageVotes: number; projects: Record }, unknown> { - const appState = useAppState(); + const roundState = useRoundState(roundId); - return api.results.votes.useQuery({ pollId: pollData?.id.toString() }, { enabled: appState === EAppState.RESULTS }); + return api.results.votes.useQuery( + { roundId, pollId: pollData?.id.toString() }, + { enabled: roundState === ERoundState.RESULTS }, + ); } const seed = 0; export function useProjectsResults( + roundId: string, pollData?: IGetPollData, ): UseTRPCInfiniteQueryResult { return api.results.projects.useInfiniteQuery( - { limit: config.pageSize, seed, pollId: pollData?.id.toString() }, + { roundId, limit: config.pageSize, seed, pollId: pollData?.id.toString() }, { getNextPageParam: (_, pages) => pages.length, }, ); } -export function useProjectCount(): UseTRPCQueryResult<{ count: number }, unknown> { - return api.projects.count.useQuery(); +export function useProjectCount(roundId: string): UseTRPCQueryResult<{ count: number }, unknown> { + return api.projects.count.useQuery({ roundId }); } export function useProjectResults( id: string, + roundId: string, pollData?: IGetPollData, ): UseTRPCQueryResult<{ amount: number }, unknown> { - const appState = useAppState(); + const appState = useRoundState(roundId); return api.results.project.useQuery( - { id, pollId: pollData?.id.toString() }, - { enabled: appState === EAppState.RESULTS }, + { id, roundId, pollId: pollData?.id.toString() }, + { enabled: appState === ERoundState.RESULTS }, ); } diff --git a/packages/interface/src/layouts/AdminLayout.tsx b/packages/interface/src/layouts/AdminLayout.tsx index 90d4dcb2..32dd94fd 100644 --- a/packages/interface/src/layouts/AdminLayout.tsx +++ b/packages/interface/src/layouts/AdminLayout.tsx @@ -1,19 +1,10 @@ import { InvalidAdmin } from "~/features/admin/components/InvalidAdmin"; import { useIsAdmin } from "~/hooks/useIsAdmin"; -import type { ReactNode, PropsWithChildren } from "react"; - -import { type LayoutProps } from "./BaseLayout"; import { Layout } from "./DefaultLayout"; +import { IAdminLayoutProps } from "./types"; -type Props = PropsWithChildren< - { - sidebar?: "left" | "right"; - sidebarComponent?: ReactNode; - } & LayoutProps ->; - -export const AdminLayout = ({ children = null, ...props }: Props): JSX.Element => { +export const AdminLayout = ({ children = null, ...props }: IAdminLayoutProps): JSX.Element => { const isAdmin = useIsAdmin(); if (isAdmin) { return {children}; diff --git a/packages/interface/src/layouts/BaseLayout.tsx b/packages/interface/src/layouts/BaseLayout.tsx index 21c41b0a..e44b9e12 100644 --- a/packages/interface/src/layouts/BaseLayout.tsx +++ b/packages/interface/src/layouts/BaseLayout.tsx @@ -2,15 +2,7 @@ import clsx from "clsx"; import Head from "next/head"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; -import { - type ReactNode, - type PropsWithChildren, - createContext, - useContext, - useEffect, - useCallback, - useMemo, -} from "react"; +import { type PropsWithChildren, createContext, useContext, useEffect, useCallback, useMemo } from "react"; import { tv } from "tailwind-variants"; import { useAccount } from "wagmi"; @@ -19,6 +11,8 @@ import { createComponent } from "~/components/ui"; import { metadata } from "~/config"; import { useMaci } from "~/contexts/Maci"; +import type { IBaseLayoutProps } from "./types"; + const Context = createContext({ eligibilityCheck: false, showBallot: false }); const MainContainer = createComponent( @@ -39,7 +33,7 @@ const MainContainer = createComponent( export const useLayoutOptions = (): { eligibilityCheck: boolean; showBallot: boolean } => useContext(Context); -const Sidebar = ({ side = undefined, ...props }: { side?: "left" | "right" } & PropsWithChildren) => ( +const Sidebar = ({ side = undefined, ...props }: PropsWithChildren<{ side?: "left" | "right" }>) => (
); -export interface LayoutProps { - title?: string; - requireAuth?: boolean; - requireRegistration?: boolean; - eligibilityCheck?: boolean; - showBallot?: boolean; - type?: string; -} - export const BaseLayout = ({ header = null, title = "", @@ -70,13 +55,7 @@ export const BaseLayout = ({ showBallot = false, type = undefined, children = null, -}: PropsWithChildren< - { - sidebar?: "left" | "right"; - sidebarComponent?: ReactNode; - header?: ReactNode; - } & LayoutProps ->): JSX.Element => { +}: IBaseLayoutProps): JSX.Element => { const { theme } = useTheme(); const router = useRouter(); const { address, isConnecting } = useAccount(); diff --git a/packages/interface/src/layouts/DefaultLayout.tsx b/packages/interface/src/layouts/DefaultLayout.tsx index 562d09f2..871d39f0 100644 --- a/packages/interface/src/layouts/DefaultLayout.tsx +++ b/packages/interface/src/layouts/DefaultLayout.tsx @@ -1,5 +1,5 @@ import { GatekeeperTrait } from "maci-cli/sdk"; -import { type ReactNode, type PropsWithChildren, useMemo } from "react"; +import { useMemo } from "react"; import { useAccount } from "wagmi"; import Header from "~/components/Header"; @@ -9,53 +9,65 @@ import { config } from "~/config"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; import { SubmitBallotButton } from "~/features/ballot/components/SubmitBallotButton"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; -import { BaseLayout, type LayoutProps } from "./BaseLayout"; +import type { ILayoutProps } from "./types"; -interface ILayoutProps extends PropsWithChildren { - sidebar?: "left" | "right"; - sidebarComponent?: ReactNode; - showInfo?: boolean; - showSubmitButton?: boolean; -} +import { BaseLayout } from "./BaseLayout"; export const Layout = ({ children = null, ...props }: ILayoutProps): JSX.Element => { const { address } = useAccount(); - const appState = useAppState(); + const roundState = useRoundState(props.roundId ?? ""); const { ballot } = useBallot(); const { isRegistered, gatekeeperTrait } = useMaci(); const navLinks = useMemo(() => { - const links = [ - { - href: "/projects", + const links = []; + + if (roundState !== ERoundState.DEFAULT) { + links.push({ + href: `/rounds/${props.roundId}`, children: "Projects", - }, - ]; + }); + } - if (appState === EAppState.VOTING && isRegistered) { + if (roundState === ERoundState.VOTING && isRegistered) { links.push({ - href: "/ballot", + href: `/rounds/${props.roundId}/ballot`, children: "My Ballot", }); } - if ((appState === EAppState.TALLYING || appState === EAppState.RESULTS) && ballot.published) { + if ( + (roundState === ERoundState.TALLYING || roundState === ERoundState.RESULTS) && + ballot.published && + isRegistered + ) { links.push({ - href: "/ballot/confirmation", + href: `/rounds/${props.roundId}/ballot/confirmation`, children: "Submitted Ballot", }); } - if (appState === EAppState.RESULTS) { + if (roundState === ERoundState.RESULTS) { links.push({ - href: "/stats", + href: `/rounds/${props.roundId}/stats`, children: "Stats", }); } + if (config.admin === address! && props.roundId) { + links.push( + ...[ + { + href: `/rounds/${props.roundId}/applications`, + children: "Applications", + }, + ], + ); + } + if (config.admin === address!) { links.push( ...[ @@ -74,12 +86,16 @@ export const Layout = ({ children = null, ...props }: ILayoutProps): JSX.Element href: "/voters", children: "Voters", }, + { + href: "/coordinator", + children: "Coordinator", + }, ], ); } return links; - }, [ballot.published, appState, isRegistered, address]); + }, [ballot.published, roundState, isRegistered, address]); return ( }> @@ -92,7 +108,8 @@ export const LayoutWithSidebar = ({ ...props }: ILayoutProps): JSX.Element => { const { isRegistered } = useMaci(); const { address } = useAccount(); const { ballot } = useBallot(); - const appState = useAppState(); + const roundId = props.roundId ?? ""; + const roundState = useRoundState(roundId); const { showInfo, showBallot, showSubmitButton } = props; @@ -102,15 +119,16 @@ export const LayoutWithSidebar = ({ ...props }: ILayoutProps): JSX.Element => { sidebarComponent={
{showSubmitButton && ballot.votes.length > 0 && (
- + { + sidebar?: "left" | "right"; + sidebarComponent?: ReactNode; + header?: ReactNode; +} + +export interface ILayoutProps extends PropsWithChildren { + sidebar?: "left" | "right"; + sidebarComponent?: ReactNode; + showInfo?: boolean; + showSubmitButton?: boolean; + roundId?: string; +} + +export interface IAdminLayoutProps extends PropsWithChildren { + sidebar?: "left" | "right"; + sidebarComponent?: ReactNode; + roundId?: string; +} diff --git a/packages/interface/src/pages/applications/index.tsx b/packages/interface/src/pages/applications/index.tsx deleted file mode 100644 index b54a5200..00000000 --- a/packages/interface/src/pages/applications/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ApplicationsToApprove } from "~/features/applications/components/ApplicationsToApprove"; -import { AdminLayout } from "~/layouts/AdminLayout"; - -const ApplicationsPage = (): JSX.Element => ( - - - -); - -export default ApplicationsPage; diff --git a/packages/interface/src/pages/coordinator/index.tsx b/packages/interface/src/pages/coordinator/index.tsx new file mode 100644 index 00000000..0a3995d8 --- /dev/null +++ b/packages/interface/src/pages/coordinator/index.tsx @@ -0,0 +1,5 @@ +import { Layout } from "~/layouts/DefaultLayout"; + +const CoordinatorPage = (): JSX.Element => This is the coordinator page.; + +export default CoordinatorPage; diff --git a/packages/interface/src/pages/index.tsx b/packages/interface/src/pages/index.tsx index 46f66b79..b0ec4ca8 100644 --- a/packages/interface/src/pages/index.tsx +++ b/packages/interface/src/pages/index.tsx @@ -1,15 +1,57 @@ -import { type GetServerSideProps } from "next"; +import { useAccount } from "wagmi"; +import { JoinButton } from "~/components/JoinButton"; +import { Button } from "~/components/ui/Button"; +import { Heading } from "~/components/ui/Heading"; +import { config } from "~/config"; +import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; +import { FAQList } from "~/features/home/components/FaqList"; +import { RoundsList } from "~/features/rounds/components/RoundsList"; +import { useIsAdmin } from "~/hooks/useIsAdmin"; import { Layout } from "~/layouts/DefaultLayout"; -const SignupPage = (): JSX.Element => ...; +const HomePage = (): JSX.Element => { + const { isConnected } = useAccount(); + const { isRegistered } = useMaci(); + const isAdmin = useIsAdmin(); + const { rounds } = useRound(); -export default SignupPage; + return ( + +
+ + {config.eventName} + -export const getServerSideProps: GetServerSideProps = async () => - Promise.resolve({ - redirect: { - destination: "/signup", - permanent: false, - }, - }); + + {config.eventDescription} + + + {!isConnected &&

Connect your wallet to get started.

} + + {isConnected && !isAdmin && !isRegistered && } + + {isConnected && isAdmin && ( +
+

Configure and deploy your contracts to get started.

+ + +
+ )} + + {isConnected && !isAdmin && rounds.length === 0 && ( +

There are no rounds deployed.

+ )} + + {rounds.length > 0 && } +
+ + +
+ ); +}; + +export default HomePage; diff --git a/packages/interface/src/pages/info/index.tsx b/packages/interface/src/pages/info/index.tsx deleted file mode 100644 index 434a1b8c..00000000 --- a/packages/interface/src/pages/info/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Heading } from "~/components/ui/Heading"; -import { config } from "~/config"; -import { Layout } from "~/layouts/DefaultLayout"; -import { cn } from "~/utils/classNames"; -import { formatDate } from "~/utils/time"; - -const steps = [ - { - label: "Registration", - date: config.startsAt, - }, - { - label: "Voting", - date: config.registrationEndsAt, - }, - { - label: "Tallying", - date: undefined, - }, - { - label: "Distribution", - date: undefined, - }, -]; - -const InfoPage = (): JSX.Element => { - const { progress, currentStepIndex } = calculateProgress(steps); - - return ( - -
-
-
- -
- {steps.map((step, i) => ( -
- - {step.label} - - - {step.date instanceof Date && !Number.isNaN(step.date) &&
{formatDate(step.date)}
} -
- ))} -
- - ); -}; - -export default InfoPage; - -function calculateProgress(items: { label: string; date?: Date }[]) { - const now = Number(new Date()); - - let currentStepIndex = items.findIndex( - (step, index) => now < Number(step.date) && (index === 0 || now >= Number(items[index - 1]?.date)), - ); - - if (currentStepIndex === -1) { - currentStepIndex = items.length; - } - - let progress = 0; - - if (currentStepIndex > 0) { - // Calculate progress for completed segments - for (let i = 0; i < currentStepIndex - 1; i += 1) { - progress += 1 / (items.length - 1); - } - - // Calculate progress within the current segment - const segmentStart = currentStepIndex === 0 ? 0 : Number(items[currentStepIndex - 1]?.date); - const segmentEnd = Number(items[currentStepIndex]?.date); - const segmentDuration = segmentEnd - segmentStart; - const timeElapsedInSegment = now - segmentStart; - - progress += Math.min(timeElapsedInSegment, segmentDuration) / segmentDuration / (items.length - 1); - } - - progress = Math.min(Math.max(progress, 0), 1); - - return { progress, currentStepIndex }; -} diff --git a/packages/interface/src/pages/projects/index.tsx b/packages/interface/src/pages/projects/index.tsx deleted file mode 100644 index 60c88134..00000000 --- a/packages/interface/src/pages/projects/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Projects } from "~/features/projects/components/Projects"; -import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; - -const ProjectsPage = (): JSX.Element => ( - - - -); - -export default ProjectsPage; diff --git a/packages/interface/src/pages/projects/results.tsx b/packages/interface/src/pages/projects/results.tsx deleted file mode 100644 index 52defe6c..00000000 --- a/packages/interface/src/pages/projects/results.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ProjectsResults } from "~/features/projects/components/ProjectsResults"; -import { Layout } from "~/layouts/DefaultLayout"; - -const ProjectsResultsPage = (): JSX.Element => ( - - - -); - -export default ProjectsResultsPage; diff --git a/packages/interface/src/pages/projects/[projectId]/Project.tsx b/packages/interface/src/pages/rounds/[roundId]/[projectId]/Project.tsx similarity index 60% rename from packages/interface/src/pages/projects/[projectId]/Project.tsx rename to packages/interface/src/pages/rounds/[roundId]/[projectId]/Project.tsx index f05cd1c7..5c057895 100644 --- a/packages/interface/src/pages/projects/[projectId]/Project.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/[projectId]/Project.tsx @@ -1,33 +1,34 @@ -import { type GetServerSideProps } from "next"; - import { ReviewBar } from "~/features/applications/components/ReviewBar"; import ProjectDetails from "~/features/projects/components/ProjectDetails"; import { useProjectById } from "~/features/projects/hooks/useProjects"; import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; + +import type { GetServerSideProps } from "next"; export interface IProjectDetailsProps { + roundId: string; projectId?: string; } -const ProjectDetailsPage = ({ projectId = "" }: IProjectDetailsProps): JSX.Element => { +const ProjectDetailsPage = ({ roundId, projectId = "" }: IProjectDetailsProps): JSX.Element => { const projects = useProjectById(projectId); const { name } = projects.data?.[0] ?? {}; - const appState = useAppState(); + const appState = useRoundState(roundId); return ( - {appState === EAppState.APPLICATION && } + {appState === ERoundState.APPLICATION && } - + ); }; export default ProjectDetailsPage; -export const getServerSideProps: GetServerSideProps = async ({ query: { projectId } }) => +export const getServerSideProps: GetServerSideProps = async ({ query: { projectId, roundId } }) => Promise.resolve({ - props: { projectId }, + props: { projectId, roundId }, }); diff --git a/packages/interface/src/pages/projects/[projectId]/index.tsx b/packages/interface/src/pages/rounds/[roundId]/[projectId]/index.tsx similarity index 100% rename from packages/interface/src/pages/projects/[projectId]/index.tsx rename to packages/interface/src/pages/rounds/[roundId]/[projectId]/index.tsx diff --git a/packages/interface/src/pages/applications/confirmation.tsx b/packages/interface/src/pages/rounds/[roundId]/applications/confirmation.tsx similarity index 80% rename from packages/interface/src/pages/applications/confirmation.tsx rename to packages/interface/src/pages/rounds/[roundId]/applications/confirmation.tsx index 7c9817fc..ac2c7102 100644 --- a/packages/interface/src/pages/applications/confirmation.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/applications/confirmation.tsx @@ -8,11 +8,15 @@ import { Heading } from "~/components/ui/Heading"; import { useApplicationByTxHash } from "~/features/applications/hooks/useApplicationByTxHash"; import { ProjectItem } from "~/features/projects/components/ProjectItem"; import { Layout } from "~/layouts/DefaultLayout"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; -const ConfirmProjectPage = (): JSX.Element => { - const state = useAppState(); +interface IConfirmProjectPageProps { + roundId: string; +} + +const ConfirmProjectPage = ({ roundId }: IConfirmProjectPageProps): JSX.Element => { + const state = useRoundState(roundId); const searchParams = useSearchParams(); const txHash = useMemo(() => searchParams.get("txHash"), [searchParams]); @@ -38,11 +42,11 @@ const ConfirmProjectPage = (): JSX.Element => { Applications can be edited and approved until the Application period ends.

- {state !== EAppState.APPLICATION && } + {state !== ERoundState.APPLICATION && } {attestation && ( - + )}
diff --git a/packages/interface/src/pages/rounds/[roundId]/applications/index.tsx b/packages/interface/src/pages/rounds/[roundId]/applications/index.tsx new file mode 100644 index 00000000..e3417942 --- /dev/null +++ b/packages/interface/src/pages/rounds/[roundId]/applications/index.tsx @@ -0,0 +1,21 @@ +import { ApplicationsToApprove } from "~/features/applications/components/ApplicationsToApprove"; +import { AdminLayout } from "~/layouts/AdminLayout"; + +import type { GetServerSideProps } from "next"; + +interface IApplicationsPageProps { + roundId: string; +} + +const ApplicationsPage = ({ roundId }: IApplicationsPageProps): JSX.Element => ( + + + +); + +export default ApplicationsPage; + +export const getServerSideProps: GetServerSideProps = async ({ query: { roundId } }) => + Promise.resolve({ + props: { roundId }, + }); diff --git a/packages/interface/src/pages/applications/new.tsx b/packages/interface/src/pages/rounds/[roundId]/applications/new.tsx similarity index 79% rename from packages/interface/src/pages/applications/new.tsx rename to packages/interface/src/pages/rounds/[roundId]/applications/new.tsx index bf41121f..b13de9a5 100644 --- a/packages/interface/src/pages/applications/new.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/applications/new.tsx @@ -4,11 +4,15 @@ import { Alert } from "~/components/ui/Alert"; import { Heading } from "~/components/ui/Heading"; import { ApplicationForm } from "~/features/applications/components/ApplicationForm"; import { Layout } from "~/layouts/DefaultLayout"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; -const NewProjectPage = (): JSX.Element => { - const state = useAppState(); +interface INewProjectPageProps { + roundId: string; +} + +const NewProjectPage = ({ roundId }: INewProjectPageProps): JSX.Element => { + const state = useRoundState(roundId); return ( @@ -34,10 +38,10 @@ const NewProjectPage = (): JSX.Element => { Applications can be edited and approved until the Application period ends.

- {state !== EAppState.APPLICATION ? ( + {state !== ERoundState.APPLICATION ? ( ) : ( - + )}
diff --git a/packages/interface/src/pages/ballot/confirmation.tsx b/packages/interface/src/pages/rounds/[roundId]/ballot/confirmation.tsx similarity index 72% rename from packages/interface/src/pages/ballot/confirmation.tsx rename to packages/interface/src/pages/rounds/[roundId]/ballot/confirmation.tsx index f5ad70e6..5d4859e9 100644 --- a/packages/interface/src/pages/ballot/confirmation.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/ballot/confirmation.tsx @@ -6,7 +6,11 @@ import { useBallot } from "~/contexts/Ballot"; import { BallotConfirmation } from "~/features/ballot/components/BallotConfirmation"; import { Layout } from "~/layouts/DefaultLayout"; -const BallotConfirmationPage = (): JSX.Element | null => { +interface IBallotConfirmationPageProps { + roundId: string; +} + +const BallotConfirmationPage = ({ roundId }: IBallotConfirmationPageProps): JSX.Element | null => { const [isLoading, setIsLoading] = useState(true); const { ballot, isLoading: isBallotLoading } = useBallot(); @@ -28,7 +32,11 @@ const BallotConfirmationPage = (): JSX.Element | null => { manageDisplay(); }, [manageDisplay]); - return {isLoading ? : }; + return ( + + {isLoading ? : } + + ); }; export default BallotConfirmationPage; diff --git a/packages/interface/src/pages/ballot/index.tsx b/packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx similarity index 77% rename from packages/interface/src/pages/ballot/index.tsx rename to packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx index 9957677c..82e48bd2 100644 --- a/packages/interface/src/pages/ballot/index.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx @@ -15,10 +15,16 @@ import { AllocationFormWrapper } from "~/features/ballot/components/AllocationFo import { BallotSchema } from "~/features/ballot/types"; import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import { formatNumber } from "~/utils/formatNumber"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; -const ClearBallot = (): JSX.Element | null => { +import type { GetServerSideProps } from "next"; + +interface IClearBallotProps { + roundId: string; +} + +const ClearBallot = ({ roundId }: IClearBallotProps): JSX.Element | null => { const form = useFormContext(); const [isOpen, setOpen] = useState(false); const { deleteBallot } = useBallot(); @@ -27,7 +33,7 @@ const ClearBallot = (): JSX.Element | null => { setOpen(true); }, [setOpen]); - if ([EAppState.TALLYING, EAppState.RESULTS].includes(useAppState())) { + if ([ERoundState.TALLYING, ERoundState.RESULTS].includes(useRoundState(roundId))) { return null; } @@ -81,8 +87,12 @@ const EmptyBallot = (): JSX.Element => (
); -const BallotAllocationForm = (): JSX.Element => { - const appState = useAppState(); +interface IBallotAllocationFormProps { + roundId: string; +} + +const BallotAllocationForm = ({ roundId }: IBallotAllocationFormProps): JSX.Element => { + const roundState = useRoundState(roundId); const { ballot, sumBallot } = useBallot(); const { initialVoiceCredits } = useMaci(); @@ -102,12 +112,12 @@ const BallotAllocationForm = (): JSX.Element => { )} -
{ballot.votes.length ? : null}
+
{ballot.votes.length ? : null}
{ballot.votes.length ? ( - + ) : ( )} @@ -123,11 +133,15 @@ const BallotAllocationForm = (): JSX.Element => { ); }; -const BallotPage = (): JSX.Element => { +interface IBallotPageProps { + roundId: string; +} + +const BallotPage = ({ roundId }: IBallotPageProps): JSX.Element => { const { address, isConnecting } = useAccount(); const { ballot, sumBallot } = useBallot(); const router = useRouter(); - const appState = useAppState(); + const roundState = useRoundState(roundId); useEffect(() => { if (!address && !isConnecting) { @@ -140,14 +154,14 @@ const BallotPage = (): JSX.Element => { }, [sumBallot]); return ( - - {appState === EAppState.VOTING && ( + + {roundState === ERoundState.VOTING && ( - + )} - {appState !== EAppState.VOTING && ( + {roundState !== ERoundState.VOTING && (
You can only vote during the voting period.
)}
@@ -155,3 +169,8 @@ const BallotPage = (): JSX.Element => { }; export default BallotPage; + +export const getServerSideProps: GetServerSideProps = async ({ query: { roundId } }) => + Promise.resolve({ + props: { roundId }, + }); diff --git a/packages/interface/src/pages/rounds/[roundId]/index.tsx b/packages/interface/src/pages/rounds/[roundId]/index.tsx new file mode 100644 index 00000000..674fea0f --- /dev/null +++ b/packages/interface/src/pages/rounds/[roundId]/index.tsx @@ -0,0 +1,21 @@ +import { Projects } from "~/features/rounds/components/Projects"; +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; + +import type { GetServerSideProps } from "next"; + +export interface IRoundsPageProps { + roundId: string; +} + +const RoundsPage = ({ roundId }: IRoundsPageProps): JSX.Element => ( + + + +); + +export default RoundsPage; + +export const getServerSideProps: GetServerSideProps = async ({ query: { roundId } }) => + Promise.resolve({ + props: { roundId }, + }); diff --git a/packages/interface/src/pages/stats/index.tsx b/packages/interface/src/pages/rounds/[roundId]/stats/index.tsx similarity index 73% rename from packages/interface/src/pages/stats/index.tsx rename to packages/interface/src/pages/rounds/[roundId]/stats/index.tsx index f28e9274..bb6fca98 100644 --- a/packages/interface/src/pages/stats/index.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/stats/index.tsx @@ -6,13 +6,15 @@ import { useAccount } from "wagmi"; import { ConnectButton } from "~/components/ConnectButton"; import { Alert } from "~/components/ui/Alert"; import { Heading } from "~/components/ui/Heading"; -import { config } from "~/config"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { useProjectCount, useProjectsResults, useResults } from "~/hooks/useResults"; import { Layout } from "~/layouts/DefaultLayout"; import { formatNumber } from "~/utils/formatNumber"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; + +import type { GetServerSideProps } from "next"; const ResultsChart = dynamic(async () => import("~/features/results/components/Chart"), { ssr: false }); @@ -26,11 +28,15 @@ const Stat = ({ title, children = null }: PropsWithChildren<{ title: string }>)
); -const Stats = () => { +interface IStatsProps { + roundId: string; +} + +const Stats = ({ roundId }: IStatsProps) => { const { isLoading, pollData } = useMaci(); - const results = useResults(pollData); - const count = useProjectCount(); - const { data: projectsResults } = useProjectsResults(pollData); + const results = useResults(roundId, pollData); + const count = useProjectCount(roundId); + const { data: projectsResults } = useProjectsResults(roundId, pollData); const { isConnected } = useAccount(); const { averageVotes, projects = {} } = results.data ?? {}; @@ -87,18 +93,24 @@ const Stats = () => { ); }; -const StatsPage = (): JSX.Element => { - const appState = useAppState(); - const duration = config.resultsAt && differenceInDays(config.resultsAt, new Date()); +interface IStatsPageProps { + roundId: string; +} + +const StatsPage = ({ roundId }: IStatsPageProps): JSX.Element => { + const roundState = useRoundState(roundId); + const { getRound } = useRound(); + const round = getRound(roundId); + const duration = round?.votingEndsAt && differenceInDays(round.votingEndsAt, new Date()); return ( - + Stats - {appState === EAppState.RESULTS ? ( - + {roundState === ERoundState.RESULTS ? ( + ) : ( The results will be revealed in
{duration && duration > 0 ? duration : 0}
@@ -110,3 +122,8 @@ const StatsPage = (): JSX.Element => { }; export default StatsPage; + +export const getServerSideProps: GetServerSideProps = async ({ query: { roundId } }) => + Promise.resolve({ + props: { roundId }, + }); diff --git a/packages/interface/src/pages/signup/index.tsx b/packages/interface/src/pages/signup/index.tsx deleted file mode 100644 index 4e6ccc35..00000000 --- a/packages/interface/src/pages/signup/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { format } from "date-fns"; -import Link from "next/link"; -import { useAccount } from "wagmi"; - -import { ConnectButton } from "~/components/ConnectButton"; -import { EligibilityDialog } from "~/components/EligibilityDialog"; -import { Info } from "~/components/Info"; -import { JoinButton } from "~/components/JoinButton"; -import { Button } from "~/components/ui/Button"; -import { Heading } from "~/components/ui/Heading"; -import { config } from "~/config"; -import { useMaci } from "~/contexts/Maci"; -import { FAQList } from "~/features/signup/components/FaqList"; -import { Layout } from "~/layouts/DefaultLayout"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; - -const SignupPage = (): JSX.Element => { - const { isConnected } = useAccount(); - const { isRegistered } = useMaci(); - const appState = useAppState(); - - return ( - - - -
- - {config.eventName} - - - - {config.roundId.toUpperCase()} - - -

- {config.startsAt && format(config.startsAt, "d MMMM, yyyy")} - - - - - {config.resultsAt && format(config.resultsAt, "d MMMM, yyyy")} -

- - {!isConnected && } - - {isConnected && appState === EAppState.APPLICATION && ( - - )} - - {isConnected && isRegistered && appState === EAppState.VOTING && ( - - )} - - {isConnected && !isRegistered && } - -
- -
-
- - -
- ); -}; - -export default SignupPage; diff --git a/packages/interface/src/providers/index.tsx b/packages/interface/src/providers/index.tsx index aa45e622..fdf56b2e 100644 --- a/packages/interface/src/providers/index.tsx +++ b/packages/interface/src/providers/index.tsx @@ -8,6 +8,7 @@ import { Toaster } from "~/components/Toaster"; import * as appConfig from "~/config"; import { BallotProvider } from "~/contexts/Ballot"; import { MaciProvider } from "~/contexts/Maci"; +import { RoundProvider } from "~/contexts/Round"; const theme = lightTheme(); @@ -37,11 +38,13 @@ export const Providers = ({ children }: PropsWithChildren): JSX.Element => { - - {children} + + + {children} - - + + + diff --git a/packages/interface/src/server/api/routers/applications.ts b/packages/interface/src/server/api/routers/applications.ts index c1c90d76..d264da67 100644 --- a/packages/interface/src/server/api/routers/applications.ts +++ b/packages/interface/src/server/api/routers/applications.ts @@ -8,23 +8,29 @@ import { createDataFilter } from "~/utils/fetchAttestationsUtils"; export const FilterSchema = z.object({ limit: z.number().default(3 * 8), cursor: z.number().default(0), + roundId: z.string(), }); export const applicationsRouter = createTRPCRouter({ - approvals: publicProcedure.input(z.object({ ids: z.array(z.string()).optional() })).query(async ({ input }) => - fetchAttestations([eas.schemas.approval], { - where: { - attester: { equals: config.admin }, - refUID: input.ids ? { in: input.ids } : undefined, - AND: [createDataFilter("type", "bytes32", "application"), createDataFilter("round", "bytes32", config.roundId)], - }, - }), - ), - list: publicProcedure.input(FilterSchema).query(async () => + approvals: publicProcedure + .input(z.object({ ids: z.array(z.string()).optional(), roundId: z.string() })) + .query(async ({ input }) => + fetchAttestations([eas.schemas.approval], { + where: { + attester: { equals: config.admin }, + refUID: input.ids ? { in: input.ids } : undefined, + AND: [ + createDataFilter("type", "bytes32", "application"), + createDataFilter("round", "bytes32", input.roundId), + ], + }, + }), + ), + list: publicProcedure.input(FilterSchema).query(async ({ input }) => fetchAttestations([eas.schemas.metadata], { orderBy: [{ time: "desc" }], where: { - AND: [createDataFilter("type", "bytes32", "application"), createDataFilter("round", "bytes32", config.roundId)], + AND: [createDataFilter("type", "bytes32", "application"), createDataFilter("round", "bytes32", input.roundId)], }, }), ), diff --git a/packages/interface/src/server/api/routers/projects.ts b/packages/interface/src/server/api/routers/projects.ts index fbb5d2e0..9a2bd4f3 100644 --- a/packages/interface/src/server/api/routers/projects.ts +++ b/packages/interface/src/server/api/routers/projects.ts @@ -11,11 +11,11 @@ import { fetchMetadata } from "~/utils/fetchMetadata"; import type { Attestation } from "~/utils/types"; export const projectsRouter = createTRPCRouter({ - count: publicProcedure.query(async () => + count: publicProcedure.input(z.object({ roundId: z.string() })).query(async ({ input }) => fetchAttestations([eas.schemas.approval], { where: { attester: { equals: config.admin }, - AND: [createDataFilter("type", "bytes32", "application"), createDataFilter("round", "bytes32", config.roundId)], + AND: [createDataFilter("type", "bytes32", "application"), createDataFilter("round", "bytes32", input.roundId)], }, }).then((attestations = []) => // Handle multiple approvals of an application - group by refUID @@ -46,7 +46,7 @@ export const projectsRouter = createTRPCRouter({ search: publicProcedure.input(FilterSchema).query(async ({ input }) => { const filters = [ createDataFilter("type", "bytes32", "application"), - createDataFilter("round", "bytes32", config.roundId), + createDataFilter("round", "bytes32", input.roundId), ]; if (input.search) { @@ -107,10 +107,10 @@ export const projectsRouter = createTRPCRouter({ .then((projects) => projects.reduce((acc, x) => ({ ...acc, [x.projectId]: x.payoutAddress }), {})), ), - allApproved: publicProcedure.query(async () => { + allApproved: publicProcedure.input(z.object({ roundId: z.string() })).query(async ({ input }) => { const filters = [ createDataFilter("type", "bytes32", "application"), - createDataFilter("round", "bytes32", config.roundId), + createDataFilter("round", "bytes32", input.roundId), ]; return fetchAttestations([eas.schemas.approval], { @@ -132,11 +132,8 @@ export const projectsRouter = createTRPCRouter({ }), }); -export async function getAllApprovedProjects(): Promise { - const filters = [ - createDataFilter("type", "bytes32", "application"), - createDataFilter("round", "bytes32", config.roundId), - ]; +export async function getAllApprovedProjects({ roundId }: { roundId: string }): Promise { + const filters = [createDataFilter("type", "bytes32", "application"), createDataFilter("round", "bytes32", roundId)]; return fetchAttestations([eas.schemas.approval], { where: { diff --git a/packages/interface/src/server/api/routers/results.ts b/packages/interface/src/server/api/routers/results.ts index cdb876ac..d231b2ca 100644 --- a/packages/interface/src/server/api/routers/results.ts +++ b/packages/interface/src/server/api/routers/results.ts @@ -11,40 +11,45 @@ import { getAllApprovedProjects } from "./projects"; export const resultsRouter = createTRPCRouter({ votes: publicProcedure - .input(z.object({ pollId: z.string().nullish() })) - .query(async ({ input }) => calculateMaciResults(input.pollId)), + .input(z.object({ roundId: z.string(), pollId: z.string().nullish() })) + .query(async ({ input }) => calculateMaciResults(input.roundId, input.pollId)), project: publicProcedure - .input(z.object({ id: z.string(), pollId: z.string().nullish() })) + .input(z.object({ id: z.string(), roundId: z.string(), pollId: z.string().nullish() })) .query(async ({ input }) => { - const { projects } = await calculateMaciResults(input.pollId); + const { projects } = await calculateMaciResults(input.roundId, input.pollId); return { amount: projects[input.id]?.votes ?? 0, }; }), - projects: publicProcedure.input(FilterSchema.extend({ pollId: z.string().nullish() })).query(async ({ input }) => { - const { projects } = await calculateMaciResults(input.pollId); - - const sortedIDs = Object.entries(projects) - .sort((a, b) => b[1].votes - a[1].votes) - .map(([id]) => id) - .slice(input.cursor * input.limit, input.cursor * input.limit + input.limit); - - return fetchAttestations([eas.schemas.metadata], { - where: { - id: { in: sortedIDs }, - }, - }).then((attestations) => - // Results aren't returned from EAS in the same order as the `where: { in: sortedIDs }` - // Sort the attestations based on the sorted array - attestations.sort((a, b) => sortedIDs.indexOf(a.id) - sortedIDs.indexOf(b.id)), - ); - }), + projects: publicProcedure + .input(FilterSchema.extend({ roundId: z.string(), pollId: z.string().nullish() })) + .query(async ({ input }) => { + const { projects } = await calculateMaciResults(input.roundId, input.pollId); + + const sortedIDs = Object.entries(projects) + .sort((a, b) => b[1].votes - a[1].votes) + .map(([id]) => id) + .slice(input.cursor * input.limit, input.cursor * input.limit + input.limit); + + return fetchAttestations([eas.schemas.metadata], { + where: { + id: { in: sortedIDs }, + }, + }).then((attestations) => + // Results aren't returned from EAS in the same order as the `where: { in: sortedIDs }` + // Sort the attestations based on the sorted array + attestations.sort((a, b) => sortedIDs.indexOf(a.id) - sortedIDs.indexOf(b.id)), + ); + }), }); -export async function calculateMaciResults(pollId?: string | null): Promise<{ +export async function calculateMaciResults( + roundId: string, + pollId?: string | null, +): Promise<{ averageVotes: number; projects: Record; }> { @@ -56,7 +61,7 @@ export async function calculateMaciResults(pollId?: string | null): Promise<{ fetch(`${config.tallyUrl}/tally-${pollId}.json`) .then((res) => res.json() as Promise) .catch(() => undefined), - getAllApprovedProjects(), + getAllApprovedProjects({ roundId }), ]); if (!tallyData) { diff --git a/packages/interface/src/server/api/routers/voters.ts b/packages/interface/src/server/api/routers/voters.ts index d3f0b2dc..cb8173f8 100644 --- a/packages/interface/src/server/api/routers/voters.ts +++ b/packages/interface/src/server/api/routers/voters.ts @@ -1,10 +1,11 @@ import { z } from "zod"; -import { config, eas } from "~/config"; +import { eas } from "~/config"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { fetchAttestations, fetchApprovedVoter, fetchApprovedVoterAttestations } from "~/utils/fetchAttestations"; import { createDataFilter } from "~/utils/fetchAttestationsUtils"; +/// TODO: change to filter with event name instead of roundId export const FilterSchema = z.object({ limit: z.number().default(3 * 8), cursor: z.number().default(0), @@ -22,7 +23,7 @@ export const votersRouter = createTRPCRouter({ list: publicProcedure.input(FilterSchema).query(async () => fetchAttestations([eas.schemas.approval], { where: { - AND: [createDataFilter("type", "bytes32", "voter"), createDataFilter("round", "bytes32", config.roundId)], + AND: [createDataFilter("type", "bytes32", "voter")], }, }), ), diff --git a/packages/interface/src/utils/fetchAttestations.ts b/packages/interface/src/utils/fetchAttestations.ts index baaceafb..dfd52630 100644 --- a/packages/interface/src/utils/fetchAttestations.ts +++ b/packages/interface/src/utils/fetchAttestations.ts @@ -1,4 +1,4 @@ -import { config, eas } from "~/config"; +import { eas } from "~/config"; import { createCachedFetch } from "./fetch"; import { parseAttestation, createDataFilter } from "./fetchAttestationsUtils"; @@ -6,9 +6,8 @@ import { type AttestationWithMetadata, type AttestationsFilter, type Attestation const cachedFetch = createCachedFetch({ ttl: 1000 * 60 * 10 }); +/// TODO: add roundId as one of the filter export async function fetchAttestations(schema: string[], filter?: AttestationsFilter): Promise { - const startsAt = config.startsAt && Math.floor(+config.startsAt / 1000); - return cachedFetch<{ attestations: AttestationWithMetadata[] }>(eas.url, { method: "POST", body: JSON.stringify({ @@ -18,7 +17,6 @@ export async function fetchAttestations(schema: string[], filter?: AttestationsF where: { schemaId: { in: schema }, revoked: { equals: false }, - time: { gte: startsAt }, ...filter?.where, }, }, diff --git a/packages/interface/src/utils/fetchAttestationsWithoutCache.ts b/packages/interface/src/utils/fetchAttestationsWithoutCache.ts index 3f328971..7d1d0c93 100644 --- a/packages/interface/src/utils/fetchAttestationsWithoutCache.ts +++ b/packages/interface/src/utils/fetchAttestationsWithoutCache.ts @@ -3,9 +3,7 @@ import { config, eas } from "~/config"; import { parseAttestation, createDataFilter } from "./fetchAttestationsUtils"; import { type AttestationWithMetadata, type Attestation, AttestationsQuery } from "./types"; -export async function fetchApprovedApplications(ids?: string[]): Promise { - const startsAt = config.startsAt && Math.floor(+config.startsAt / 1000); - +export async function fetchApprovedApplications(roundId: string, ids?: string[]): Promise { return fetch(eas.url, { method: "POST", headers: { @@ -17,13 +15,9 @@ export async function fetchApprovedApplications(ids?: string[]): Promise { +export const useRoundState = (roundId: string): ERoundState => { const now = new Date(); - const { votingEndsAt, pollData, tallyData } = useMaci(); + const { getRound } = useRound(); + const round = getRound(roundId); - if (config.registrationEndsAt && isAfter(config.registrationEndsAt, now)) { - return EAppState.APPLICATION; + if (!round) { + return ERoundState.DEFAULT; } - if (isAfter(votingEndsAt, now)) { - return EAppState.VOTING; + if (round.registrationEndsAt && isAfter(round.registrationEndsAt, now)) { + return ERoundState.APPLICATION; } - if (!pollData?.isMerged || !tallyData) { - return EAppState.TALLYING; + if (round.votingEndsAt && isAfter(round.votingEndsAt, now)) { + return ERoundState.VOTING; } - return EAppState.RESULTS; + if (round.votingEndsAt && isAfter(now, round.votingEndsAt) && !round.tallyURL) { + return ERoundState.TALLYING; + } + + if (round.tallyURL) { + return ERoundState.RESULTS; + } + + return ERoundState.DEFAULT; }; diff --git a/packages/interface/src/utils/time.ts b/packages/interface/src/utils/time.ts index c1749050..7da2b432 100644 --- a/packages/interface/src/utils/time.ts +++ b/packages/interface/src/utils/time.ts @@ -10,3 +10,17 @@ export const calculateTimeLeft = (date: Date): [number, number, number, number] }; export const formatDate = (date: Date | number): string => format(date, "dd MMM yyyy HH:mm"); + +export function formatPeriod({ start, end }: { start: Date; end: Date }): string { + const fullFormat = "d MMM yyyy"; + + if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) { + return `${start.getDate()} - ${format(end, fullFormat)}`; + } + + if (start.getFullYear() === end.getFullYear()) { + return `${format(start, "d MMM")} - ${format(end, fullFormat)}`; + } + + return `${format(start, fullFormat)} - ${format(end, fullFormat)}`; +} diff --git a/packages/interface/src/utils/types.ts b/packages/interface/src/utils/types.ts index 036951d1..c7f5a494 100644 --- a/packages/interface/src/utils/types.ts +++ b/packages/interface/src/utils/types.ts @@ -1,11 +1,12 @@ import { type Address } from "viem"; -export enum EAppState { +export enum ERoundState { LOADING = "LOADING", APPLICATION = "APPLICATION", VOTING = "VOTING", TALLYING = "TALLYING", RESULTS = "RESULTS", + DEFAULT = "DEFAULT", } export enum EInfoCardState { From 7850be1c8f3c6b14882a95aadff1ca7d1c709eec Mon Sep 17 00:00:00 2001 From: yu-zhen Date: Sat, 5 Oct 2024 02:41:23 +0900 Subject: [PATCH 2/2] feat: fetch rounds data from subgraph and update on the interface --- .../scripts/uploadRoundMetadata.ts | 7 ++ packages/interface/.env.example | 7 +- .../src/components/EligibilityDialog.tsx | 20 ++-- packages/interface/src/components/Info.tsx | 8 +- .../interface/src/components/VotingInfo.tsx | 17 ++- packages/interface/src/config.ts | 1 - packages/interface/src/contexts/Ballot.tsx | 8 +- packages/interface/src/contexts/Maci.tsx | 107 +----------------- packages/interface/src/contexts/Round.tsx | 54 ++++++--- packages/interface/src/contexts/types.ts | 15 ++- packages/interface/src/env.js | 4 - .../components/AllocationFormWrapper.tsx | 9 +- .../ballot/components/BallotConfirmation.tsx | 4 +- .../ballot/components/SubmitBallotButton.tsx | 10 +- .../ballot/components/VotingEndsIn.tsx | 17 ++- .../projects/components/ProjectAwarded.tsx | 11 +- .../projects/components/ProjectDetails.tsx | 2 +- .../projects/components/ProjectsResults.tsx | 11 +- .../projects/components/VotingWidget.tsx | 9 +- .../projects/hooks/useSelectProjects.ts | 12 +- .../features/rounds/components/Projects.tsx | 11 +- .../features/rounds/components/RoundItem.tsx | 4 +- .../features/rounds/components/RoundsList.tsx | 4 +- .../src/features/rounds/types/index.ts | 12 -- packages/interface/src/hooks/useResults.ts | 19 +--- .../interface/src/layouts/DefaultLayout.tsx | 12 +- packages/interface/src/pages/index.tsx | 4 +- .../pages/rounds/[roundId]/ballot/index.tsx | 2 +- .../pages/rounds/[roundId]/stats/index.tsx | 20 ++-- .../interface/src/server/api/routers/maci.ts | 48 +++++++- .../src/server/api/routers/results.ts | 22 ++-- packages/interface/src/utils/fetchPoll.ts | 69 +++++++---- packages/interface/src/utils/state.ts | 13 ++- packages/interface/src/utils/types.ts | 36 +++++- 34 files changed, 331 insertions(+), 278 deletions(-) delete mode 100644 packages/interface/src/features/rounds/types/index.ts diff --git a/packages/coordinator/scripts/uploadRoundMetadata.ts b/packages/coordinator/scripts/uploadRoundMetadata.ts index e62b5ef1..86fb5512 100644 --- a/packages/coordinator/scripts/uploadRoundMetadata.ts +++ b/packages/coordinator/scripts/uploadRoundMetadata.ts @@ -13,6 +13,7 @@ export interface RoundMetadata { registrationEndsAt: Date; votingStartsAt: Date; votingEndsAt: Date; + tallyFile: string; } interface IUploadMetadataProps { @@ -29,9 +30,11 @@ function isValidDate(formattedDateStr: string) { } export async function uploadRoundMetadata({ data, name }: IUploadMetadataProps): Promise { + // 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; @@ -139,6 +142,9 @@ export async function collectMetadata(): Promise { 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, @@ -146,6 +152,7 @@ export async function collectMetadata(): Promise { registrationEndsAt, votingStartsAt, votingEndsAt, + tallyFile: `${vercelStoragePrefix}/tally-${roundId}.json`, }; } diff --git a/packages/interface/.env.example b/packages/interface/.env.example index d769c805..6738a93f 100644 --- a/packages/interface/.env.example +++ b/packages/interface/.env.example @@ -72,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 diff --git a/packages/interface/src/components/EligibilityDialog.tsx b/packages/interface/src/components/EligibilityDialog.tsx index b27cd99b..32d6caf9 100644 --- a/packages/interface/src/components/EligibilityDialog.tsx +++ b/packages/interface/src/components/EligibilityDialog.tsx @@ -4,12 +4,13 @@ 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 { useRoundState } from "~/utils/state"; import { ERoundState, jsonPCD } from "~/utils/types"; @@ -25,21 +26,20 @@ interface IEligibilityDialogProps { export const EligibilityDialog = ({ roundId = "" }: IEligibilityDialogProps): JSX.Element | null => { const { address } = useAccount(); const { disconnect } = useDisconnect(); + const { getRoundByRoundId } = useRound(); const [openDialog, setOpenDialog] = useState(!!address); - const { - onSignup, - isEligibleToVote, - isRegistered, - initialVoiceCredits, - votingEndsAt, - gatekeeperTrait, - storeZupassProof, - } = useMaci(); + const { onSignup, isEligibleToVote, isRegistered, initialVoiceCredits, gatekeeperTrait, storeZupassProof } = + useMaci(); const router = useRouter(); 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(); diff --git a/packages/interface/src/components/Info.tsx b/packages/interface/src/components/Info.tsx index 46ecd5e2..f1907ada 100644 --- a/packages/interface/src/components/Info.tsx +++ b/packages/interface/src/components/Info.tsx @@ -40,8 +40,8 @@ export const Info = ({ showBallot = false, }: IInfoProps): JSX.Element => { const roundState = useRoundState(roundId); - const { getRound } = useRound(); - const round = getRound(roundId); + const { getRoundByRoundId } = useRound(); + const round = getRoundByRoundId(roundId); const { asPath } = useRouter(); const steps = [ @@ -76,9 +76,9 @@ export const Info = ({ {showRoundInfo && } - {showBallot && } + {showBallot && } - {showRoundInfo && roundState === ERoundState.VOTING && } + {showRoundInfo && roundState === ERoundState.VOTING && } {showAppState && steps.map((step) => ( diff --git a/packages/interface/src/components/VotingInfo.tsx b/packages/interface/src/components/VotingInfo.tsx index 444df2a0..20ed17e2 100644 --- a/packages/interface/src/components/VotingInfo.tsx +++ b/packages/interface/src/components/VotingInfo.tsx @@ -1,15 +1,26 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import { useHarmonicIntervalFn } from "react-use"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { calculateTimeLeft } from "~/utils/time"; import { TimeSlot } from "./TimeSlot"; -export const VotingInfo = (): JSX.Element => { - const { isLoading, votingEndsAt } = useMaci(); +interface IVotingInfoProps { + roundId: string; +} + +export const VotingInfo = ({ roundId }: IVotingInfoProps): JSX.Element => { + const { isLoading } = useMaci(); + const { getRoundByRoundId } = useRound(); const [timeLeft, setTimeLeft] = useState<[number, number, number, number]>([0, 0, 0, 0]); + const votingEndsAt = useMemo(() => { + const round = getRoundByRoundId(roundId); + return round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(); + }, [getRoundByRoundId, roundId]); + useHarmonicIntervalFn(() => { setTimeLeft(calculateTimeLeft(votingEndsAt)); }, 1000); diff --git a/packages/interface/src/config.ts b/packages/interface/src/config.ts index c93d938d..5c34f84c 100644 --- a/packages/interface/src/config.ts +++ b/packages/interface/src/config.ts @@ -82,7 +82,6 @@ export const config = { maciAddress: process.env.NEXT_PUBLIC_MACI_ADDRESS, maciStartBlock: Number(process.env.NEXT_PUBLIC_MACI_START_BLOCK ?? 0), maciSubgraphUrl: process.env.NEXT_PUBLIC_MACI_SUBGRAPH_URL ?? "", - tallyUrl: process.env.NEXT_PUBLIC_TALLY_URL, roundOrganizer: process.env.NEXT_PUBLIC_ROUND_ORGANIZER ?? "PSE", roundLogo: process.env.NEXT_PUBLIC_ROUND_LOGO, semaphoreSubgraphUrl: process.env.NEXT_PUBLIC_SEMAPHORE_SUBGRAPH, diff --git a/packages/interface/src/contexts/Ballot.tsx b/packages/interface/src/contexts/Ballot.tsx index 5a53aec3..66a896a4 100644 --- a/packages/interface/src/contexts/Ballot.tsx +++ b/packages/interface/src/contexts/Ballot.tsx @@ -4,7 +4,7 @@ import { useAccount } from "wagmi"; import type { BallotContextType, BallotProviderProps } from "./types"; import type { Ballot, Vote } from "~/features/ballot/types"; -import { useMaci } from "./Maci"; +import { useRound } from "./Round"; export const BallotContext = createContext(undefined); @@ -13,9 +13,9 @@ const defaultBallot = { votes: [], published: false, edited: false }; export const BallotProvider: React.FC = ({ children }: BallotProviderProps) => { const [ballot, setBallot] = useState(defaultBallot); const [isLoading, setLoading] = useState(true); + const { rounds } = useRound(); const { isDisconnected } = useAccount(); - const { pollData } = useMaci(); // when summing the ballot we take the individual vote and square it // if the mode is quadratic voting, otherwise we just add the amount @@ -23,9 +23,9 @@ export const BallotProvider: React.FC = ({ children }: Ball (votes?: Vote[]) => (votes ?? []).reduce((sum, x) => { const amount = !Number.isNaN(Number(x.amount)) ? Number(x.amount) : 0; - return sum + (pollData && pollData.mode.toString() === "0" ? amount ** 2 : amount); + return sum + (rounds && rounds.length > 0 && rounds[0]?.mode.toString() === "0" ? amount ** 2 : amount); }, 0), - [pollData], + [rounds], ); const ballotContains = useCallback((id: string) => ballot.votes.find((v) => v.projectId === id), [ballot]); diff --git a/packages/interface/src/contexts/Maci.tsx b/packages/interface/src/contexts/Maci.tsx index 050292fd..253808f1 100644 --- a/packages/interface/src/contexts/Maci.tsx +++ b/packages/interface/src/contexts/Maci.tsx @@ -3,15 +3,11 @@ import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; import { type StandardMerkleTreeData } from "@openzeppelin/merkle-tree/dist/standard"; import { type ZKEdDSAEventTicketPCD } from "@pcd/zk-eddsa-event-ticket-pcd/ZKEdDSAEventTicketPCD"; import { Identity } from "@semaphore-protocol/core"; -import { isAfter } from "date-fns"; -import { type Signer, BrowserProvider, AbiCoder } from "ethers"; +import { type Signer, AbiCoder } from "ethers"; import { signup, isRegisteredUser, publishBatch, - type TallyData, - type IGetPollData, - getPoll, genKeyPair, GatekeeperTrait, getGatekeeperTrait, @@ -29,7 +25,6 @@ import { getSemaphoreProof } from "~/utils/semaphore"; import type { IVoteArgs, MaciContextType, MaciProviderProps } from "./types"; import type { PCD } from "@pcd/pcd-types"; -import type { EIP1193Provider } from "viem"; import type { Attestation } from "~/utils/types"; export const MaciContext = createContext(undefined); @@ -48,8 +43,6 @@ export const MaciProvider: React.FC = ({ children }: MaciProv const [initialVoiceCredits, setInitialVoiceCredits] = useState(0); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); - const [pollData, setPollData] = useState(); - const [tallyData, setTallyData] = useState(); const [treeData, setTreeData] = useState>(); const [semaphoreIdentity, setSemaphoreIdentity] = useState(); @@ -68,8 +61,6 @@ export const MaciProvider: React.FC = ({ children }: MaciProv { enabled: Boolean(maciPubKey && config.maciSubgraphUrl) }, ); - const poll = api.maci.poll.useQuery(undefined, { enabled: Boolean(config.maciSubgraphUrl) }); - // fetch the gatekeeper trait useEffect(() => { if (!signer) { @@ -224,15 +215,6 @@ export const MaciProvider: React.FC = ({ children }: MaciProv } }, [setMaciPrivKey, setMaciPubKey, setSemaphoreIdentity]); - // on load we fetch the data from the poll - useEffect(() => { - if (poll.data) { - return; - } - - poll.refetch().catch(console.error); - }, [poll]); - // generate the maci keypair using a ECDSA signature const generateKeypair = useCallback(async () => { // if we are not connected then do not generate the key pair @@ -258,15 +240,6 @@ export const MaciProvider: React.FC = ({ children }: MaciProv [setZupassProof], ); - // memo to calculate the voting end date - const votingEndsAt = useMemo( - () => - pollData && pollData.duration !== 0 - ? new Date(Number(pollData.deployTime) * 1000 + Number(pollData.duration) * 1000) - : undefined, - [pollData?.deployTime, pollData?.duration], - ); - // function to be used to signup to MACI const onSignup = useCallback( async (onError: () => void) => { @@ -301,8 +274,8 @@ export const MaciProvider: React.FC = ({ children }: MaciProv // function to be used to vote on a poll const onVote = useCallback( - async (votes: IVoteArgs[], onError: () => Promise, onSuccess: () => Promise) => { - if (!signer || !stateIndex || !pollData) { + async (votes: IVoteArgs[], pollId: string, onError: () => Promise, onSuccess: () => Promise) => { + if (!signer || !stateIndex) { return; } @@ -327,7 +300,7 @@ export const MaciProvider: React.FC = ({ children }: MaciProv maciAddress: config.maciAddress!, publicKey: maciPubKey!, privateKey: maciPrivKey!, - pollId: BigInt(pollData.id), + pollId: BigInt(pollId), signer, }) .then(() => onSuccess()) @@ -339,7 +312,7 @@ export const MaciProvider: React.FC = ({ children }: MaciProv setIsLoading(false); }); }, - [stateIndex, pollData, maciPubKey, maciPrivKey, signer, setIsLoading, setError], + [stateIndex, maciPubKey, maciPrivKey, signer, setIsLoading, setError], ); useEffect(() => { @@ -408,69 +381,6 @@ export const MaciProvider: React.FC = ({ children }: MaciProv setInitialVoiceCredits, ]); - /// check the poll data and tally data - useEffect(() => { - setIsLoading(true); - - // if we have the subgraph url then it means we can get the poll data through there - if (config.maciSubgraphUrl) { - if (!poll.data) { - setIsLoading(false); - return; - } - - const { isMerged, id } = poll.data; - - setPollData(poll.data); - - if (isMerged) { - fetch(`${config.tallyUrl}/tally-${id}.json`) - .then((res) => res.json() as Promise) - .then((res) => { - setTallyData(res); - }) - .catch(() => undefined); - } - - setIsLoading(false); - } else { - if (!window.ethereum) { - setIsLoading(false); - return; - } - - const provider = new BrowserProvider(window.ethereum as unknown as EIP1193Provider, { - chainId: config.network.id, - name: config.network.name, - }); - - getPoll({ - maciAddress: config.maciAddress!, - provider, - }) - .then((data) => { - setPollData(data); - return data; - }) - .then(async (data) => { - if (!data.isMerged || (votingEndsAt && isAfter(votingEndsAt, new Date()))) { - return undefined; - } - - return fetch(`${config.tallyUrl}/tally-${data.id}.json`) - .then((res) => res.json() as Promise) - .then((res) => { - setTallyData(res); - }) - .catch(() => undefined); - }) - .catch(console.error) - .finally(() => { - setIsLoading(false); - }); - } - }, [signer, votingEndsAt, setIsLoading, setTallyData, setPollData, poll.data]); - /// check the tree data useEffect(() => { // if we have the tree url then it means we can get the tree data through there @@ -492,13 +402,9 @@ export const MaciProvider: React.FC = ({ children }: MaciProv isLoading, isEligibleToVote, initialVoiceCredits, - votingEndsAt, stateIndex, isRegistered: isRegistered ?? false, - pollId: pollData?.id.toString(), error, - pollData, - tallyData, maciPubKey, onSignup, onVote, @@ -510,11 +416,8 @@ export const MaciProvider: React.FC = ({ children }: MaciProv isLoading, isEligibleToVote, initialVoiceCredits, - votingEndsAt, stateIndex, isRegistered, - pollData, - tallyData, error, maciPubKey, onSignup, diff --git a/packages/interface/src/contexts/Round.tsx b/packages/interface/src/contexts/Round.tsx index a5415337..686a5e6e 100644 --- a/packages/interface/src/contexts/Round.tsx +++ b/packages/interface/src/contexts/Round.tsx @@ -1,33 +1,51 @@ -import React, { createContext, useContext, useMemo, useCallback } from "react"; +import React, { createContext, useContext, useMemo, useCallback, useEffect, useState } from "react"; + +import { config } from "~/config"; +import { api } from "~/utils/api"; import type { RoundContextType, RoundProviderProps } from "./types"; -import type { Round } from "~/features/rounds/types"; +import type { IRoundData } from "~/utils/types"; export const RoundContext = createContext(undefined); export const RoundProvider: React.FC = ({ children }: RoundProviderProps) => { - const rounds = [ - { - roundId: "open-rpgf-1", - description: "This is the description of this round, please add your own description.", - startsAt: 1723477832000, - registrationEndsAt: 1723487832000, - votingEndsAt: 1724009826000, - tallyURL: "https://upblxu2duoxmkobt.public.blob.vercel-storage.com/tally.json", - }, - ]; - - const getRound = useCallback( - (roundId: string): Round | undefined => rounds.find((round) => round.roundId === roundId), + const [isLoading, setIsLoading] = useState(false); + + const polls = api.maci.poll.useQuery(undefined, { enabled: Boolean(config.maciSubgraphUrl) }); + const rounds = api.maci.round.useQuery({ polls: polls.data ?? [] }, { enabled: Boolean(polls.data) }); + + // on load we fetch the data from the poll + useEffect(() => { + if (polls.data) { + setIsLoading(false); + return; + } + + setIsLoading(true); + // eslint-disable-next-line no-console + polls.refetch().catch(console.error); + // eslint-disable-next-line no-console + rounds.refetch().catch(console.error); + }, [polls, rounds]); + + const getRoundByRoundId = useCallback( + (roundId: string): IRoundData | undefined => rounds.data?.find((round) => round.roundId === roundId), + [rounds], + ); + + const getRoundByPollId = useCallback( + (pollId: string): IRoundData | undefined => rounds.data?.find((round) => round.pollId === pollId), [rounds], ); const value = useMemo( () => ({ - rounds, - getRound, + rounds: rounds.data, + getRoundByRoundId, + getRoundByPollId, + isLoading, }), - [rounds, getRound], + [rounds, getRoundByRoundId, getRoundByPollId, isLoading], ); return {children}; diff --git a/packages/interface/src/contexts/types.ts b/packages/interface/src/contexts/types.ts index 6a84cb09..3fdc2096 100644 --- a/packages/interface/src/contexts/types.ts +++ b/packages/interface/src/contexts/types.ts @@ -1,10 +1,10 @@ import { type StandardMerkleTreeData } from "@openzeppelin/merkle-tree/dist/standard"; -import { type TallyData, type IGetPollData, type GatekeeperTrait } from "maci-cli/sdk"; +import { type GatekeeperTrait } from "maci-cli/sdk"; import { type ReactNode } from "react"; import type { PCD } from "@pcd/pcd-types"; import type { Ballot, Vote } from "~/features/ballot/types"; -import type { Round } from "~/features/rounds/types"; +import type { IRoundData } from "~/utils/types"; export interface IVoteArgs { voteOptionIndex: bigint; @@ -15,13 +15,9 @@ export interface MaciContextType { isLoading: boolean; isEligibleToVote: boolean; initialVoiceCredits: number; - votingEndsAt: Date; stateIndex?: string; isRegistered?: boolean; - pollId?: string; error?: string; - pollData?: IGetPollData; - tallyData?: TallyData; maciPubKey?: string; gatekeeperTrait?: GatekeeperTrait; storeZupassProof: (args: PCD) => Promise; @@ -29,6 +25,7 @@ export interface MaciContextType { onSignup: (onError: () => void) => Promise; onVote: ( args: IVoteArgs[], + pollId: string, onError: () => void | Promise, onSuccess: () => void | Promise, ) => Promise; @@ -54,8 +51,10 @@ export interface BallotProviderProps { } export interface RoundContextType { - rounds: Round[]; - getRound: (roundId: string) => Round | undefined; + rounds: IRoundData[] | undefined; + getRoundByRoundId: (roundId: string) => IRoundData | undefined; + getRoundByPollId: (pollId: string) => IRoundData | undefined; + isLoading: boolean; } export interface RoundProviderProps { diff --git a/packages/interface/src/env.js b/packages/interface/src/env.js index 46adb175..7221d7c6 100644 --- a/packages/interface/src/env.js +++ b/packages/interface/src/env.js @@ -48,8 +48,6 @@ module.exports = createEnv({ NEXT_PUBLIC_MACI_START_BLOCK: z.string().optional(), NEXT_PUBLIC_MACI_SUBGRAPH_URL: z.string().url().optional(), - NEXT_PUBLIC_TALLY_URL: z.string().url(), - NEXT_PUBLIC_ROUND_LOGO: z.string().optional(), NEXT_PUBLIC_SEMAPHORE_SUBGRAPH: z.string().url().optional(), @@ -81,8 +79,6 @@ module.exports = createEnv({ NEXT_PUBLIC_MACI_START_BLOCK: process.env.NEXT_PUBLIC_MACI_START_BLOCK, NEXT_PUBLIC_MACI_SUBGRAPH_URL: process.env.NEXT_PUBLIC_MACI_SUBGRAPH_URL, - NEXT_PUBLIC_TALLY_URL: process.env.NEXT_PUBLIC_TALLY_URL, - NEXT_PUBLIC_ROUND_LOGO: process.env.NEXT_PUBLIC_ROUND_LOGO, NEXT_PUBLIC_SEMAPHORE_SUBGRAPH: process.env.NEXT_PUBLIC_SEMAPHORE_SUBGRAPH, diff --git a/packages/interface/src/features/ballot/components/AllocationFormWrapper.tsx b/packages/interface/src/features/ballot/components/AllocationFormWrapper.tsx index cdac7d42..a6c7a43e 100644 --- a/packages/interface/src/features/ballot/components/AllocationFormWrapper.tsx +++ b/packages/interface/src/features/ballot/components/AllocationFormWrapper.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { useFieldArray, useFormContext } from "react-hook-form"; import { HiOutlineTrash } from "react-icons/hi"; @@ -5,6 +6,7 @@ import { IconButton } from "~/components/ui/Button"; import { Table, Tbody, Tr, Td } from "~/components/ui/Table"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import type { Vote } from "../types"; import type { ReactNode } from "react"; @@ -13,19 +15,22 @@ import { AllocationInput } from "./AllocationInput"; import { ProjectAvatarWithName } from "./ProjectAvatarWithName"; interface AllocationFormProps { + roundId: string; disabled?: boolean; projectIsLink?: boolean; renderHeader?: () => ReactNode; } export const AllocationFormWrapper = ({ + roundId, disabled = false, projectIsLink = false, renderHeader = undefined, }: AllocationFormProps): JSX.Element => { const form = useFormContext<{ votes: Vote[] }>(); - const { initialVoiceCredits, pollId } = useMaci(); + const { initialVoiceCredits } = useMaci(); const { addToBallot: onSave, removeFromBallot: onRemove } = useBallot(); + const { getRoundByRoundId } = useRound(); const { fields, remove } = useFieldArray({ name: "votes", @@ -33,6 +38,8 @@ export const AllocationFormWrapper = ({ control: form.control, }); + const pollId = useMemo(() => getRoundByRoundId(roundId)?.pollId, [roundId, getRoundByRoundId]); + return ( {renderHeader?.()} diff --git a/packages/interface/src/features/ballot/components/BallotConfirmation.tsx b/packages/interface/src/features/ballot/components/BallotConfirmation.tsx index 5beb6b29..6a708292 100644 --- a/packages/interface/src/features/ballot/components/BallotConfirmation.tsx +++ b/packages/interface/src/features/ballot/components/BallotConfirmation.tsx @@ -35,8 +35,8 @@ export const BallotConfirmation = ({ roundId }: IBallotConfirmationProps): JSX.E const allocations = ballot.votes; const { data: projectCount } = useProjectCount(roundId); const roundState = useRoundState(roundId); - const { getRound } = useRound(); - const round = getRound(roundId); + const { getRoundByRoundId } = useRound(); + const round = getRoundByRoundId(roundId); const sum = useMemo(() => formatNumber(sumBallot(ballot.votes)), [ballot, sumBallot]); diff --git a/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx b/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx index cc69f24e..8ff3837d 100644 --- a/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx +++ b/packages/interface/src/features/ballot/components/SubmitBallotButton.tsx @@ -6,6 +6,7 @@ import { Button } from "~/components/ui/Button"; import { Dialog } from "~/components/ui/Dialog"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { useProjectIdMapping } from "~/features/projects/hooks/useProjects"; interface ISubmitBallotButtonProps { @@ -17,6 +18,7 @@ export const SubmitBallotButton = ({ roundId }: ISubmitBallotButtonProps): JSX.E const [isOpen, setOpen] = useState(false); const { onVote, isLoading, initialVoiceCredits } = useMaci(); const { ballot, publishBallot, sumBallot } = useBallot(); + const { getRoundByRoundId } = useRound(); const projectIndices = useProjectIdMapping(ballot, roundId); const ableToSubmit = useMemo( @@ -28,6 +30,8 @@ export const SubmitBallotButton = ({ roundId }: ISubmitBallotButtonProps): JSX.E toast.error("Voting error"); }, []); + const pollId = useMemo(() => getRoundByRoundId(roundId)?.pollId, [roundId, getRoundByRoundId]); + const handleSubmitBallot = useCallback(async () => { const votes = ballot.votes.map(({ amount, projectId }) => { const index = projectIndices[projectId]; @@ -41,7 +45,11 @@ export const SubmitBallotButton = ({ roundId }: ISubmitBallotButtonProps): JSX.E }; }); - await onVote(votes, onVotingError, () => { + if (!pollId) { + throw new Error("The pollId is undefined."); + } + + await onVote(votes, pollId, onVotingError, () => { publishBallot(); router.push("/ballot/confirmation"); }); diff --git a/packages/interface/src/features/ballot/components/VotingEndsIn.tsx b/packages/interface/src/features/ballot/components/VotingEndsIn.tsx index f8134acc..23568c15 100644 --- a/packages/interface/src/features/ballot/components/VotingEndsIn.tsx +++ b/packages/interface/src/features/ballot/components/VotingEndsIn.tsx @@ -1,8 +1,10 @@ +import { useMemo } from "react"; import { createGlobalState, useHarmonicIntervalFn } from "react-use"; import { tv } from "tailwind-variants"; import { createComponent } from "~/components/ui"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { calculateTimeLeft } from "~/utils/time"; const useEndDate = createGlobalState<[number, number, number, number]>([0, 0, 0, 0]); @@ -19,8 +21,19 @@ export function useVotingTimeLeft(votingEndsAt: Date): [number, number, number, const TimeSlice = createComponent("span", tv({ base: "text-gray-900" })); -export const VotingEndsIn = (): JSX.Element => { - const { isLoading, votingEndsAt } = useMaci(); +interface IVotingEndsInProps { + roundId: string; +} + +export const VotingEndsIn = ({ roundId }: IVotingEndsInProps): JSX.Element => { + const { isLoading } = useMaci(); + const { getRoundByRoundId } = useRound(); + + const votingEndsAt = useMemo(() => { + const round = getRoundByRoundId(roundId); + return round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(); + }, [roundId, getRoundByRoundId]); + const [days, hours, minutes, seconds] = useVotingTimeLeft(votingEndsAt); if (isLoading) { diff --git a/packages/interface/src/features/projects/components/ProjectAwarded.tsx b/packages/interface/src/features/projects/components/ProjectAwarded.tsx index 3da3170c..67520f90 100644 --- a/packages/interface/src/features/projects/components/ProjectAwarded.tsx +++ b/packages/interface/src/features/projects/components/ProjectAwarded.tsx @@ -1,17 +1,20 @@ import { Button } from "~/components/ui/Button"; import { config } from "~/config"; -import { useMaci } from "~/contexts/Maci"; import { useProjectResults } from "~/hooks/useResults"; import { formatNumber } from "~/utils/formatNumber"; export interface IProjectAwardedProps { roundId: string; + tallyFile?: string; id?: string; } -export const ProjectAwarded = ({ roundId, id = "" }: IProjectAwardedProps): JSX.Element | null => { - const { pollData } = useMaci(); - const amount = useProjectResults(id, roundId, pollData); +export const ProjectAwarded = ({ + roundId, + tallyFile = undefined, + id = "", +}: IProjectAwardedProps): JSX.Element | null => { + const amount = useProjectResults(id, roundId, tallyFile); if (amount.isLoading) { return null; diff --git a/packages/interface/src/features/projects/components/ProjectDetails.tsx b/packages/interface/src/features/projects/components/ProjectDetails.tsx index 1e93c2bc..a27697f3 100644 --- a/packages/interface/src/features/projects/components/ProjectDetails.tsx +++ b/packages/interface/src/features/projects/components/ProjectDetails.tsx @@ -54,7 +54,7 @@ const ProjectDetails = ({ {attestation?.name} - {roundState === ERoundState.VOTING && } + {roundState === ERoundState.VOTING && } diff --git a/packages/interface/src/features/projects/components/ProjectsResults.tsx b/packages/interface/src/features/projects/components/ProjectsResults.tsx index cc12cf3e..bdf4466f 100644 --- a/packages/interface/src/features/projects/components/ProjectsResults.tsx +++ b/packages/interface/src/features/projects/components/ProjectsResults.tsx @@ -1,10 +1,10 @@ import clsx from "clsx"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { InfiniteLoading } from "~/components/InfiniteLoading"; -import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { useResults, useProjectsResults } from "~/hooks/useResults"; import { useRoundState } from "~/utils/state"; import { ERoundState } from "~/utils/types"; @@ -19,9 +19,10 @@ interface IProjectsResultsProps { export const ProjectsResults = ({ roundId }: IProjectsResultsProps): JSX.Element => { const router = useRouter(); - const { pollData } = useMaci(); - const projects = useProjectsResults(roundId, pollData); - const results = useResults(roundId); + const { getRoundByRoundId } = useRound(); + const round = useMemo(() => getRoundByRoundId(roundId), [roundId, getRoundByRoundId]); + const projects = useProjectsResults(roundId, round?.tallyFile); + const results = useResults(roundId, round?.tallyFile); const roundState = useRoundState(roundId); const handleAction = useCallback( diff --git a/packages/interface/src/features/projects/components/VotingWidget.tsx b/packages/interface/src/features/projects/components/VotingWidget.tsx index fab111d4..b7769235 100644 --- a/packages/interface/src/features/projects/components/VotingWidget.tsx +++ b/packages/interface/src/features/projects/components/VotingWidget.tsx @@ -6,19 +6,24 @@ import { Button } from "~/components/ui/Button"; import { Input } from "~/components/ui/Input"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { EButtonState } from "../types"; interface IVotingWidgetProps { projectId: string; + roundId: string; } -export const VotingWidget = ({ projectId }: IVotingWidgetProps): JSX.Element => { - const { pollId, initialVoiceCredits } = useMaci(); +export const VotingWidget = ({ projectId, roundId }: IVotingWidgetProps): JSX.Element => { + const { initialVoiceCredits } = useMaci(); const { ballotContains, removeFromBallot, addToBallot } = useBallot(); const projectBallot = useMemo(() => ballotContains(projectId), [ballotContains, projectId]); const projectIncluded = useMemo(() => !!projectBallot, [projectBallot]); const [amount, setAmount] = useState(projectBallot?.amount); + const { getRoundByRoundId } = useRound(); + + const pollId = useMemo(() => getRoundByRoundId(roundId)?.pollId, [roundId, getRoundByRoundId]); /** * buttonState diff --git a/packages/interface/src/features/projects/hooks/useSelectProjects.ts b/packages/interface/src/features/projects/hooks/useSelectProjects.ts index b20d3f8e..6cfc7401 100644 --- a/packages/interface/src/features/projects/hooks/useSelectProjects.ts +++ b/packages/interface/src/features/projects/hooks/useSelectProjects.ts @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import { useBallot } from "~/contexts/Ballot"; -import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; export interface IUseSelectProjectsReturn { count: number; @@ -11,12 +11,18 @@ export interface IUseSelectProjectsReturn { getState: (id: string) => 0 | 1 | 2; } -export function useSelectProjects(): IUseSelectProjectsReturn { +interface IUseSelectProjectsProps { + roundId: string; +} + +export function useSelectProjects({ roundId }: IUseSelectProjectsProps): IUseSelectProjectsReturn { const { addToBallot, ballotContains } = useBallot(); - const { pollId } = useMaci(); + const { getRoundByRoundId } = useRound(); const [selected, setSelected] = useState>({}); + const pollId = useMemo(() => getRoundByRoundId(roundId)?.pollId, [roundId, getRoundByRoundId]); + const toAdd = useMemo( () => Object.keys(selected) diff --git a/packages/interface/src/features/rounds/components/Projects.tsx b/packages/interface/src/features/rounds/components/Projects.tsx index ebbd5fa6..1a5d6667 100644 --- a/packages/interface/src/features/rounds/components/Projects.tsx +++ b/packages/interface/src/features/rounds/components/Projects.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import Link from "next/link"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { FiAlertCircle } from "react-icons/fi"; import { InfiniteLoading } from "~/components/InfiniteLoading"; @@ -9,6 +9,7 @@ import { StatusBar } from "~/components/StatusBar"; import { Heading } from "~/components/ui/Heading"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; +import { useRound } from "~/contexts/Round"; import { useResults } from "~/hooks/useResults"; import { useRoundState } from "~/utils/state"; import { ERoundState } from "~/utils/types"; @@ -25,9 +26,13 @@ export const Projects = ({ roundId = "" }: IProjectsProps): JSX.Element => { const appState = useRoundState(roundId); const projects = useSearchProjects({ roundId, needApproval: appState !== ERoundState.APPLICATION }); - const { pollData, pollId, isRegistered } = useMaci(); + const { isRegistered } = useMaci(); const { addToBallot, removeFromBallot, ballotContains, ballot } = useBallot(); - const results = useResults(roundId, pollData); + const { getRoundByRoundId } = useRound(); + + const round = useMemo(() => getRoundByRoundId(roundId), [roundId, getRoundByRoundId]); + const results = useResults(roundId, round?.tallyFile); + const pollId = useMemo(() => round?.pollId, [round]); const handleAction = useCallback( (projectId: string) => (e: Event) => { diff --git a/packages/interface/src/features/rounds/components/RoundItem.tsx b/packages/interface/src/features/rounds/components/RoundItem.tsx index 2033dc11..c5d47ae4 100644 --- a/packages/interface/src/features/rounds/components/RoundItem.tsx +++ b/packages/interface/src/features/rounds/components/RoundItem.tsx @@ -8,7 +8,7 @@ import { useRoundState } from "~/utils/state"; import { formatPeriod } from "~/utils/time"; import { ERoundState } from "~/utils/types"; -import type { Round } from "~/features/rounds/types"; +import type { IRoundData } from "~/utils/types"; interface ITimeBarProps { start: Date; @@ -20,7 +20,7 @@ interface IRoundTagProps { } interface IRoundItemProps { - round: Round; + round: IRoundData; } const TimeBar = ({ start, end }: ITimeBarProps): JSX.Element => { diff --git a/packages/interface/src/features/rounds/components/RoundsList.tsx b/packages/interface/src/features/rounds/components/RoundsList.tsx index 4d4b35a8..5fffbffa 100644 --- a/packages/interface/src/features/rounds/components/RoundsList.tsx +++ b/packages/interface/src/features/rounds/components/RoundsList.tsx @@ -8,9 +8,7 @@ export const RoundsList = (): JSX.Element => { return (
- {rounds.map((round) => ( - - ))} + {rounds?.map((round) => )}
); }; diff --git a/packages/interface/src/features/rounds/types/index.ts b/packages/interface/src/features/rounds/types/index.ts deleted file mode 100644 index 25a13431..00000000 --- a/packages/interface/src/features/rounds/types/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; - -export const RoundSchema = z.object({ - roundId: z.string(), - description: z.string(), - startsAt: z.number(), - registrationEndsAt: z.number(), - votingEndsAt: z.number(), - tallyURL: z.string().optional(), -}); - -export type Round = z.infer; diff --git a/packages/interface/src/hooks/useResults.ts b/packages/interface/src/hooks/useResults.ts index a68027fe..0b6bab19 100644 --- a/packages/interface/src/hooks/useResults.ts +++ b/packages/interface/src/hooks/useResults.ts @@ -4,28 +4,24 @@ import { useRoundState } from "~/utils/state"; import { ERoundState } from "~/utils/types"; import type { UseTRPCInfiniteQueryResult, UseTRPCQueryResult } from "@trpc/react-query/shared"; -import type { IGetPollData } from "maci-cli/sdk"; import type { Attestation } from "~/utils/types"; export function useResults( roundId: string, - pollData?: IGetPollData, + tallyFile?: string, ): UseTRPCQueryResult<{ averageVotes: number; projects: Record }, unknown> { const roundState = useRoundState(roundId); - return api.results.votes.useQuery( - { roundId, pollId: pollData?.id.toString() }, - { enabled: roundState === ERoundState.RESULTS }, - ); + return api.results.votes.useQuery({ roundId, tallyFile }, { enabled: roundState === ERoundState.RESULTS }); } const seed = 0; export function useProjectsResults( roundId: string, - pollData?: IGetPollData, + tallyFile?: string, ): UseTRPCInfiniteQueryResult { return api.results.projects.useInfiniteQuery( - { roundId, limit: config.pageSize, seed, pollId: pollData?.id.toString() }, + { roundId, limit: config.pageSize, seed, tallyFile }, { getNextPageParam: (_, pages) => pages.length, }, @@ -39,12 +35,9 @@ export function useProjectCount(roundId: string): UseTRPCQueryResult<{ count: nu export function useProjectResults( id: string, roundId: string, - pollData?: IGetPollData, + tallyFile?: string, ): UseTRPCQueryResult<{ amount: number }, unknown> { const appState = useRoundState(roundId); - return api.results.project.useQuery( - { id, roundId, pollId: pollData?.id.toString() }, - { enabled: appState === ERoundState.RESULTS }, - ); + return api.results.project.useQuery({ id, roundId, tallyFile }, { enabled: appState === ERoundState.RESULTS }); } diff --git a/packages/interface/src/layouts/DefaultLayout.tsx b/packages/interface/src/layouts/DefaultLayout.tsx index 871d39f0..bc67639e 100644 --- a/packages/interface/src/layouts/DefaultLayout.tsx +++ b/packages/interface/src/layouts/DefaultLayout.tsx @@ -58,14 +58,10 @@ export const Layout = ({ children = null, ...props }: ILayoutProps): JSX.Element } if (config.admin === address! && props.roundId) { - links.push( - ...[ - { - href: `/rounds/${props.roundId}/applications`, - children: "Applications", - }, - ], - ); + links.push({ + href: `/rounds/${props.roundId}/applications`, + children: "Applications", + }); } if (config.admin === address!) { diff --git a/packages/interface/src/pages/index.tsx b/packages/interface/src/pages/index.tsx index b0ec4ca8..0b78e9e1 100644 --- a/packages/interface/src/pages/index.tsx +++ b/packages/interface/src/pages/index.tsx @@ -42,11 +42,11 @@ const HomePage = (): JSX.Element => { )} - {isConnected && !isAdmin && rounds.length === 0 && ( + {isConnected && !isAdmin && rounds && rounds.length === 0 && (

There are no rounds deployed.

)} - {rounds.length > 0 && } + {rounds && rounds.length > 0 && } diff --git a/packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx b/packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx index 82e48bd2..7a788f10 100644 --- a/packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/ballot/index.tsx @@ -117,7 +117,7 @@ const BallotAllocationForm = ({ roundId }: IBallotAllocationFormProps): JSX.Elem
{ballot.votes.length ? ( - + ) : ( )} diff --git a/packages/interface/src/pages/rounds/[roundId]/stats/index.tsx b/packages/interface/src/pages/rounds/[roundId]/stats/index.tsx index bb6fca98..dc68207e 100644 --- a/packages/interface/src/pages/rounds/[roundId]/stats/index.tsx +++ b/packages/interface/src/pages/rounds/[roundId]/stats/index.tsx @@ -33,10 +33,12 @@ interface IStatsProps { } const Stats = ({ roundId }: IStatsProps) => { - const { isLoading, pollData } = useMaci(); - const results = useResults(roundId, pollData); + const { isLoading } = useMaci(); + const { getRoundByRoundId } = useRound(); + const round = useMemo(() => getRoundByRoundId(roundId), [roundId, getRoundByRoundId]); + const results = useResults(roundId, round?.tallyFile); const count = useProjectCount(roundId); - const { data: projectsResults } = useProjectsResults(roundId, pollData); + const { data: projectsResults } = useProjectsResults(roundId); const { isConnected } = useAccount(); const { averageVotes, projects = {} } = results.data ?? {}; @@ -56,7 +58,7 @@ const Stats = ({ roundId }: IStatsProps) => { return
Loading...
; } - if (!pollData && !isConnected) { + if (!isConnected) { return ( Connect your wallet to see results @@ -68,10 +70,6 @@ const Stats = ({ roundId }: IStatsProps) => { ); } - if (!pollData) { - return
Something went wrong. Try later.
; - } - return (
Top Projects @@ -85,7 +83,7 @@ const Stats = ({ roundId }: IStatsProps) => { {Object.keys(projects).length} - {pollData.numSignups ? Number(pollData.numSignups) - 1 : 0} + {round?.numSignups ? Number(round.numSignups) - 1 : 0} {formatNumber(averageVotes)}
@@ -99,8 +97,8 @@ interface IStatsPageProps { const StatsPage = ({ roundId }: IStatsPageProps): JSX.Element => { const roundState = useRoundState(roundId); - const { getRound } = useRound(); - const round = getRound(roundId); + const { getRoundByRoundId } = useRound(); + const round = useMemo(() => getRoundByRoundId(roundId), [roundId, getRoundByRoundId]); const duration = round?.votingEndsAt && differenceInDays(round.votingEndsAt, new Date()); return ( diff --git a/packages/interface/src/server/api/routers/maci.ts b/packages/interface/src/server/api/routers/maci.ts index 2e278fc6..eaa3c5af 100644 --- a/packages/interface/src/server/api/routers/maci.ts +++ b/packages/interface/src/server/api/routers/maci.ts @@ -1,13 +1,55 @@ import { PubKey } from "maci-domainobjs"; -import { z } from "zod"; +import { z, ZodType } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; -import { fetchPoll } from "~/utils/fetchPoll"; +import { fetchMetadata } from "~/utils/fetchMetadata"; +import { fetchPolls } from "~/utils/fetchPoll"; import { fetchUser } from "~/utils/fetchUser"; +import type { IPollData, IRoundMetadata, IRoundData } from "~/utils/types"; + +const PollSchema = z.object({ + id: z.union([z.string(), z.number(), z.bigint()]), + mode: z.union([z.string(), z.number(), z.bigint()]), + address: z.string(), + isMerged: z.boolean(), + duration: z.union([z.string(), z.number(), z.bigint()]), + deployTime: z.union([z.string(), z.number(), z.bigint()]), + numSignups: z.union([z.string(), z.number(), z.bigint()]), + registryAddress: z.string(), + metadataUrl: z.string(), +}) satisfies ZodType; + export const maciRouter = createTRPCRouter({ user: publicProcedure .input(z.object({ publicKey: z.string() })) .query(async ({ input }) => fetchUser(PubKey.deserialize(input.publicKey).rawPubKey)), - poll: publicProcedure.query(async () => fetchPoll()), + poll: publicProcedure.query(async () => fetchPolls()), + round: publicProcedure.input(z.object({ polls: z.array(PollSchema) })).query(async ({ input }) => + Promise.all( + input.polls.map((poll) => + fetchMetadata(poll.metadataUrl).then((metadata) => { + const data = metadata as unknown as IRoundMetadata; + + return { + isMerged: poll.isMerged, + pollId: poll.id, + duration: poll.duration, + deployTime: poll.deployTime, + numSignups: poll.numSignups, + pollAddress: poll.address, + mode: poll.mode, + registryAddress: poll.registryAddress, + roundId: data.roundId, + description: data.description, + startsAt: data.startsAt, + registrationEndsAt: data.registrationEndsAt, + votingStartsAt: data.votingStartsAt, + votingEndsAt: data.votingEndsAt, + tallyFile: data.tallyFile, + } as IRoundData; + }), + ), + ), + ), }); diff --git a/packages/interface/src/server/api/routers/results.ts b/packages/interface/src/server/api/routers/results.ts index d231b2ca..3d418f8c 100644 --- a/packages/interface/src/server/api/routers/results.ts +++ b/packages/interface/src/server/api/routers/results.ts @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server"; import { type TallyData } from "maci-cli/sdk"; import { z } from "zod"; -import { config, eas } from "~/config"; +import { eas } from "~/config"; import { FilterSchema } from "~/features/filter/types"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { fetchAttestations } from "~/utils/fetchAttestations"; @@ -11,13 +11,13 @@ import { getAllApprovedProjects } from "./projects"; export const resultsRouter = createTRPCRouter({ votes: publicProcedure - .input(z.object({ roundId: z.string(), pollId: z.string().nullish() })) - .query(async ({ input }) => calculateMaciResults(input.roundId, input.pollId)), + .input(z.object({ roundId: z.string(), tallyFile: z.string().optional() })) + .query(async ({ input }) => calculateMaciResults(input.roundId, input.tallyFile)), project: publicProcedure - .input(z.object({ id: z.string(), roundId: z.string(), pollId: z.string().nullish() })) + .input(z.object({ id: z.string(), roundId: z.string(), tallyFile: z.string().optional() })) .query(async ({ input }) => { - const { projects } = await calculateMaciResults(input.roundId, input.pollId); + const { projects } = await calculateMaciResults(input.roundId, input.tallyFile); return { amount: projects[input.id]?.votes ?? 0, @@ -25,9 +25,9 @@ export const resultsRouter = createTRPCRouter({ }), projects: publicProcedure - .input(FilterSchema.extend({ roundId: z.string(), pollId: z.string().nullish() })) + .input(FilterSchema.extend({ roundId: z.string(), tallyFile: z.string().optional() })) .query(async ({ input }) => { - const { projects } = await calculateMaciResults(input.roundId, input.pollId); + const { projects } = await calculateMaciResults(input.roundId, input.tallyFile); const sortedIDs = Object.entries(projects) .sort((a, b) => b[1].votes - a[1].votes) @@ -48,17 +48,17 @@ export const resultsRouter = createTRPCRouter({ export async function calculateMaciResults( roundId: string, - pollId?: string | null, + tallyFile?: string, ): Promise<{ averageVotes: number; projects: Record; }> { - if (!pollId) { - throw new Error("No pollId provided."); + if (!tallyFile) { + throw new Error("No tallyFile URL provided."); } const [tallyData, projects] = await Promise.all([ - fetch(`${config.tallyUrl}/tally-${pollId}.json`) + fetch(tallyFile) .then((res) => res.json() as Promise) .catch(() => undefined), getAllApprovedProjects({ roundId }), diff --git a/packages/interface/src/utils/fetchPoll.ts b/packages/interface/src/utils/fetchPoll.ts index 9294d6c1..d53dca64 100644 --- a/packages/interface/src/utils/fetchPoll.ts +++ b/packages/interface/src/utils/fetchPoll.ts @@ -1,12 +1,20 @@ -import { IGetPollData } from "maci-cli/sdk"; +import { BigNumberish } from "ethers"; import { config } from "~/config"; +import type { IPollData } from "./types"; +import type { Hex } from "viem"; + import { createCachedFetch } from "./fetch"; const cachedFetch = createCachedFetch({ ttl: 1000 * 60 * 10 }); -interface Poll { +interface IRegistry { + id: Hex; + metadataUrl: string; +} + +interface IPoll { pollId: string; createdAt: string; duration: string; @@ -15,17 +23,19 @@ interface Poll { numSignups: string; id: string; mode: string; + registry: IRegistry; + initTime: BigNumberish; } export interface GraphQLResponse { data?: { - polls: Poll[]; + polls: IPoll[]; }; } const PollQuery = ` query Poll { - polls(orderBy: createdAt, orderDirection: desc, first: 1) { + polls(orderBy: createdAt, orderDirection: desc) { pollId duration createdAt @@ -34,28 +44,39 @@ const PollQuery = ` numSignups id mode + initTime + + registry { + id + metadataUrl + } } } `; -export async function fetchPoll(): Promise { - const poll = ( - await cachedFetch<{ polls: Poll[] }>(config.maciSubgraphUrl, { - method: "POST", - body: JSON.stringify({ - query: PollQuery, - }), - }).then((response: GraphQLResponse) => response.data?.polls) - )?.at(0); - - // cast this to a IGetPollData object so that we can deal with one object only in MACIContext - return { - isMerged: !!poll?.messageRoot, - id: poll?.pollId ?? 0, - duration: poll?.duration ?? 0, - deployTime: poll?.createdAt ?? 0, - numSignups: poll?.numSignups ?? 0, - address: poll?.id ?? "", - mode: poll?.mode ?? "", - }; +function mappedPollData(polls: IPoll[]): IPollData[] { + return polls.map((poll) => ({ + isMerged: !!poll.messageRoot, + id: poll.pollId, + duration: poll.duration, + deployTime: poll.createdAt, + numSignups: poll.numSignups, + address: poll.id, + mode: poll.mode, + registryAddress: poll.registry.id, + metadataUrl: poll.registry.metadataUrl, + initTime: poll.initTime, + })); +} + +export async function fetchPolls(): Promise { + const polls = await cachedFetch<{ polls: IPoll[] }>(config.maciSubgraphUrl, { + method: "POST", + body: JSON.stringify({ + query: PollQuery, + }), + }).then((response: GraphQLResponse) => response.data?.polls); + + // cast this to a IPollData object array so that we can deal with one object only in MACIContext + return polls ? mappedPollData(polls) : undefined; } diff --git a/packages/interface/src/utils/state.ts b/packages/interface/src/utils/state.ts index e76d72fb..5e8a79a7 100644 --- a/packages/interface/src/utils/state.ts +++ b/packages/interface/src/utils/state.ts @@ -6,8 +6,8 @@ import { ERoundState } from "./types"; export const useRoundState = (roundId: string): ERoundState => { const now = new Date(); - const { getRound } = useRound(); - const round = getRound(roundId); + const { getRoundByRoundId } = useRound(); + const round = getRoundByRoundId(roundId); if (!round) { return ERoundState.DEFAULT; @@ -21,13 +21,14 @@ export const useRoundState = (roundId: string): ERoundState => { return ERoundState.VOTING; } - if (round.votingEndsAt && isAfter(now, round.votingEndsAt) && !round.tallyURL) { + if (round.votingEndsAt && isAfter(now, round.votingEndsAt)) { return ERoundState.TALLYING; } - if (round.tallyURL) { - return ERoundState.RESULTS; - } + /// TODO: how to collect tally.json url + // if (round.votingEndsAt && isAfter(now, round.votingEndsAt)) { + // return ERoundState.RESULTS; + // } return ERoundState.DEFAULT; }; diff --git a/packages/interface/src/utils/types.ts b/packages/interface/src/utils/types.ts index c7f5a494..9189f2eb 100644 --- a/packages/interface/src/utils/types.ts +++ b/packages/interface/src/utils/types.ts @@ -1,4 +1,5 @@ -import { type Address } from "viem"; +import type { IGetPollData } from "maci-cli/sdk"; +import type { Address } from "viem"; export enum ERoundState { LOADING = "LOADING", @@ -89,3 +90,36 @@ export const AttestationsQuery = ` } } `; + +export interface IPollData extends IGetPollData { + registryAddress: string; + metadataUrl: string; +} + +export interface IRoundMetadata { + roundId: string; + description: string; + startsAt: string; + registrationEndsAt: string; + votingStartsAt: string; + votingEndsAt: string; + tallyFile: string; +} + +export interface IRoundData { + isMerged: boolean; + pollId: string; + duration: string; + deployTime: string; + numSignups: string; + pollAddress: string; + mode: string; + registryAddress: string; + roundId: string; + description: string; + startsAt: string; + registrationEndsAt: string; + votingStartsAt: string; + votingEndsAt: string; + tallyFile: string; +}