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 4edf71c6..4c864733 100644 --- a/packages/interface/src/components/EligibilityDialog.tsx +++ b/packages/interface/src/components/EligibilityDialog.tsx @@ -4,12 +4,16 @@ import { toast } from "sonner"; import { useAccount, useDisconnect } from "wagmi"; import { useMaci } from "~/contexts/Maci"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; +import { useRoundState } from "~/utils/state"; +import { ERoundState } from "~/utils/types"; import { Dialog } from "./ui/Dialog"; -export const EligibilityDialog = (): JSX.Element | null => { +interface IEligibilityDialogProps { + roundId?: string; +} + +export const EligibilityDialog = ({ roundId = "" }: IEligibilityDialogProps): JSX.Element | null => { const { address } = useAccount(); const { disconnect } = useDisconnect(); @@ -17,7 +21,7 @@ export const EligibilityDialog = (): JSX.Element | null => { const { onSignup, isEligibleToVote, isRegistered, initialVoiceCredits, votingEndsAt } = useMaci(); const router = useRouter(); - const appState = useAppState(); + const roundState = useRoundState(roundId); const onError = useCallback(() => toast.error("Signup error"), []); @@ -38,15 +42,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) { + /// TODO: edit X to real date + if (roundState === ERoundState.VOTING && isRegistered) { return (

You have {initialVoiceCredits} voice credits to vote with.

@@ -85,21 +84,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 @@ -120,13 +119,13 @@ export const EligibilityDialog = (): JSX.Element | null => { ); } - 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 28fa3f6e..44e80d73 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, @@ -100,7 +94,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/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 29eab6bd..2637d0b8 100644 --- a/packages/interface/src/contexts/types.ts +++ b/packages/interface/src/contexts/types.ts @@ -2,6 +2,7 @@ import { type TallyData, type IGetPollData, type GatekeeperTrait } from "maci-cl import { type ReactNode } from "react"; import type { Ballot, Vote } from "~/features/ballot/types"; +import type { Round } from "~/features/rounds/types"; export interface IVoteArgs { voteOptionIndex: bigint; @@ -47,3 +48,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 11cb918a..5ff03102 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(), @@ -81,13 +66,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, @@ -96,7 +74,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..a17e20e7 100644 --- a/packages/interface/src/features/applications/components/ApplicationForm.tsx +++ b/packages/interface/src/features/applications/components/ApplicationForm.tsx @@ -18,7 +18,11 @@ 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,6 +64,7 @@ export const ApplicationForm = (): JSX.Element => { toast.error("Application create error", { description: err.reason ?? err.data?.message, }), + roundId, }); const { error: createError } = create; diff --git a/packages/interface/src/features/applications/components/ApplicationsToApprove.tsx b/packages/interface/src/features/applications/components/ApplicationsToApprove.tsx index 17d8bfab..7c546aa9 100644 --- a/packages/interface/src/features/applications/components/ApplicationsToApprove.tsx +++ b/packages/interface/src/features/applications/components/ApplicationsToApprove.tsx @@ -20,10 +20,14 @@ import { ApplicationHeader } from "./ApplicationHeader"; import { ApplicationItem } from "./ApplicationItem"; import { ApproveButton } from "./ApproveButton"; -export const ApplicationsToApprove = (): JSX.Element => { - 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); }; 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/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/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 02b8cf0a..05f6e390 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 {