diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 929d7a29..f35e28bd 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,12 +2,15 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { type ComponentPropsWithRef, useState } from "react"; import clsx from "clsx"; +import { Menu, X } from "lucide-react"; +import dynamic from "next/dynamic"; import { ConnectButton } from "./ConnectButton"; import { IconButton } from "./ui/Button"; import { Logo } from "./ui/Logo"; -import { Menu, X } from "lucide-react"; -import dynamic from "next/dynamic"; +import { useBallot } from "~/contexts/Ballot"; +import { getAppState } from "~/utils/state"; +import { EAppState } from "~/utils/types"; const NavLink = ({ isActive, @@ -26,6 +29,8 @@ type NavLink = { href: string; children: string }; export const Header = ({ navLinks }: { navLinks: NavLink[] }) => { const { asPath } = useRouter(); const [isOpen, setOpen] = useState(false); + const { ballot } = useBallot(); + const appState = getAppState(); return (
@@ -42,15 +47,26 @@ export const Header = ({ navLinks }: { navLinks: NavLink[] }) => {
- {navLinks?.map((link) => ( - - {link.children} - - ))} + {navLinks?.map((link) => { + const pageName = `/${link.href.split("/")[1]}`; + return ( + + {link.children} + {appState === EAppState.VOTING && + pageName === "/ballot" && + ballot && + ballot.votes.length > 0 && ( +
+ {ballot.votes.length} +
+ )} +
+ ); + })}
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 2f938166..f3c99a3b 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -7,7 +7,7 @@ const button = tv({ variants: { variant: { primary: - "bg-primary-600 hover:bg-primary-700 dark:bg-white dark:hover:bg-primary-500 dark:text-gray-900 text-white dark:disabled:bg-gray-500", + "bg-black text-white w-full uppercase rounded-lg hover:bg-gray-400", ghost: "hover:bg-gray-100 dark:hover:bg-gray-800", default: "bg-gray-100 dark:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700", diff --git a/src/components/ui/Dialog.tsx b/src/components/ui/Dialog.tsx index 48150976..c422b658 100644 --- a/src/components/ui/Dialog.tsx +++ b/src/components/ui/Dialog.tsx @@ -1,18 +1,20 @@ import * as RadixDialog from "@radix-ui/react-dialog"; import type { ReactNode, PropsWithChildren, ComponentProps } from "react"; -import { IconButton } from "./Button"; -import { createComponent } from "."; import { tv } from "tailwind-variants"; import { X } from "lucide-react"; import clsx from "clsx"; +import { IconButton } from "./Button"; +import { createComponent } from "."; import { theme } from "~/config"; +import { Spinner } from "./Spinner"; export const Dialog = ({ title, description, size, isOpen, + isLoading, button, buttonName, buttonAction, @@ -23,6 +25,7 @@ export const Dialog = ({ description?: string | ReactNode; size?: "sm" | "md"; isOpen?: boolean; + isLoading?: boolean; button?: "primary" | "secondary"; buttonName?: string; buttonAction?: () => void; @@ -42,7 +45,8 @@ export const Dialog = ({ {description} {children} - {button && buttonName && buttonAction && ( + {isLoading && } + {!isLoading && button && buttonName && buttonAction && ( -
-
-
- - +

+ Your votes have been successfully submitted 🥳 +

+

+ Thank you for participating in {config.eventName} {config.roundId}{" "} + round. +

+
+ Summary of your voting +

+ Round you voted in: {config.roundId}
+ Number of projects you voted for: {allocations.length} of{" "} + {projectCount?.count} +

+
+ {allocations.map((project) => { + return ( +
+ +
+ ); + })} +
+
+

Total votes allocated:

+

{sum}

+
+
+ + {appState === EAppState.VOTING && ( -
-
- Here's how you voted! -
-
- -

Your vote will always be private

-
-
-
-

Project name

-

{config.tokenName} allocated by you

+
+ + Wanna change your mind? + +

+ Your can edit your ballot and resubmit it anytime during the + voting period. +

- -
- {votes && } -
- -
-
- Help us improve next round of RetroPGF -
-

- Your anonymized feedback will be influential to help us iterate on - Optimism's RetroPGF process. -

-
-
+ )} + +
+ + Help us improve our next round of {config.eventName} + +

+ Your anonymized feedback will be influential to help us iterate on + {config.eventName} process. +

+
+
+ +
+
+ +
+ Want to run a round? +

+ Our code is open source so you can fork it and run a round anytime. + If you need any assistance or want to share with us your + awesomeness, find us at #🗳️-maci channel in PSE Discord. +

+
+
+ +
+
); }; diff --git a/src/features/ballot/components/ProjectAvatarWithName.tsx b/src/features/ballot/components/ProjectAvatarWithName.tsx new file mode 100644 index 00000000..2837b530 --- /dev/null +++ b/src/features/ballot/components/ProjectAvatarWithName.tsx @@ -0,0 +1,43 @@ +import Link from "next/link"; + +import { ProjectAvatar } from "~/features/projects/components/ProjectAvatar"; +import { + useProjectById, + useProjectMetadata, +} from "~/features/projects/hooks/useProjects"; + +interface ProjectAvatarWithNameProps { + id?: string; + isLink?: boolean; + showDescription?: boolean; + allocation?: number; +} + +export const ProjectAvatarWithName = ({ + id, + isLink, + showDescription, + allocation, +}: ProjectAvatarWithNameProps) => { + const { data: project } = useProjectById(id!); + const metadata = useProjectMetadata(project?.metadataPtr); + + const Component = isLink ? Link : "div"; + + return ( + + +
+
{project?.name}
+
+

{showDescription && (metadata.data?.bio ?? null)}

+

{allocation && `Votes you have allocated: ${allocation}`}

+
+
+
+ ); +}; diff --git a/src/features/ballot/components/SubmitBallotButton.tsx b/src/features/ballot/components/SubmitBallotButton.tsx new file mode 100644 index 00000000..d1e37f80 --- /dev/null +++ b/src/features/ballot/components/SubmitBallotButton.tsx @@ -0,0 +1,56 @@ +import { useState, useCallback } from "react"; +import { useRouter } from "next/router"; +import { toast } from "sonner"; + +import { useProjectIdMapping } from "~/features/projects/hooks/useProjects"; +import { useMaci } from "~/contexts/Maci"; +import { useBallot } from "~/contexts/Ballot"; +import { Button } from "~/components/ui/Button"; +import { Dialog } from "~/components/ui/Dialog"; + +export const SubmitBallotButton = () => { + const router = useRouter(); + const [isOpen, setOpen] = useState(false); + const { onVote, isLoading } = useMaci(); + const { ballot, publishBallot } = useBallot(); + const projectIndices = useProjectIdMapping(ballot); + + const handleSubmitBallot = useCallback(async () => { + const votes = + ballot?.votes.map(({ amount, projectId }) => ({ + voteOptionIndex: BigInt(projectIndices[projectId]), + newVoteWeight: BigInt(amount), + })) ?? []; + + await onVote( + votes, + () => toast.error("Voting error"), + async () => { + publishBallot(); + await router.push("/ballot/confirmation"); + }, + ); + }, [ballot, router, onVote, publishBallot]); + + const handleOpenDialog = useCallback(() => setOpen(true), [setOpen]); + + return ( + <> + + + + + ); +}; diff --git a/src/features/projects/components/ProjectItem.tsx b/src/features/projects/components/ProjectItem.tsx index 7bc42f73..6ddad56e 100644 --- a/src/features/projects/components/ProjectItem.tsx +++ b/src/features/projects/components/ProjectItem.tsx @@ -27,7 +27,7 @@ export function ProjectItem({ const metadata = useProjectMetadata(attestation?.metadataPtr); const appState = getAppState(); const defaultButtonStyle = - "uppercase text-xs rounded-md border border-black p-1.5 cursor-pointer"; + "uppercase text-xs rounded-md border border-black p-1.5"; return (
{ + const manageDisplay = useCallback(async () => { if (requireAuth && !address && !isConnecting) { - void router.push("/"); + await router.push("/"); } }, [requireAuth, address, isConnecting, router]); + useEffect(() => { + manageDisplay(); + }, [manageDisplay]); + const wrappedSidebar = {sidebarComponent}; title = title ? `${title} - ${metadata.title}` : metadata.title; diff --git a/src/layouts/DefaultLayout.tsx b/src/layouts/DefaultLayout.tsx index da5f6aa2..99f4c8ab 100644 --- a/src/layouts/DefaultLayout.tsx +++ b/src/layouts/DefaultLayout.tsx @@ -1,4 +1,4 @@ -import type { ReactNode, PropsWithChildren } from "react"; +import { type ReactNode, type PropsWithChildren, useMemo } from "react"; import { useAccount } from "wagmi"; import Header from "~/components/Header"; @@ -9,6 +9,9 @@ import { getAppState } from "~/utils/state"; import { EAppState } from "~/utils/types"; import { config } from "~/config"; import { useMaci } from "~/contexts/Maci"; +import { useBallot } from "~/contexts/Ballot"; +import { Notification } from "~/components/ui/Notification"; +import { SubmitBallotButton } from "~/features/ballot/components/SubmitBallotButton"; type Props = PropsWithChildren< { @@ -19,39 +22,52 @@ type Props = PropsWithChildren< export const Layout = ({ children, ...props }: Props) => { const { address } = useAccount(); const appState = getAppState(); + const { ballot } = useBallot(); - const navLinks = [ - { - href: "/projects", - children: "Projects", - }, - { - href: "/ballot", - children: "My Ballot", - }, - ]; + const navLinks = useMemo(() => { + const navLinks = [ + { + href: "/projects", + children: "Projects", + }, + ]; - if (appState === EAppState.RESULTS) { - navLinks.push({ - href: "/stats", - children: "Stats", - }); - } + if (ballot?.published) { + navLinks.push({ + href: "/ballot/confirmation", + children: "My Ballot", + }); + } else { + navLinks.push({ + href: "/ballot", + children: "My Ballot", + }); + } - if (config.admin === address!) { - navLinks.push( - ...[ - { - href: "/applications", - children: "Applications", - }, - { - href: "/voters", - children: "Voters", - }, - ], - ); - } + if (appState === EAppState.RESULTS) { + navLinks.push({ + href: "/stats", + children: "Stats", + }); + } + + if (config.admin === address!) { + navLinks.push( + ...[ + { + href: "/applications", + children: "Applications", + }, + { + href: "/voters", + children: "Voters", + }, + ], + ); + } + + return navLinks; + }, [ballot, appState, address]); return ( }> @@ -60,17 +76,28 @@ export const Layout = ({ children, ...props }: Props) => { ); }; -export function LayoutWithBallot(props: Props) { +export function LayoutWithSidebar(props: Props) { const { isRegistered } = useMaci(); const { address } = useAccount(); + const { ballot } = useBallot(); return ( - - {address && isRegistered && } + {props.showInfo && } + {props.showBallot && address && isRegistered && } + {props.showSubmitButton && ballot && ballot.votes.length > 0 && ( +
+ + +
+ )} +
} {...props} diff --git a/src/pages/ballot/confirmation.tsx b/src/pages/ballot/confirmation.tsx index d193d712..2888e97c 100644 --- a/src/pages/ballot/confirmation.tsx +++ b/src/pages/ballot/confirmation.tsx @@ -1,17 +1,34 @@ +import { useRouter } from "next/router"; +import { useEffect, useState, useCallback } from "react"; + import { useBallot } from "~/contexts/Ballot"; import { BallotConfirmation } from "~/features/ballot/components/BallotConfirmation"; import { Layout } from "~/layouts/DefaultLayout"; +import { Spinner } from "~/components/ui/Spinner"; export default function BallotConfirmationPage() { - const { ballot } = useBallot(); + const [isLoading, setIsLoading] = useState(true); + + const { ballot, isLoading: isBallotLoading } = useBallot(); + const router = useRouter(); + + const manageDisplay = useCallback(async () => { + if (isBallotLoading) return; + + if (ballot.published) { + setIsLoading(false); + } else { + await router.push("/ballot"); + } + }, [router, ballot]); - if (!ballot) return null; + useEffect(() => { + manageDisplay(); + }, [manageDisplay]); return ( - +b.amount - +a.amount)} - /> + {isLoading ? : } ); } diff --git a/src/pages/ballot/index.tsx b/src/pages/ballot/index.tsx index a3f1cc7a..3511543c 100644 --- a/src/pages/ballot/index.tsx +++ b/src/pages/ballot/index.tsx @@ -6,10 +6,9 @@ import { useAccount } from "wagmi"; import { Button } from "~/components/ui/Button"; import { Dialog } from "~/components/ui/Dialog"; import { Form } from "~/components/ui/Form"; -import { config } from "~/config"; import { AllocationFormWrapper } from "~/features/ballot/components/AllocationList"; -import { BallotSchema, type Vote } from "~/features/ballot/types"; -import { LayoutWithBallot } from "~/layouts/DefaultLayout"; +import { BallotSchema } from "~/features/ballot/types"; +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import { useBallot } from "~/contexts/Ballot"; import { formatNumber } from "~/utils/formatNumber"; import { getAppState } from "~/utils/state"; @@ -36,7 +35,7 @@ export default function BallotPage() { } return ( - +
- -
- + ); } function BallotAllocationForm() { const appState = getAppState(); - const { ballot } = useBallot(); + const { ballot, sumBallot } = useBallot(); + + const sum = useMemo( + () => formatNumber(sumBallot(ballot?.votes)), + [ballot, sumBallot], + ); return ( -
-

Review your ballot

-

+

+

My Ballot

+

Once you have reviewed your vote allocation, you can submit your ballot.

-
+
{ballot?.votes?.length ? : null}
-
+
-
- {ballot?.votes?.length ? ( - - ) : ( - - )} -
+ {ballot?.votes?.length ? ( + + ) : ( + + )}
-
-
Total votes in ballot
-
- -
+
+

Total votes:

+

{sum}

@@ -104,30 +103,20 @@ function ClearBallot() { return ( <> - +
setOpen(true)}> + Remove all projects +
-

- This will empty your ballot and remove all the projects you have - added. -

-
- -
-
+ description="This will empty your ballot and remove all the projects you have added." + button="primary" + buttonName="Yes, Clear my ballot" + buttonAction={handleClearBallot} + /> ); } @@ -148,16 +137,3 @@ const EmptyBallot = () => (
); - -const TotalAllocation = () => { - const { sumBallot } = useBallot(); - const form = useFormContext<{ votes: Vote[] }>(); - const votes = form.watch("votes") ?? []; - const sum = sumBallot(votes); - - return ( -
- {formatNumber(sum)} {config.tokenName} -
- ); -}; diff --git a/src/pages/projects/[projectId]/Project.tsx b/src/pages/projects/[projectId]/Project.tsx index b49a4ed8..cda540b1 100644 --- a/src/pages/projects/[projectId]/Project.tsx +++ b/src/pages/projects/[projectId]/Project.tsx @@ -1,6 +1,6 @@ import { type GetServerSideProps } from "next"; -import { LayoutWithBallot } from "~/layouts/DefaultLayout"; +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import ProjectDetails from "~/features/projects/components/ProjectDetails"; import { useProjectById } from "~/features/projects/hooks/useProjects"; @@ -9,9 +9,15 @@ export default function ProjectDetailsPage({ projectId = "" }) { const { name } = project.data ?? {}; return ( - + - + ); } diff --git a/src/pages/projects/index.tsx b/src/pages/projects/index.tsx index 9b5b7bb4..88397c3f 100644 --- a/src/pages/projects/index.tsx +++ b/src/pages/projects/index.tsx @@ -1,10 +1,10 @@ -import { LayoutWithBallot } from "~/layouts/DefaultLayout"; +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import { Projects } from "~/features/projects/components/Projects"; export default function ProjectsPage() { return ( - + - + ); } diff --git a/src/providers/index.tsx b/src/providers/index.tsx index bf0bb901..44f722bb 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -36,23 +36,21 @@ const customTheme: Theme = { }, }; -export function Providers({ - children, -}: PropsWithChildren) { +export function Providers({ children }: PropsWithChildren) { const { config, queryClient } = useMemo(() => createWagmiConfig(), []); return ( - - - - - {children} - - - - - + + + + + {children} + + + + + ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index 66512bd5..e4a35015 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -34,6 +34,7 @@ h1 { font-size: 60px; + font-family: "Share Tech Mono"; } h2 {