From 912cb0f7fe11e11bc140edeffbc5420d8cfc663c Mon Sep 17 00:00:00 2001 From: yu-zhen Date: Tue, 25 Jun 2024 18:31:31 +0900 Subject: [PATCH] Design UI updates --- .env.example | 6 +- package.json | 2 +- pnpm-lock.yaml | 3 +- public/Logo.svg | 4 + public/arrow-go-to.svg | 3 + public/check-black.svg | 3 + public/check-white.svg | 3 + public/circle-check-blue.svg | 3 + public/dropdown.svg | 3 + public/fonts/DM_Sans.woff2 | Bin 0 -> 22984 bytes public/fonts/Share_Tech_Mono.woff2 | Bin 0 -> 7356 bytes public/round-logo.svg | 9 + src/components/AddedProjects.tsx | 25 ++ src/components/BallotOverview.tsx | 12 + src/components/ConnectButton.tsx | 159 ++++------- src/components/EligibilityDialog.tsx | 101 +++++-- src/components/Footer.tsx | 69 +++-- src/components/Header.tsx | 63 +++-- src/components/Info.tsx | 94 +++++++ src/components/InfoCard.tsx | 65 +++++ src/components/JoinButton.tsx | 59 ++++ src/components/RoundInfo.tsx | 17 ++ src/components/SortByDropdown.tsx | 66 +++-- src/components/SortFilter.tsx | 4 +- src/components/TimeSlot.tsx | 15 + src/components/Toaster.tsx | 4 +- src/components/VotingInfo.tsx | 32 +++ src/components/VotingUsage.tsx | 29 ++ src/components/ui/Avatar.tsx | 2 +- src/components/ui/Banner.tsx | 7 +- src/components/ui/Button.tsx | 25 +- src/components/ui/Chip.tsx | 12 +- src/components/ui/Dialog.tsx | 100 ++++--- src/components/ui/Form.tsx | 56 +--- src/components/ui/Input.tsx | 53 ++++ src/components/ui/Link.tsx | 2 +- src/components/ui/Logo.tsx | 14 + src/components/ui/Navigator.tsx | 19 ++ src/components/ui/Notification.tsx | 46 ++++ src/components/ui/Table.tsx | 17 +- src/components/ui/Tag.tsx | 6 +- src/config.ts | 7 +- src/contexts/Ballot.tsx | 5 +- src/contexts/Maci.tsx | 2 +- src/contexts/types.ts | 3 +- src/env.js | 4 + .../components/ApplicationForm.tsx | 2 +- .../ballot/components/AllocationInput.tsx | 4 +- .../ballot/components/AllocationList.tsx | 203 +++++--------- .../ballot/components/BallotConfirmation.tsx | 181 +++++++----- .../ballot/components/BallotOverview.tsx | 260 ------------------ .../components/ProjectAvatarWithName.tsx | 43 +++ .../ballot/components/SubmitBallotButton.tsx | 56 ++++ .../projects/components/AddToBallot.tsx | 89 +++--- .../projects/components/ProjectContacts.tsx | 45 +++ .../components/ProjectContributions.tsx | 2 +- .../components/ProjectDescriptionSection.tsx | 67 +++++ .../projects/components/ProjectDetails.tsx | 123 ++++----- .../projects/components/ProjectItem.tsx | 63 ++++- .../components/ProjectSelectButton.tsx | 39 --- src/features/projects/components/Projects.tsx | 130 ++++----- .../projects/components/VotingWidget.tsx | 101 +++++++ src/features/projects/types.ts | 46 ++++ src/layouts/BaseLayout.tsx | 38 ++- src/layouts/DefaultLayout.tsx | 119 +++++--- src/pages/ballot/confirmation.tsx | 27 +- src/pages/ballot/index.tsx | 125 ++++----- src/pages/index.tsx | 19 +- src/pages/projects/[projectId]/Project.tsx | 27 +- src/pages/projects/index.tsx | 14 +- src/pages/signup/index.tsx | 49 ++++ src/providers/index.tsx | 33 ++- src/styles/globals.css | 45 +++ src/utils/types.ts | 9 +- tailwind.config.ts | 39 ++- 75 files changed, 2026 insertions(+), 1205 deletions(-) create mode 100644 public/Logo.svg create mode 100644 public/arrow-go-to.svg create mode 100644 public/check-black.svg create mode 100644 public/check-white.svg create mode 100644 public/circle-check-blue.svg create mode 100644 public/dropdown.svg create mode 100644 public/fonts/DM_Sans.woff2 create mode 100644 public/fonts/Share_Tech_Mono.woff2 create mode 100644 public/round-logo.svg create mode 100644 src/components/AddedProjects.tsx create mode 100644 src/components/BallotOverview.tsx create mode 100644 src/components/Info.tsx create mode 100644 src/components/InfoCard.tsx create mode 100644 src/components/JoinButton.tsx create mode 100644 src/components/RoundInfo.tsx create mode 100644 src/components/TimeSlot.tsx create mode 100644 src/components/VotingInfo.tsx create mode 100644 src/components/VotingUsage.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Logo.tsx create mode 100644 src/components/ui/Navigator.tsx create mode 100644 src/components/ui/Notification.tsx delete mode 100644 src/features/ballot/components/BallotOverview.tsx create mode 100644 src/features/ballot/components/ProjectAvatarWithName.tsx create mode 100644 src/features/ballot/components/SubmitBallotButton.tsx create mode 100644 src/features/projects/components/ProjectContacts.tsx create mode 100644 src/features/projects/components/ProjectDescriptionSection.tsx delete mode 100644 src/features/projects/components/ProjectSelectButton.tsx create mode 100644 src/features/projects/components/VotingWidget.tsx create mode 100644 src/features/projects/types.ts create mode 100644 src/pages/signup/index.tsx diff --git a/.env.example b/.env.example index 0e86b964..e65612e1 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,9 @@ NEXT_PUBLIC_WALLETCONNECT_ID= # What the message will say when you sign in with the wallet NEXT_PUBLIC_SIGN_STATEMENT="Sign in to MACI-RPGF" +# Event title for the round, just for display +NEXT_PUBLIC_EVENT_NAME="ETH GLOBAL" + # 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 @@ -46,7 +49,6 @@ NEXT_PUBLIC_TOKEN_NAME="Votes" # 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_REVIEW_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 @@ -100,3 +102,5 @@ NEXT_PUBLIC_TALLY_URL=https://upblxu2duoxmkobt.public.blob.vercel-storage.com # Whether the poll is in qv or non qv mode NEXT_PUBLIC_POLL_MODE="non-qv" + +NEXT_PUBLIC_ROUND_LOGO="round-logo.png" diff --git a/package.json b/package.json index aa5fd868..d00af076 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@vercel/blob": "^0.19.0", "clsx": "^2.1.0", "cmdk": "^0.2.0", - "date-fns": "^3.3.1", + "date-fns": "^3.6.0", "ethers": "^6.11.2", "formidable": "^3.5.1", "graphql-request": "^6.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdd4cd8e..1c31521c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,7 +69,7 @@ dependencies: specifier: ^0.2.0 version: 0.2.1(@types/react@18.3.2)(react-dom@18.2.0)(react@18.2.0) date-fns: - specifier: ^3.3.1 + specifier: ^3.6.0 version: 3.6.0 ethers: specifier: ^6.11.2 @@ -10717,7 +10717,6 @@ packages: /glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} - deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 diff --git a/public/Logo.svg b/public/Logo.svg new file mode 100644 index 00000000..d3feab19 --- /dev/null +++ b/public/Logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/arrow-go-to.svg b/public/arrow-go-to.svg new file mode 100644 index 00000000..05fffe11 --- /dev/null +++ b/public/arrow-go-to.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/check-black.svg b/public/check-black.svg new file mode 100644 index 00000000..79c1d55c --- /dev/null +++ b/public/check-black.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/check-white.svg b/public/check-white.svg new file mode 100644 index 00000000..51145219 --- /dev/null +++ b/public/check-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/circle-check-blue.svg b/public/circle-check-blue.svg new file mode 100644 index 00000000..6392b913 --- /dev/null +++ b/public/circle-check-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/dropdown.svg b/public/dropdown.svg new file mode 100644 index 00000000..4562ea6d --- /dev/null +++ b/public/dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/fonts/DM_Sans.woff2 b/public/fonts/DM_Sans.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..469840ce80327457ff30ce11ebb61ac8c8c164e7 GIT binary patch literal 22984 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 Lah#d}w#fhh|4~0U literal 0 HcmV?d00001 diff --git a/public/fonts/Share_Tech_Mono.woff2 b/public/fonts/Share_Tech_Mono.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..50371e428b5ac9ef5a53b8689d188a901db334f4 GIT binary patch literal 7356 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 literal 0 HcmV?d00001 diff --git a/public/round-logo.svg b/public/round-logo.svg new file mode 100644 index 00000000..d8bb29c7 --- /dev/null +++ b/public/round-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/AddedProjects.tsx b/src/components/AddedProjects.tsx new file mode 100644 index 00000000..f993ff3f --- /dev/null +++ b/src/components/AddedProjects.tsx @@ -0,0 +1,25 @@ +import { useBallot } from "~/contexts/Ballot"; +import { useProjectCount } from "~/features/projects/hooks/useProjects"; + +export const AddedProjects = () => { + const { ballot } = useBallot(); + const allocations = ballot?.votes ?? []; + const { data: projectCount } = useProjectCount(); + + return ( +
+

Projects Added

+
+ + {allocations.length} + + + of + + + {projectCount?.count} + +
+
+ ); +}; diff --git a/src/components/BallotOverview.tsx b/src/components/BallotOverview.tsx new file mode 100644 index 00000000..3a62a958 --- /dev/null +++ b/src/components/BallotOverview.tsx @@ -0,0 +1,12 @@ +import { AddedProjects } from "./AddedProjects"; +import { VotingUsage } from "./VotingUsage"; + +export function BallotOverview() { + return ( +
+

My Ballot

+ + +
+ ); +} diff --git a/src/components/ConnectButton.tsx b/src/components/ConnectButton.tsx index e905397d..754c09cd 100644 --- a/src/components/ConnectButton.tsx +++ b/src/components/ConnectButton.tsx @@ -1,118 +1,34 @@ import { ConnectButton as RainbowConnectButton } from "@rainbow-me/rainbowkit"; -import Image from "next/image"; -import Link from "next/link"; -import { type ComponentPropsWithRef, useCallback } from "react"; -import { FaListCheck } from "react-icons/fa6"; import { createBreakpoint } from "react-use"; -import { toast } from "sonner"; -import { useEnsAvatar, useEnsName } from "wagmi"; - -import { config } from "~/config"; -import { useBallot } from "~/contexts/Ballot"; -import { useMaci } from "~/contexts/Maci"; -import { useLayoutOptions } from "~/layouts/BaseLayout"; - -import type { Address } from "viem"; +import Image from "next/image"; import { Button } from "./ui/Button"; import { Chip } from "./ui/Chip"; +import { config } from "~/config"; const useBreakpoint = createBreakpoint({ XL: 1280, L: 768, S: 350 }); -const UserInfo = ({ address, children, ...props }: { address: Address } & ComponentPropsWithRef) => { - const ens = useEnsName({ - address, - chainId: 1, - query: { enabled: Boolean(address) }, - }); - const name = ens.data ?? undefined; - const avatar = useEnsAvatar({ - name, - chainId: 1, - query: { enabled: Boolean(name) }, - }); - - return ( - -
- {avatar.data ? ( - {name!} - ) : ( -
- )} -
- - {children} - - ); -}; - -const SignupButton = ({ - loading, - ...props -}: ComponentPropsWithRef & { loading: boolean }): JSX.Element => ( - - {loading ? "Loading..." : "Sign up"} - -); - -const ConnectedDetails = ({ - openAccountModal, - account, - isMobile, -}: { - account: { address: string; displayName: string }; - openAccountModal: () => void; - isMobile: boolean; -}) => { - const { isLoading, isRegistered, isEligibleToVote, onSignup } = useMaci(); - const { ballot } = useBallot(); - const ballotSize = (ballot?.votes ?? []).length; - - const { showBallot } = useLayoutOptions(); - - const onError = useCallback(() => toast.error("Signup error"), []); - const handleSignup = useCallback(() => onSignup(onError), [onSignup, onError]); - - return ( -
-
- {!isEligibleToVote && You are not allowed to vote} - - {isEligibleToVote && !isRegistered && ( - - )} - - {isRegistered && showBallot && ballot?.published && Already submitted} - - {isRegistered && showBallot && !ballot?.published && ( - - {isMobile ? : `View Ballot`} - -
- {ballotSize} -
-
- )} - - - {isMobile ? null : account.displayName} - -
-
- ); -}; - -export const ConnectButton = (): JSX.Element => { +export const ConnectButton = () => { const breakpoint = useBreakpoint(); const isMobile = breakpoint === "S"; return ( - {({ account, chain, openAccountModal, openChainModal, openConnectModal, mounted, authenticationStatus }) => { + {({ + account, + chain, + openAccountModal, + openChainModal, + openConnectModal, + mounted, + authenticationStatus, + }) => { const ready = mounted && authenticationStatus !== "loading"; const connected = - ready && account && chain && (!authenticationStatus || authenticationStatus === "authenticated"); + ready && + account && + chain && + (!authenticationStatus || authenticationStatus === "authenticated"); return (
{ return ( ); } - if (chain.unsupported ?? ![Number(config.network.id)].includes(chain.id)) { - return Wrong network; + if ( + chain.unsupported ?? + ![Number(config.network.id)].includes(chain.id) + ) { + return ( + + Wrong network + + ); } - return ; + return ( + + ); })()}
); @@ -151,3 +79,24 @@ export const ConnectButton = (): JSX.Element => {
); }; + +const ConnectedDetails = ({ + openAccountModal, + account, + isMobile, +}: { + account: { address: string; displayName: string }; + openAccountModal: () => void; + isMobile: boolean; +}) => { + return ( +
+
+ + {isMobile ? null : account.displayName} + + +
+
+ ); +}; diff --git a/src/components/EligibilityDialog.tsx b/src/components/EligibilityDialog.tsx index 774427bf..a549aead 100644 --- a/src/components/EligibilityDialog.tsx +++ b/src/components/EligibilityDialog.tsx @@ -1,35 +1,90 @@ import { useAccount, useDisconnect } from "wagmi"; - -import { metadata } from "~/config"; -import { useApprovedVoter } from "~/features/voters/hooks/useApprovedVoter"; +import { toast } from "sonner"; +import { useState, useCallback, useEffect } from "react"; +import { useRouter } from "next/router"; import { Dialog } from "./ui/Dialog"; +import { useMaci } from "~/contexts/Maci"; export const EligibilityDialog = (): JSX.Element | null => { const { address } = useAccount(); const { disconnect } = useDisconnect(); - const { data, isLoading, error } = useApprovedVoter(address!); - if (isLoading || !address || error) { - return null; - } + const [openDialog, setOpenDialog] = useState(!!address); + const { onSignup, isEligibleToVote, isRegistered } = useMaci(); + const router = useRouter(); + + const onError = useCallback(() => toast.error("Signup error"), []); + + const handleSignup = useCallback(async () => { + await onSignup(onError); + setOpenDialog(false); + }, [onSignup, onError, setOpenDialog]); + + useEffect(() => { + setOpenDialog(!!address); + }, [address, setOpenDialog]); return ( - - You are not eligible to vote 😔 - - } - onOpenChange={() => { - disconnect(); - }} - > -
-

Only badgeholders are able to vote in {metadata.title}

-
-
+
+ {isRegistered && ( + setOpenDialog(false)} + title="You're all set to vote" + description={ +
+

You have X voice credits to vote with.

+

+ Get started by adding projects to your ballot, then adding the + amount of votes you want to allocate to each one. +

+

Please submit your ballot by X date!

+
+ } + button="secondary" + buttonName="See all projects" + buttonAction={() => router.push("/projects")} + /> + )} + {!isRegistered && isEligibleToVote && ( + setOpenDialog(false)} + title="Account verified!" + description={ +
+ } + button="secondary" + buttonName="Join voting round" + buttonAction={handleSignup} + /> + )} + {!isEligibleToVote && ( + setOpenDialog(false)} + title="Sorry, this account does not have the credentials to be verified." + description="To participate in this round, you must be in the voter's registry. Contact the round organizers to get access as a voter." + button="secondary" + buttonName="Disconnect" + buttonAction={() => disconnect()} + /> + )} +
); }; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index b46b8fb5..5778229b 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,29 +1,48 @@ -import { GithubIcon } from "lucide-react"; +import { FaXTwitter } from "react-icons/fa6"; +import { FaTelegramPlane, FaGithub, FaDiscord } from "react-icons/fa"; +import Image from "next/image"; -export const Footer = (): JSX.Element => ( - + ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bea1e7f1..b46b4178 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,35 +1,22 @@ -import clsx from "clsx"; -import { Menu, X } from "lucide-react"; -import dynamic from "next/dynamic"; -import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import { type ComponentPropsWithRef, useState } from "react"; - -import { config, metadata } from "~/config"; +import clsx from "clsx"; +import { Menu, X } from "lucide-react"; +import dynamic from "next/dynamic"; import { ConnectButton } from "./ConnectButton"; import { IconButton } from "./ui/Button"; - -const Logo = () => ( -
- {config.logoUrl ? ( - logo - ) : ( -
- {metadata.title} -
- )} -
-); +import { Logo } from "./ui/Logo"; +import { useBallot } from "~/contexts/Ballot"; +import { useAppState } from "~/utils/state"; +import { EAppState } from "~/utils/types"; const NavLink = ({ isActive, ...props }: { isActive: boolean } & ComponentPropsWithRef) => ( @@ -58,9 +45,11 @@ interface INavLink { const Header = ({ navLinks }: { navLinks: INavLink[] }) => { const { asPath } = useRouter(); const [isOpen, setOpen] = useState(false); + const { ballot } = useBallot(); + const appState = useAppState(); return ( -
+
{
- -
- {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/Info.tsx b/src/components/Info.tsx new file mode 100644 index 00000000..77c1050a --- /dev/null +++ b/src/components/Info.tsx @@ -0,0 +1,94 @@ +import { tv } from "tailwind-variants"; + +import { createComponent } from "~/components/ui"; +import { EInfoCardState } from "~/utils/types"; +import { useMaci } from "~/contexts/Maci"; +import { config } from "~/config"; +import { getAppState } from "~/utils/state"; +import { EAppState } from "~/utils/types"; + +import { RoundInfo } from "./RoundInfo"; +import { VotingInfo } from "./VotingInfo"; +import { InfoCard } from "./InfoCard"; + +const InfoContainer = createComponent( + "div", + tv({ + base: "flex items-center justify-center gap-2 rounded-lg bg-white p-5 shadow-lg", + variants: { + size: { + sm: "flex-col", + default: "flex-col max-lg:w-full lg:flex-row", + }, + }, + }), +); + +interface InfoProps { + size: string; + showVotingInfo?: boolean; +} + +export function Info({ size, showVotingInfo }: InfoProps) { + const { votingEndsAt } = useMaci(); + const appState = getAppState(); + + const steps = [ + { + label: "application", + start: config.startsAt, + end: config.registrationEndsAt, + }, + { + label: "voting", + start: config.registrationEndsAt, + end: votingEndsAt, + }, + { + label: "tallying", + start: votingEndsAt, + end: config.resultsAt, + }, + { + label: "results", + start: config.resultsAt, + end: config.resultsAt, + }, + ]; + + return ( +
+ + {showVotingInfo && ( +
+ + {appState === EAppState.VOTING && } +
+ )} + {steps.map((step, i) => ( + + ))} +
+
+ ); +} + +function defineState({ + start, + end, +}: { + start: Date; + end: Date; +}): EInfoCardState { + const now = new Date(); + + if (end < now) return EInfoCardState.PASSED; + else if (end > now && start < now) return EInfoCardState.ONGOING; + else return EInfoCardState.UPCOMING; +} diff --git a/src/components/InfoCard.tsx b/src/components/InfoCard.tsx new file mode 100644 index 00000000..af6914d6 --- /dev/null +++ b/src/components/InfoCard.tsx @@ -0,0 +1,65 @@ +import { tv } from "tailwind-variants"; +import Image from "next/image"; +import { format } from "date-fns"; + +import { createComponent } from "~/components/ui"; +import { EInfoCardState } from "~/utils/types"; + +const InfoCardContainer = createComponent( + "div", + tv({ + base: "rounded-md p-2 max-lg:w-full lg:w-64", + variants: { + state: { + [EInfoCardState.PASSED]: + "border border-blue-500 bg-blue-50 text-blue-500", + [EInfoCardState.ONGOING]: + "border border-blue-500 bg-blue-500 text-white", + [EInfoCardState.UPCOMING]: + "border border-gray-200 bg-transparent text-gray-200", + }, + }, + }), +); + +interface InfoCardProps { + state: EInfoCardState; + title: string; + start: Date; + end: Date; +} + +export const InfoCard = ({ state, title, start, end }: InfoCardProps) => { + return ( + +
+

+ {title} +

+ {state === EInfoCardState.PASSED ? ( + + ) : state == EInfoCardState.ONGOING ? ( +
+ ) : ( +
+ )} +
+

{formatDateString({ 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)}`; + } else if (start.getFullYear() === end.getFullYear()) { + return `${format(start, "d MMM")} - ${format(end, fullFormat)}`; + } else { + return `${format(start, fullFormat)} - ${format(end, fullFormat)}`; + } +} diff --git a/src/components/JoinButton.tsx b/src/components/JoinButton.tsx new file mode 100644 index 00000000..8aab4bfd --- /dev/null +++ b/src/components/JoinButton.tsx @@ -0,0 +1,59 @@ +import { toast } from "sonner"; +import { useCallback } from "react"; + +import { useMaci } from "~/contexts/Maci"; +import { Button } from "./ui/Button"; +import { getAppState } from "~/utils/state"; +import { EAppState } from "~/utils/types"; + +export const JoinButton = () => { + const { isLoading, isRegistered, isEligibleToVote, onSignup } = useMaci(); + const appState = getAppState(); + + const onError = useCallback(() => toast.error("Signup error"), []); + const handleSignup = useCallback( + () => onSignup(onError), + [onSignup, onError], + ); + + const applyApplication = () => {}; + + const viewResults = () => {}; + + return ( +
+ {appState === EAppState.VOTING && !isEligibleToVote && ( + + )} + + {appState === EAppState.VOTING && isEligibleToVote && !isRegistered && ( + + )} + + {appState === EAppState.APPLICATION && ( + + )} + + {appState === EAppState.TALLYING && ( + + )} + + {appState === EAppState.RESULTS && ( + + )} +
+ ); +}; diff --git a/src/components/RoundInfo.tsx b/src/components/RoundInfo.tsx new file mode 100644 index 00000000..d80f62fd --- /dev/null +++ b/src/components/RoundInfo.tsx @@ -0,0 +1,17 @@ +import Image from "next/image"; + +import { config } from "~/config"; + +export const RoundInfo = () => { + return ( +
+

Round

+
+ {config.roundLogo && ( + + )} +

{config.roundId}

+
+
+ ); +}; diff --git a/src/components/SortByDropdown.tsx b/src/components/SortByDropdown.tsx index ee5a7e5c..6e7f1a8e 100644 --- a/src/components/SortByDropdown.tsx +++ b/src/components/SortByDropdown.tsx @@ -16,7 +16,7 @@ interface IRadioItemProps { value?: string; } -const RadioItem = ({ value = "", label = "" }: IRadioItemProps): JSX.Element => ( +const RadioItem = ({ value = "", label = "" }: IRadioItemProps) => ( - {label} ); -export const SortByDropdown = ({ value, onChange, options = [] }: ISortByDropdownProps): JSX.Element => ( - - - - Sort by: {sortLabels[value]} - - - - - { + return ( + + - - Sort By - + + Sort by: {value && sortLabels[value]} + + - { - onChange(v); - }} + + - {options.map((option) => ( - - ))} - - - - -); + + Sort By + + onChange(v)} + > + {options.map((value) => ( + + ))} + + + + + ); +}; diff --git a/src/components/SortFilter.tsx b/src/components/SortFilter.tsx index 46cf4919..62549bc0 100644 --- a/src/components/SortFilter.tsx +++ b/src/components/SortFilter.tsx @@ -24,8 +24,8 @@ export const SortFilter = (): JSX.Element => { return (
diff --git a/src/components/TimeSlot.tsx b/src/components/TimeSlot.tsx new file mode 100644 index 00000000..f0444763 --- /dev/null +++ b/src/components/TimeSlot.tsx @@ -0,0 +1,15 @@ +interface TimeSlotProps { + num: number; + unit: string; +} + +export const TimeSlot = ({ num, unit }: TimeSlotProps) => { + return ( +
+

+ {num} +

+

{unit}

+
+ ); +}; diff --git a/src/components/Toaster.tsx b/src/components/Toaster.tsx index 918a87cc..fc9bddea 100644 --- a/src/components/Toaster.tsx +++ b/src/components/Toaster.tsx @@ -7,10 +7,12 @@ export const Toaster = (): JSX.Element => { { + const { isLoading, votingEndsAt } = useMaci(); + const [timeLeft, setTimeLeft] = useState<[number, number, number, number]>([ + 0, 0, 0, 0, + ]); + + useHarmonicIntervalFn( + () => setTimeLeft(calculateTimeLeft(votingEndsAt)), + 1000, + ); + + return ( +
+

Voting Ends In

+ {isLoading &&

Loading...

} + {!isLoading && ( +
+ + + + +
+ )} +
+ ); +}; diff --git a/src/components/VotingUsage.tsx b/src/components/VotingUsage.tsx new file mode 100644 index 00000000..478cafb9 --- /dev/null +++ b/src/components/VotingUsage.tsx @@ -0,0 +1,29 @@ +import { useMemo } from "react"; + +import { useBallot } from "~/contexts/Ballot"; +import { useMaci } from "~/contexts/Maci"; + +export const VotingUsage = () => { + const { initialVoiceCredits } = useMaci(); + const { ballot, sumBallot } = useBallot(); + + const sum = useMemo(() => sumBallot(ballot?.votes), [sumBallot, ballot]); + + return ( +
+

Voting Power

+
+

+ {initialVoiceCredits} +

+

Votes Left

+
+
+

+ {sum} +

+

Votes Used

+
+
+ ); +}; diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index b6a42f90..551e5b75 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -7,7 +7,7 @@ import { createComponent } from "."; export const Avatar = createComponent( BackgroundImage, tv({ - base: "bg-gray-200 dark:bg-gray-800", + base: "bg-gray-200 dark:bg-gray-800 border-2 border-white", variants: { size: { xs: "w-5 h-5 rounded-xs", diff --git a/src/components/ui/Banner.tsx b/src/components/ui/Banner.tsx index 20f7bd83..dad55a68 100644 --- a/src/components/ui/Banner.tsx +++ b/src/components/ui/Banner.tsx @@ -10,11 +10,8 @@ export const Banner = createComponent( base: "bg-gray-200 dark:bg-gray-800", variants: { size: { - md: "h-24 rounded-2xl", - lg: "h-80 rounded-3xl", - }, - rounded: { - full: "rounded-full", + md: "h-24 rounded-t-xl", + lg: "h-80 rounded-t-xl", }, }, defaultVariants: { diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index cd17c494..29848d56 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -5,20 +5,25 @@ import { tv } from "tailwind-variants"; import { createComponent } from "."; const button = tv({ - base: "inline-flex items-center justify-center font-semibold text-center transition-colors rounded-full duration-150 whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 dark:ring-offset-gray-800", + base: "inline-flex items-center justify-center font-semibold uppercase rounded-lg text-center transition-colors duration-150 whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", 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", + primary: "bg-black text-white hover:bg-blue-950", + inverted: + "text-black border border-black hover:text-blue-500 hover:border-blue-500", + tertiary: + "bg-blue-50 text-blue-500 border border-blue-500 hover:bg-blue-100", + secondary: "bg-blue-500 text-white hover:bg-blue-600", 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", - inverted: "bg-white text-black hover:bg-white/90", - link: "bg-none hover:underline", - outline: "border-2 hover:bg-white/5", + outline: "border border-gray-200 hover:border-gray-300", + disabled: + "border border-gray-200 bg-gray-50 text-gray-200 cursor-not-allowed", + none: "", }, size: { - sm: "px-3 py-2 h-10 min-w-[40px]", - default: "px-4 py-2 h-12", + sm: "px-3 py-2 h-8 text-xs rounded-md", + default: "px-4 py-2 h-10 w-full", + auto: "px-4 py-2 h-10 w-auto", icon: "h-12 w-12", }, disabled: { @@ -26,7 +31,7 @@ const button = tv({ }, }, defaultVariants: { - variant: "default", + variant: "none", size: "default", }, }); diff --git a/src/components/ui/Chip.tsx b/src/components/ui/Chip.tsx index 5e83c2e3..78cf4c27 100644 --- a/src/components/ui/Chip.tsx +++ b/src/components/ui/Chip.tsx @@ -3,8 +3,16 @@ import { tv } from "tailwind-variants"; import { createComponent } from "."; const chip = tv({ - base: "border border-gray-700 rounded-full min-w-[42px] px-2 md:px-3 py-2 cursor-pointer inline-flex justify-center items-center whitespace-nowrap text-neutral-200 hover:text-neutral-100", - variants: {}, + base: "rounded-md min-w-[42px] px-2 md:px-3 py-2 cursor-pointer inline-flex justify-center items-center whitespace-nowrap uppercase", + variants: { + color: { + primary: "text-white bg-black border-none", + secondary: "text-black bg-white border border-black", + neutral: "text-blue-500 bg-blue-50 border border-blue-500", + disabled: + "cursor-not-allowed text-gray-500 bg-gray-50 border border-gray-500", + }, + }, }); export const Chip = createComponent("button", chip); diff --git a/src/components/ui/Dialog.tsx b/src/components/ui/Dialog.tsx index 0b14943f..1bc6019f 100644 --- a/src/components/ui/Dialog.tsx +++ b/src/components/ui/Dialog.tsx @@ -1,26 +1,17 @@ import * as RadixDialog from "@radix-ui/react-dialog"; +import type { ReactNode, PropsWithChildren, ComponentProps } from "react"; import { X } from "lucide-react"; import { tv } from "tailwind-variants"; -import { theme } from "~/config"; - -import type { ReactNode, PropsWithChildren, ComponentProps } from "react"; - -import { IconButton } from "./Button"; - +import { IconButton, Button } from "./Button"; import { createComponent } from "."; - -export interface IDialogProps extends PropsWithChildren { - title?: string | ReactNode; - isOpen?: boolean; - size?: "sm" | "md"; - onOpenChange?: ComponentProps["onOpenChange"]; -} +import { theme } from "~/config"; +import { Spinner } from "./Spinner"; const Content = createComponent( RadixDialog.Content, tv({ - base: "z-20 fixed bottom-0 rounded-t-2xl bg-white dark:bg-gray-900 dark:text-white px-7 py-6 w-full sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-2xl", + base: "z-20 fixed bottom-0 rounded-md bg-white p-12 flex flex-col justify-center gap-4 items-center text-center w-full font-sans sm:bottom-auto sm:left-1/2 sm:top-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2", variants: { size: { sm: "sm:w-[456px] md:w-[456px]", @@ -34,30 +25,59 @@ const Content = createComponent( ); export const Dialog = ({ - title = 0, - size = undefined, - isOpen = false, + title, + description, + size, + isOpen, + isLoading, + button, + buttonName, + buttonAction, children, - onOpenChange = undefined, -}: IDialogProps): JSX.Element => ( - - - - - {/* Because of Portal we need to set the theme here */} -
- - {title} - - {children} - - {onOpenChange ? ( - - - - ) : null} - -
-
-
-); + onOpenChange, +}: { + title?: string | ReactNode; + description?: string | ReactNode; + size?: "sm" | "md"; + isOpen?: boolean; + isLoading?: boolean; + button?: "primary" | "secondary"; + buttonName?: string; + buttonAction?: () => void; + onOpenChange?: ComponentProps["onOpenChange"]; +} & PropsWithChildren) => { + return ( + + + + {/* Because of Portal we need to set the theme here */} +
+ + + {title} + + + {description} + + {children} + {isLoading && } + {!isLoading && button && buttonName && buttonAction && ( + + )} + {onOpenChange ? ( + + + + ) : null} + +
+
+
+ ); +}; diff --git a/src/components/ui/Form.tsx b/src/components/ui/Form.tsx index 4969aa07..1eac80ea 100644 --- a/src/components/ui/Form.tsx +++ b/src/components/ui/Form.tsx @@ -24,57 +24,10 @@ import { type z } from "zod"; import { cn } from "~/utils/classNames"; import { IconButton } from "./Button"; +import { inputBase, Input, InputWrapper, InputIcon } from "./Input"; import { createComponent } from "."; -const inputBase = [ - "dark:bg-gray-900", - "dark:text-gray-300", - "dark:border-gray-700", - "rounded", - "disabled:opacity-30", - "checked:bg-gray-800", -]; - -export const Input = createComponent( - "input", - tv({ - base: ["w-full", ...inputBase], - variants: { - error: { - true: "!border-red-900", - }, - }, - }), -); - -export const InputWrapper = createComponent( - "div", - tv({ - base: "flex w-full relative", - variants: {}, - }), -); - -export const InputAddon = createComponent( - "div", - tv({ - base: "absolute right-0 text-gray-900 dark:text-gray-300 inline-flex items-center justify-center h-full border-gray-300 dark:border-gray-800 border-l px-4 font-semibold", - variants: { - disabled: { - true: "text-gray-500 dark:text-gray-500", - }, - }, - }), -); - -export const InputIcon = createComponent( - "div", - tv({ - base: "absolute text-gray-600 left-0 inline-flex items-center justify-center h-full px-4", - }), -); - export const Select = createComponent( "select", tv({ @@ -182,8 +135,11 @@ export const FieldArray = ({ return (
- {error &&
{String(error)}
} - + {error && ( +
+ {String(error)} +
+ )} {fields.map((field, i) => (
{renderField(field, i)} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 00000000..a50c013b --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,53 @@ +import { tv } from "tailwind-variants"; +import { createComponent } from "."; + +export const inputBase = [ + "dark:bg-gray-900", + "dark:text-gray-300", + "dark:border-gray-700", + "disabled:opacity-30", + "checked:bg-gray-800", + "outline-none", + "border-gray-200", + "rounded-lg", + "border", +]; + +export const Input = createComponent( + "input", + tv({ + base: ["w-full", ...inputBase], + variants: { + error: { + true: "!border-red-900", + }, + }, + }), +); + +export const InputWrapper = createComponent( + "div", + tv({ + base: "flex w-full relative", + variants: {}, + }), +); + +export const InputAddon = createComponent( + "div", + tv({ + base: "absolute right-0 text-gray-900 dark:text-gray-300 inline-flex items-center justify-center h-full border-gray-300 dark:border-gray-800 border-l px-4 font-semibold", + variants: { + disabled: { + true: "text-gray-500 dark:text-gray-500", + }, + }, + }), +); + +export const InputIcon = createComponent( + "div", + tv({ + base: "absolute text-gray-600 left-0 inline-flex items-center justify-center h-full px-4", + }), +); diff --git a/src/components/ui/Link.tsx b/src/components/ui/Link.tsx index aef90b5b..5b8bbba8 100644 --- a/src/components/ui/Link.tsx +++ b/src/components/ui/Link.tsx @@ -9,7 +9,7 @@ import { createComponent } from "."; export const Link = createComponent( NextLink, tv({ - base: "font-semibold underline-offset-2 hover:underline text-secondary-600", + base: "flex items-center gap-1 text-blue-400 hover:underline", }), ); diff --git a/src/components/ui/Logo.tsx b/src/components/ui/Logo.tsx new file mode 100644 index 00000000..1dce0aee --- /dev/null +++ b/src/components/ui/Logo.tsx @@ -0,0 +1,14 @@ +import { config, metadata } from "~/config"; +import Image from "next/image"; + +export const Logo = () => ( +
+ {config.logoUrl ? ( + logo + ) : ( +
+ {metadata.title} +
+ )} +
+); diff --git a/src/components/ui/Navigator.tsx b/src/components/ui/Navigator.tsx new file mode 100644 index 00000000..493930f2 --- /dev/null +++ b/src/components/ui/Navigator.tsx @@ -0,0 +1,19 @@ +import Link from "next/link"; + +interface NavigatorProps { + projectName: string; +} + +export const Navigator = ({ projectName }: NavigatorProps) => { + return ( +
+ + Projects + + {">"} + + {projectName} + +
+ ); +}; diff --git a/src/components/ui/Notification.tsx b/src/components/ui/Notification.tsx new file mode 100644 index 00000000..273148b3 --- /dev/null +++ b/src/components/ui/Notification.tsx @@ -0,0 +1,46 @@ +import { RiErrorWarningLine } from "react-icons/ri"; +import { tv } from "tailwind-variants"; +import clsx from "clsx"; + +import { createComponent } from "."; + +const notification = tv({ + base: "w-full flex items-start text-sm justify-center gap-1 text-base", + variants: { + variant: { + default: "text-blue-400", + block: "text-blue-700 bg-blue-400 border border-blue-700 rounded-lg p-4", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +const NotificationContainer = createComponent("div", notification); + +interface NotificationProps { + content: string; + variant?: string; + italic?: boolean; + title?: string; +} + +export const Notification = ({ + content, + variant, + italic, + title, +}: NotificationProps) => { + return ( + + + + +
+ {title ?? null} +

{content}

+
+
+ ); +}; diff --git a/src/components/ui/Table.tsx b/src/components/ui/Table.tsx index ad263ccb..7be40608 100644 --- a/src/components/ui/Table.tsx +++ b/src/components/ui/Table.tsx @@ -5,16 +5,21 @@ import { createComponent } from "."; export const Table = createComponent( "table", tv({ - base: "w-full", + base: "w-full border-separate border-spacing-y-4 border-spacing-x-0", }), ); export const Thead = createComponent("thead", tv({ base: "" })); export const Tbody = createComponent("tbody", tv({ base: "" })); -export const Tr = createComponent( - "tr", +export const Tr = createComponent("tr", tv({ base: "" })); +export const Td = createComponent( + "td", tv({ - base: "border-b dark:border-gray-800 last:border-none", + base: "p-4 border-y border-gray-200", + variants: { + variant: { + first: "border-l rounded-l-lg", + last: "border-r rounded-r-lg", + }, + }, }), ); -export const Th = createComponent("th", tv({ base: "text-left" })); -export const Td = createComponent("td", tv({ base: "px-1 py-2" })); diff --git a/src/components/ui/Tag.tsx b/src/components/ui/Tag.tsx index ead931dd..953545da 100644 --- a/src/components/ui/Tag.tsx +++ b/src/components/ui/Tag.tsx @@ -5,7 +5,7 @@ import { createComponent } from "."; export const Tag = createComponent( "div", tv({ - base: "cursor-pointer inline-flex items-center border border-gray-200 justify-center gap-2 bg-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 text-gray-700 whitespace-nowrap transition", + base: "cursor-pointer inline-flex items-center border border-blue-400 justify-center gap-2 text-blue-400 whitespace-nowrap transition hover:bg-blue-50", variants: { size: { sm: "rounded py-1 px-2 text-xs", @@ -13,10 +13,10 @@ export const Tag = createComponent( lg: "rounded-xl py-2 px-4 text-lg", }, selected: { - true: "border-gray-900 dark:border-gray-300", + true: "bg-blue-400 text-white", }, disabled: { - true: "opacity-50 cursor-not-allowed", + true: "border-gray-200 text-gray-200 cursor-not-allowed", }, }, defaultVariants: { diff --git a/src/config.ts b/src/config.ts index a9a9bea5..2428e9e6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,17 +8,17 @@ export const metadata = { }; export const config = { - logoUrl: "", + logoUrl: "/Logo.svg", pageSize: 3 * 4, // TODO: temp solution until we come up with solid one // https://github.com/privacy-scaling-explorations/maci-rpgf/issues/31 voteLimit: 50, startsAt: new Date(process.env.NEXT_PUBLIC_START_DATE!), registrationEndsAt: new Date(process.env.NEXT_PUBLIC_REGISTRATION_END_DATE!), - reviewEndsAt: new Date(process.env.NEXT_PUBLIC_REVIEW_END_DATE!), resultsAt: new Date(process.env.NEXT_PUBLIC_RESULTS_DATE!), skipApprovedVoterCheck: ["true", "1"].includes(process.env.NEXT_PUBLIC_SKIP_APPROVED_VOTER_CHECK!), tokenName: process.env.NEXT_PUBLIC_TOKEN_NAME!, + eventName: process.env.NEXT_PUBLIC_EVENT_NAME ?? "MACI-RPGF", roundId: process.env.NEXT_PUBLIC_ROUND_ID!, admin: (process.env.NEXT_PUBLIC_ADMIN_ADDRESS ?? "") as `0x${string}`, network: wagmiChains[process.env.NEXT_PUBLIC_CHAIN_NAME as keyof typeof wagmiChains], @@ -28,10 +28,11 @@ export const config = { tallyUrl: process.env.NEXT_PUBLIC_TALLY_URL, roundOrganizer: process.env.NEXT_PUBLIC_ROUND_ORGANIZER ?? "Optimism", pollMode: process.env.NEXT_PUBLIC_POLL_MODE ?? "non-qv", + roundLogo: process.env.NEXT_PUBLIC_ROUND_LOGO, }; export const theme = { - colorMode: "dark", + colorMode: "light", }; export const eas = { diff --git a/src/contexts/Ballot.tsx b/src/contexts/Ballot.tsx index 06c59ded..0ea08aa7 100644 --- a/src/contexts/Ballot.tsx +++ b/src/contexts/Ballot.tsx @@ -12,6 +12,7 @@ const defaultBallot = { votes: [], published: false }; export const BallotProvider: React.FC = ({ children }: BallotProviderProps) => { const [ballot, setBallot] = useState(defaultBallot); + const [isLoading, setLoading] = useState(true); const { isDisconnected } = useAccount(); @@ -90,6 +91,7 @@ export const BallotProvider: React.FC = ({ children }: Ball ) as typeof defaultBallot; setBallot(savedBallot); + setLoading(false); }, [setBallot]); /// store ballot to localStorage once it changes @@ -108,6 +110,7 @@ export const BallotProvider: React.FC = ({ children }: Ball const value = useMemo( () => ({ ballot, + isLoading, addToBallot, removeFromBallot, deleteBallot, @@ -115,7 +118,7 @@ export const BallotProvider: React.FC = ({ children }: Ball sumBallot, publishBallot, }), - [ballot, addToBallot, removeFromBallot, deleteBallot, ballotContains, sumBallot, publishBallot], + [ballot, isLoading, addToBallot, removeFromBallot, deleteBallot, ballotContains, sumBallot, publishBallot], ); return {children}; diff --git a/src/contexts/Maci.tsx b/src/contexts/Maci.tsx index 27157cbe..de595e48 100644 --- a/src/contexts/Maci.tsx +++ b/src/contexts/Maci.tsx @@ -137,7 +137,7 @@ export const MaciProvider: React.FC = ({ children }: MaciProv } if (!votes.length) { - await onError(); + onError(); setError("No votes provided"); return; } diff --git a/src/contexts/types.ts b/src/contexts/types.ts index 02a8f8c5..17bdaf34 100644 --- a/src/contexts/types.ts +++ b/src/contexts/types.ts @@ -33,7 +33,8 @@ export interface MaciProviderProps { } export interface BallotContextType { - ballot?: Ballot; + ballot: Ballot; + isLoading: boolean; addToBallot: (votes: Vote[], pollId: string) => void; removeFromBallot: (projectId: string) => void; deleteBallot: () => void; diff --git a/src/env.js b/src/env.js index 53480895..9c836169 100644 --- a/src/env.js +++ b/src/env.js @@ -58,6 +58,7 @@ export const env = createEnv({ 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_WALLETCONNECT_ID: z.string().optional(), NEXT_PUBLIC_ALCHEMY_ID: z.string().optional(), @@ -71,6 +72,7 @@ export const env = createEnv({ NEXT_PUBLIC_TALLY_URL: z.string().url(), NEXT_PUBLIC_POLL_MODE: z.enum(["qv", "non-qv"]).default("non-qv"), + NEXT_PUBLIC_ROUND_LOGO: z.string().optional(), }, /** @@ -102,6 +104,7 @@ export const env = createEnv({ NEXT_PUBLIC_APPROVAL_SCHEMA: process.env.NEXT_PUBLIC_APPROVAL_SCHEMA, 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_MACI_ADDRESS: process.env.NEXT_PUBLIC_MACI_ADDRESS, @@ -111,6 +114,7 @@ export const env = createEnv({ NEXT_PUBLIC_TALLY_URL: process.env.NEXT_PUBLIC_TALLY_URL, NEXT_PUBLIC_POLL_MODE: process.env.NEXT_PUBLIC_POLL_MODE, + NEXT_PUBLIC_ROUND_LOGO: process.env.NEXT_PUBLIC_ROUND_LOGO, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/src/features/applications/components/ApplicationForm.tsx b/src/features/applications/components/ApplicationForm.tsx index 2573d372..3d9888eb 100644 --- a/src/features/applications/components/ApplicationForm.tsx +++ b/src/features/applications/components/ApplicationForm.tsx @@ -13,11 +13,11 @@ import { Form, FormControl, FormSection, - Input, Label, Select, Textarea, } from "~/components/ui/Form"; +import { Input } from "~/components/ui/Input"; import { Spinner } from "~/components/ui/Spinner"; import { Tag } from "~/components/ui/Tag"; import { impactCategories } from "~/config"; diff --git a/src/features/ballot/components/AllocationInput.tsx b/src/features/ballot/components/AllocationInput.tsx index 8d9d5b70..6cccb8e2 100644 --- a/src/features/ballot/components/AllocationInput.tsx +++ b/src/features/ballot/components/AllocationInput.tsx @@ -2,7 +2,7 @@ import { type ComponentPropsWithRef } from "react"; import { useFormContext, Controller } from "react-hook-form"; import { NumericFormat } from "react-number-format"; -import { Input, InputAddon, InputWrapper } from "~/components/ui/Form"; +import { Input, InputAddon, InputWrapper } from "~/components/ui/Input"; import { config } from "~/config"; export interface IAllocationInputProps extends ComponentPropsWithRef<"input"> { @@ -22,7 +22,7 @@ export const AllocationInput = ({ const form = useFormContext(); return ( - + { - const { data: projects } = useProjectById(id); - const project = projects?.[0]; - const Component = link ? Link : "div"; +import { useMaci } from "~/contexts/Maci"; +import { useBallot } from "~/contexts/Ballot"; +import { config } from "~/config"; +import { ProjectAvatarWithName } from "./ProjectAvatarWithName"; +export const AllocationList = ({ votes }: { votes?: Vote[] }) => { return ( - - - -
-
{project?.name}
- -
{subtitle}
-
-
- ); -}; - -export const AllocationList = ({ votes = [] }: IAllocationListProps): JSX.Element => ( - - {votes.map((project) => ( + {votes?.map((project) => ( - + - - ))}
- + + + + {formatNumber(project.amount)} {config.tokenName} {`${formatNumber(project.amount)} ${config.tokenName}`}
-
-); + ); +}; -interface AllocationFormProps { +type AllocationFormProps = { disabled?: boolean; projectIsLink?: boolean; renderHeader?: () => ReactNode; - renderExtraColumn?: ( - { form, project }: { form: UseFormReturn<{ votes: Vote[] }>; project: Vote }, - i: number, - ) => ReactNode; } -export const AllocationFormWrapper = ({ - disabled = false, - projectIsLink = false, - renderHeader = undefined, - renderExtraColumn = undefined, -}: AllocationFormProps): JSX.Element => { +export function AllocationFormWrapper({ + disabled, + projectIsLink, + renderHeader, +}: AllocationFormProps) { const form = useFormContext<{ votes: Vote[] }>(); const { initialVoiceCredits, pollId } = useMaci(); const { addToBallot: onSave, removeFromBallot: onRemove } = useBallot(); @@ -106,63 +54,48 @@ export const AllocationFormWrapper = ({ }); return ( - - - {renderHeader?.()} - - - {fields.map((project, i) => { - const idx = i; - - return ( - - - - - - - - - - ); - })} - -
- - {renderExtraColumn?.({ project, form }, i)} - { - onSave(form.getValues().votes, pollId!); - }} - /> - - { - remove(idx); - onRemove(project.projectId); - }} - /> -
-
+ + {renderHeader?.()} + + {fields.map((project, i) => { + return ( + + + + + + ); + })} + +
+ + + onSave?.(form.getValues().votes, pollId)} + /> + + { + remove(i); + onRemove?.(project.projectId); + }} + /> +
); -}; - -export const DistributionForm = ({ ...props }: AllocationFormProps): JSX.Element => ( - ( - - - - )} - /> -); +} diff --git a/src/features/ballot/components/BallotConfirmation.tsx b/src/features/ballot/components/BallotConfirmation.tsx index 0408a902..deec1f6d 100644 --- a/src/features/ballot/components/BallotConfirmation.tsx +++ b/src/features/ballot/components/BallotConfirmation.tsx @@ -1,85 +1,136 @@ -import { Lock } from "lucide-react"; -import Link from "next/link"; -import React from "react"; import { tv } from "tailwind-variants"; +import Link from "next/link"; +import { useMemo } from "react"; +import { format } from "date-fns"; -import { createComponent } from "~/components/ui"; import { Button } from "~/components/ui/Button"; +import { Notification } from "~/components/ui/Notification"; +import { createComponent } from "~/components/ui"; import { config } from "~/config"; - -import { type Vote } from "../types"; - -import { AllocationList } from "./AllocationList"; +import { useAppState } from "~/utils/state"; +import { useBallot } from "~/contexts/Ballot"; +import { useProjectCount } from "~/features/projects/hooks/useProjects"; +import { ProjectAvatarWithName } from "./ProjectAvatarWithName"; +import { formatNumber } from "~/utils/formatNumber"; +import { EAppState } from "~/utils/types"; const feedbackUrl = process.env.NEXT_PUBLIC_FEEDBACK_URL; -const Card = createComponent("div", tv({ base: "rounded-3xl border p-8 dark:border-gray-700" })); - -export interface IBallotConfirmationProps { - votes: Vote[]; -} - -export const BallotConfirmation = ({ votes }: IBallotConfirmationProps): JSX.Element => ( -
-
- -
-
-

- Your vote has been received 🥳 -

+const Card = createComponent( + "div", + tv({ + base: "rounded-lg border border-blue-400 p-8 bg-blue-50 flex justify-between items-center gap-8 my-14", + }), +); -

- Thank you for participating in this round. If you have 5 minutes, we'd love to hear your feedback on - what we could do better to improve! Your feedback would always remain anonymous. It would, however, - greatly help us continue to iterate on the MACI-RPGF stack to keep learning and implementing improvements - to continue to build a better experience. +export const BallotConfirmation = () => { + const { ballot, sumBallot } = useBallot(); + const allocations = ballot?.votes ?? []; + const { data: projectCount } = useProjectCount(); + const appState = useAppState(); + + const sum = useMemo( + () => formatNumber(sumBallot(ballot?.votes)), + [ballot, sumBallot], + ); + + return ( +

+

+ 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 && ( + +
+ + Wanna change your mind? + +

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

- -
+
+
- -
-
-
- + + )} -
-
Here's how you voted!
- -
- - -

Your vote will always be private

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

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

- -
-

Project name

- -

{config.tokenName} allocated by you

+
+
- -
- -
- -
-
Help us improve the next round of MACI RPGF
- -

- Your anonymized feedback will be influential to help us iterate on the MACI RPGF 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/BallotOverview.tsx b/src/features/ballot/components/BallotOverview.tsx deleted file mode 100644 index 4c4b9d56..00000000 --- a/src/features/ballot/components/BallotOverview.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import clsx from "clsx"; -import dynamic from "next/dynamic"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { type PropsWithChildren, type ReactNode, useState, useCallback } from "react"; -import { toast } from "sonner"; -import { useAccount } from "wagmi"; - -import { Alert } from "~/components/ui/Alert"; -import { Button } from "~/components/ui/Button"; -import { Dialog } from "~/components/ui/Dialog"; -import { Progress } from "~/components/ui/Progress"; -import { Spinner } from "~/components/ui/Spinner"; -import { config } from "~/config"; -import { useBallot } from "~/contexts/Ballot"; -import { useMaci } from "~/contexts/Maci"; -import { useProjectCount, useProjectIdMapping } from "~/features/projects/hooks/useProjects"; -import { formatNumber } from "~/utils/formatNumber"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; - -import { VotingEndsIn } from "./VotingEndsIn"; - -const BallotHeader = ({ children, ...props }: PropsWithChildren): JSX.Element => ( -

- {children} -

-); - -const BallotSection = ({ title, children }: { title: string | ReactNode } & PropsWithChildren) => ( -
-

{title}

- -
{children}
-
-); - -interface ISubmitBallotButtonProps { - disabled?: boolean; -} - -const SubmitBallotButton = ({ disabled = false }: ISubmitBallotButtonProps): JSX.Element => { - const [isOpen, setOpen] = useState(false); - const { isLoading, error, onVote } = useMaci(); - const { ballot, publishBallot } = useBallot(); - - const projectIndices = useProjectIdMapping(ballot); - - const router = useRouter(); - - const handleOpen = useCallback(() => { - setOpen(true); - }, [setOpen]); - - const handleClose = useCallback(() => { - setOpen(false); - }, [setOpen]); - - const submit = { - isLoading, - error, - mutate: async () => { - const votes = - ballot?.votes.map(({ amount, projectId }) => ({ - voteOptionIndex: BigInt(projectIndices[projectId]!), - newVoteWeight: BigInt(amount), - })) ?? []; - - await onVote( - votes, - () => { - toast.error("Voting failed"); - }, - async () => { - await router.push("/ballot/confirmation"); - publishBallot(); - }, - ); - }, - }; - - const messages = { - signing: { - title: "Sign vote", - instructions: "Confirm the transactions in your wallet to submit your vote.", - }, - submitting: { - title: "Submit vote", - instructions: "Once you submit your vote, you won’t be able to change it. If you are ready, go ahead and submit!", - }, - error: { - title: "Error submitting vote", - instructions: ( - - There was an error submitting the vote. - - ), - }, - }; - - const messageKey = submit.error ? "error" : "submitting"; - const { title, instructions } = messages[submit.isLoading ? "signing" : messageKey]; - - return ( - <> - - - -

{instructions}

- -
- - - -
-
- - ); -}; - -const BallotOverview = () => { - const router = useRouter(); - - const { isRegistered, isEligibleToVote, initialVoiceCredits } = useMaci(); - const { sumBallot, ballot } = useBallot(); - - const sum = sumBallot(ballot?.votes); - - const allocations = ballot?.votes ?? []; - const canSubmit = router.route === "/ballot" && allocations.length; - const viewBallot = router.route !== "/ballot" && allocations.length; - - const { data: projectCount } = useProjectCount(); - - const appState = useAppState(); - - const { address } = useAccount(); - - if (appState === EAppState.LOADING) { - return ; - } - - if (appState === EAppState.RESULTS) { - return ( -
- Results are live! - - -
- ); - } - - if (appState === EAppState.TALLYING) { - return ( -
- Voting has ended - - -
- ); - } - - if (appState !== EAppState.VOTING) { - return ( -
- Voting has not started yet - - {appState === EAppState.REVIEWING ? ( - - ) : ( - - )} -
- ); - } - - return ( -
- Voting Round: {config.roundId} - - - - - - {address && isRegistered && ( - <> - Your vote - - -
- {`${allocations.length}/${projectCount?.count}`} -
-
- - - {config.tokenName} allocated: - -
initialVoiceCredits, - })} - > - {`${formatNumber(sum)} ${config.tokenName}`} -
-
- } - > - - -
-
Total
- -
{`${formatNumber(initialVoiceCredits)} ${config.tokenName}`}
-
- - - )} - - {isRegistered && isEligibleToVote ? ( - <> - {ballot?.published && ( - - )} - - {!ballot?.published && canSubmit && initialVoiceCredits} />} - - {!ballot?.published && !canSubmit && viewBallot ? ( - - ) : ( - - )} - - ) : null} -
- ); -}; - -export default dynamic(() => Promise.resolve(BallotOverview), { ssr: false }); 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/AddToBallot.tsx b/src/features/projects/components/AddToBallot.tsx index 5df07067..473ec9dc 100644 --- a/src/features/projects/components/AddToBallot.tsx +++ b/src/features/projects/components/AddToBallot.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { Check } from "lucide-react"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { useFormContext } from "react-hook-form"; import { useAccount } from "wagmi"; import { z } from "zod"; @@ -88,63 +88,62 @@ const ProjectAllocation = ({ ); }; -export const ProjectAddToBallot = ({ id = "", name = "" }: IProjectAddToBallotProps): JSX.Element | null => { +export const ProjectAddToBallot = ({ id, name }: IProjectAddToBallotProps) => { const { address } = useAccount(); const [isOpen, setOpen] = useState(false); - const { isRegistered, isEligibleToVote, initialVoiceCredits, pollId } = useMaci(); - const { ballot, ballotContains, sumBallot, addToBallot, removeFromBallot } = useBallot(); + const { isRegistered, isEligibleToVote, initialVoiceCredits, pollId } = + useMaci(); + const { ballot, ballotContains, sumBallot, addToBallot, removeFromBallot } = + useBallot(); - const inBallot = ballotContains(id); + const inBallot = ballotContains(id!); const allocations = ballot?.votes ?? []; const sum = sumBallot(allocations.filter((p) => p.projectId !== id)); - const numVotes = ballot?.votes.length ?? 0; + const numVotes = ballot?.votes?.length ?? 0; - const dialogMessage = `How much ${config.tokenName} should this Project receive to fill the gap between the impact they generated for - ${config.roundOrganizer} and the profit they received for generating this impact`; - - const handleOpen = useCallback(() => { - setOpen(true); - }, [setOpen]); - - if (useAppState() !== EAppState.VOTING) { - return null; - } + if (useAppState() !== EAppState.VOTING) return null; return (
{numVotes > config.voteLimit && ( - You have exceeded your vote limit. You can only vote for {config.voteLimit} options. + You have exceeded your vote limit. You can only vote for{" "} + {config.voteLimit} options. )} - {isEligibleToVote && isRegistered ? ( - <> - {ballot?.published && } - - {!ballot?.published && inBallot && ( - - {formatNumber(config.pollMode === "qv" ? inBallot.amount ** 2 : inBallot.amount)} allocated - - )} - - {!ballot?.published && !inBallot && ( - - )} - - ) : null} - - -

{dialogMessage}

- + {!isEligibleToVote || !isRegistered ? null : ballot?.published ? ( + + ) : inBallot ? ( + setOpen(true)} + variant="primary" + icon={Check} + > + {formatNumber(config.pollMode === "qv" ? inBallot.amount ** 2 : inBallot.amount)} allocated + + ) : ( + + )} + +

+ How much {config.tokenName} should this Project receive to fill the + gap between the impact they generated for Optimism and the profit they + received for generating this impact +

{ - addToBallot([{ projectId: id, amount }], pollId!); + addToBallot([{ projectId: id!, amount }], pollId); setOpen(false); }} > @@ -163,7 +162,7 @@ export const ProjectAddToBallot = ({ id = "", name = "" }: IProjectAddToBallotPr current={sum} inBallot={Boolean(inBallot)} onRemove={() => { - removeFromBallot(id); + removeFromBallot(id!); setOpen(false); }} /> diff --git a/src/features/projects/components/ProjectContacts.tsx b/src/features/projects/components/ProjectContacts.tsx new file mode 100644 index 00000000..2fb0ddc5 --- /dev/null +++ b/src/features/projects/components/ProjectContacts.tsx @@ -0,0 +1,45 @@ +import { FaXTwitter } from "react-icons/fa6"; +import { FaGithub, FaEthereum } from "react-icons/fa"; +import { RiGlobalLine } from "react-icons/ri"; +import { Link } from "~/components/ui/Link"; + +export const ProjectContacts = ({ + author, + website, + github, + twitter, +}: { + author?: string; + website?: string; + github?: string; + twitter?: string; +}) => { + return ( +
+ {author && ( + + + {author} + + )} + {twitter && ( + + + x.com + + )} + {website && ( + + + {website} + + )} + {github && ( + + + {github} + + )} +
+ ); +}; diff --git a/src/features/projects/components/ProjectContributions.tsx b/src/features/projects/components/ProjectContributions.tsx index 7e8b1a62..0577890f 100644 --- a/src/features/projects/components/ProjectContributions.tsx +++ b/src/features/projects/components/ProjectContributions.tsx @@ -51,4 +51,4 @@ const ProjectContributions = ({ isLoading, project = undefined }: IProjectContri ); -export default ProjectContributions; +export default ProjectContributions; \ No newline at end of file diff --git a/src/features/projects/components/ProjectDescriptionSection.tsx b/src/features/projects/components/ProjectDescriptionSection.tsx new file mode 100644 index 00000000..157ebf77 --- /dev/null +++ b/src/features/projects/components/ProjectDescriptionSection.tsx @@ -0,0 +1,67 @@ +import { FaGithub, FaEthereum } from "react-icons/fa"; +import { RiGlobalLine } from "react-icons/ri"; +import { Link } from "~/components/ui/Link"; + +import { + type ImpactMetrix, + type ContributionLink, + type FundingSource, + EContributionType, +} from "../types"; + +interface ProjectDescriptionSectionProps { + title: string; + description?: string; + links?: ContributionLink[] | ImpactMetrix[]; + fundings?: FundingSource[]; +} + +export const ProjectDescriptionSection = ({ + title, + description, + links, + fundings, +}: ProjectDescriptionSectionProps) => { + return ( +
+

{title}

+ {description &&

{description}

} + {links && ( +
+

{title} links

+ {links.map((link) => ( + + {link.type && link.type === EContributionType.GITHUB_REPO && ( + + )} + {link.type && + link.type === EContributionType.CONTRACT_ADDRESS && ( + + )} + {link.type && link.type === EContributionType.OTHER && ( + + )} + {link.description} + {link.number && ` - ${link.number}k`} + + ))} +
+ )} + {fundings && ( +
+ {fundings.map((funding) => ( +
+ {funding.description} +
+

+ {funding.type.split("_").join(" ").toLowerCase()} +

+

{funding.amount}

+

{funding.currency}

+
+ ))} +
+ )} +
+ ); +}; diff --git a/src/features/projects/components/ProjectDetails.tsx b/src/features/projects/components/ProjectDetails.tsx index ebb91863..7d4f9c2f 100644 --- a/src/features/projects/components/ProjectDetails.tsx +++ b/src/features/projects/components/ProjectDetails.tsx @@ -1,35 +1,39 @@ -import { type ReactNode } from "react"; +import { useMemo } from "react"; -import { NameENS } from "~/components/ENS"; -import { Heading } from "~/components/ui/Heading"; -import { ProjectAvatar } from "~/features/projects/components/ProjectAvatar"; import { ProjectBanner } from "~/features/projects/components/ProjectBanner"; -import { type Attestation } from "~/utils/fetchAttestations"; -import { suffixNumber } from "~/utils/suffixNumber"; - +import { ProjectAvatar } from "~/features/projects/components/ProjectAvatar"; import { useProjectMetadata } from "../hooks/useProjects"; - -import ProjectContributions from "./ProjectContributions"; -import ProjectImpact from "./ProjectImpact"; +import { type Attestation } from "~/utils/fetchAttestations"; +import { Navigator } from "~/components/ui/Navigator"; +import { VotingWidget } from "~/features/projects/components/VotingWidget"; +import { ProjectContacts } from "./ProjectContacts"; +import { ProjectDescriptionSection } from "./ProjectDescriptionSection"; export interface IProjectDetailsProps { - action: ReactNode; + projectId: string; attestation?: Attestation; } -const ProjectDetails = ({ attestation = undefined, action }: IProjectDetailsProps): JSX.Element => { +const ProjectDetails({ + projectId, + attestation = undefined, +}: IProjectDetailsProps) { const metadata = useProjectMetadata(attestation?.metadataPtr); const { bio, websiteUrl, payoutAddress, fundingSources } = metadata.data ?? {}; + const github = useMemo( + () => + metadata.data?.contributionLinks + ? metadata.data.contributionLinks.find((l) => l.type === "GITHUB_REPO") + : undefined, + [metadata, useProjectMetadata], + ); + return (
-
-
-

{attestation?.name}

- - {action} -
+
+
@@ -37,55 +41,42 @@ const ProjectDetails = ({ attestation = undefined, action }: IProjectDetailsProp
- - -
- -
+
- -

{bio}

- -
- - Impact statements - - - - - - - - Past grants and funding - - -
- {fundingSources?.map((source) => { - const type = - { - OTHER: "Other", - RETROPGF_2: "RetroPGF2", - GOVERNANCE_FUND: "Governance Fund", - PARTNER_FUND: "Partner Fund", - REVENUE: "Revenue", - }[source.type] ?? source.type; - return ( -
-
{source.description}
- -
{type}
- -
{`${suffixNumber(source.amount)} ${source.currency}`}
-
- ); - })} -
+
+

{attestation?.name}

+ +
+ +

{bio}

+
+

+ Impact statements +

+ + +
); diff --git a/src/features/projects/components/ProjectItem.tsx b/src/features/projects/components/ProjectItem.tsx index 1ff2d3cc..6e30a559 100644 --- a/src/features/projects/components/ProjectItem.tsx +++ b/src/features/projects/components/ProjectItem.tsx @@ -1,8 +1,14 @@ +import Image from "next/image"; + import { Heading } from "~/components/ui/Heading"; import { Skeleton } from "~/components/ui/Skeleton"; +import { Button } from "~/components/ui/Button"; import { config } from "~/config"; import { type Attestation } from "~/utils/fetchAttestations"; import { formatNumber } from "~/utils/formatNumber"; +import { useAppState } from "~/utils/state"; +import { EAppState } from "~/utils/types"; +import { EProjectState } from "../types"; import { useProjectMetadata } from "../hooks/useProjects"; @@ -13,37 +19,64 @@ import { ProjectBanner } from "./ProjectBanner"; export interface IProjectItemProps { attestation: Attestation; isLoading: boolean; + state: EProjectState; + action: (e: Event) => void; } -export const ProjectItem = ({ attestation, isLoading }: IProjectItemProps): JSX.Element => { - const metadata = useProjectMetadata(attestation.metadataPtr); +export function ProjectItem({ + attestation, + isLoading, + state, + action, +}: IProjectItemProps) { + const metadata = useProjectMetadata(attestation?.metadataPtr); + const appState = useAppState(); return (
- - - {attestation.name} - - -
-

- +

+ + {attestation?.name} + +

+ {metadata.data?.bio}

+ + + + {!isLoading && appState === EAppState.VOTING && ( +
+ + {state === EProjectState.DEFAULT && ( + + )} + {state === EProjectState.ADDED && ( + + )} + {state === EProjectState.SUBMITTED && ( + + )} + +
+ )}
- - - -
); }; diff --git a/src/features/projects/components/ProjectSelectButton.tsx b/src/features/projects/components/ProjectSelectButton.tsx deleted file mode 100644 index 421c34b6..00000000 --- a/src/features/projects/components/ProjectSelectButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { AlbumIcon, CheckIcon, PlusIcon } from "lucide-react"; -import { type ComponentProps } from "react"; -import { tv } from "tailwind-variants"; - -import { createComponent } from "~/components/ui"; - -const ActionButton = createComponent( - "button", - tv({ - base: "flex h-6 w-6 items-center justify-center rounded-full border-2 border-transparent transition-colors bg-gray-100 dark:bg-gray-900", - variants: { - color: { - default: - "dark:border-white/50 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:border-white", - highlight: "hover:bg-white dark:hover:bg-gray-800 dark:border-white dark:text-white", - green: "border-transparent border-gray-100 dark:border-gray-900 text-gray-500", - }, - }, - defaultVariants: { color: "default" }, - }), -); - -interface IProjectSelectButtonProps extends ComponentProps { - state: 0 | 1 | 2; -} - -export const ProjectSelectButton = ({ state, ...props }: IProjectSelectButtonProps): JSX.Element => { - const { color, icon: Icon } = { - 0: { color: "default", icon: PlusIcon }, - 1: { color: "highlight", icon: CheckIcon }, - 2: { color: "green", icon: AlbumIcon }, - }[state]; - - return ( - - - - ); -}; diff --git a/src/features/projects/components/Projects.tsx b/src/features/projects/components/Projects.tsx index 362fa396..44955dfb 100644 --- a/src/features/projects/components/Projects.tsx +++ b/src/features/projects/components/Projects.tsx @@ -1,97 +1,83 @@ import clsx from "clsx"; -import { XIcon } from "lucide-react"; import Link from "next/link"; -import { useCallback } from "react"; import { InfiniteLoading } from "~/components/InfiniteLoading"; -import { SortFilter } from "~/components/SortFilter"; -import { Alert } from "~/components/ui/Alert"; -import { Button } from "~/components/ui/Button"; -import { config } from "~/config"; -import { useMaci } from "~/contexts/Maci"; -import { useResults } from "~/hooks/useResults"; +import { useSearchProjects } from "../hooks/useProjects"; import { useAppState } from "~/utils/state"; import { EAppState } from "~/utils/types"; - -import { useSearchProjects } from "../hooks/useProjects"; -import { useSelectProjects } from "../hooks/useSelectProjects"; - +import { EProjectState } from "../types"; +import { useResults } from "~/hooks/useResults"; +import { SortFilter } from "~/components/SortFilter"; import { ProjectItem, ProjectItemAwarded } from "./ProjectItem"; -import { ProjectSelectButton } from "./ProjectSelectButton"; +import { useMaci } from "~/contexts/Maci"; +import { useBallot } from "~/contexts/Ballot"; export const Projects = (): JSX.Element => { const projects = useSearchProjects(); - const select = useSelectProjects(); const appState = useAppState(); - const { isRegistered, pollData } = useMaci(); + const { pollData, pollId, isRegistered } = useMaci(); + const { addToBallot, removeFromBallot, ballotContains, ballot } = useBallot(); const results = useResults(pollData); - const handleAdd = useCallback(() => { - select.add(); - }, [select]); + const handleAction = (e: Event, projectId: string) => { + e.preventDefault(); + + if (!ballotContains(projectId)) { + addToBallot( + [ + { + projectId, + amount: 0, + }, + ], + pollId, + ); + } else { + removeFromBallot(projectId); + } + }; - const handleReset = useCallback(() => { - select.reset(); - }, [select]); + const defineState = (projectId: string): EProjectState => { + if (!isRegistered) return EProjectState.UNREGISTERED; + else if (ballotContains(projectId) && ballot?.published) + return EProjectState.SUBMITTED; + else if (ballotContains(projectId) && !ballot?.published) + return EProjectState.ADDED; + else return EProjectState.DEFAULT; + }; return (
- {select.count > config.voteLimit && ( - - You have exceeded your vote limit. You can only vote for {config.voteLimit} options. - - )} - -
- - - -
- -
- +
+

Projects

+
+ +
( - - {isRegistered && !isLoading && appState === EAppState.VOTING ? ( -
- { - e.preventDefault(); - select.toggle(item.id); - }} + renderItem={(item, { isLoading }) => { + return ( + + {!results.isLoading && appState === EAppState.RESULTS ? ( + -
- ) : null} - - {!results.isLoading && appState === EAppState.RESULTS ? ( - - ) : null} - - - - )} + ) : null} + handleAction(e, item.id)} + /> + + ); + }} />
); diff --git a/src/features/projects/components/VotingWidget.tsx b/src/features/projects/components/VotingWidget.tsx new file mode 100644 index 00000000..4ffa1a5a --- /dev/null +++ b/src/features/projects/components/VotingWidget.tsx @@ -0,0 +1,101 @@ +import { useMemo, useCallback, useState } from "react"; + +import { useMaci } from "~/contexts/Maci"; +import { useBallot } from "~/contexts/Ballot"; +import { Input } from "~/components/ui/Input"; +import { Button } from "~/components/ui/Button"; +import { EButtonState } from "../types"; + +export const VotingWidget = ({ projectId }: { projectId: string }) => { + const { pollId } = useMaci(); + const { ballotContains, removeFromBallot, addToBallot } = useBallot(); + const projectBallot = useMemo( + () => ballotContains(projectId), + [ballotContains, projectId], + ); + const projectIncluded = useMemo(() => !!projectBallot, [projectBallot]); + const [amount, setAmount] = useState( + projectBallot?.amount, + ); + + /** + * buttonState + * 0. this project is not included in the ballot before + * 1. this project is included in the ballot before + * 2. after onChange from a value to another value (original state is 1) + * 3. after edited + */ + const [buttonState, setButtonState] = useState( + projectIncluded ? EButtonState.ADDED : EButtonState.DEFAULT, + ); + + const handleRemove = useCallback(() => { + removeFromBallot(projectId); + setAmount(undefined); + setButtonState(0); + }, [removeFromBallot]); + + const handleInput = (e: Event) => { + setAmount(e.target?.value as number); + + if ( + buttonState === EButtonState.ADDED || + buttonState === EButtonState.UPDATED + ) { + setButtonState(EButtonState.EDIT); + } + }; + + const handleButtonAction = () => { + if (!amount) return; + + addToBallot([{ projectId, amount }], pollId); + if (buttonState === EButtonState.DEFAULT) + setButtonState(EButtonState.ADDED); + else setButtonState(EButtonState.UPDATED); + }; + + return ( +
+ {projectIncluded && ( +
+ Remove from My Ballot +
+ )} +
+ + {buttonState === EButtonState.DEFAULT && ( + + )} + {buttonState === EButtonState.ADDED && ( +
+ votes added + +
+ )} + {buttonState === EButtonState.EDIT && ( + + )} + {buttonState === EButtonState.UPDATED && ( +
+ votes updated + +
+ )} +
+
+ ); +}; diff --git a/src/features/projects/types.ts b/src/features/projects/types.ts new file mode 100644 index 00000000..1ca21a80 --- /dev/null +++ b/src/features/projects/types.ts @@ -0,0 +1,46 @@ +export enum EContributionType { + CONTRACT_ADDRESS = "CONTRACT_ADDRESS", + GITHUB_REPO = "GIGHUB_REPO", + OTHER = "OTHER", +} + +export enum EFundingSourceType { + OTHER = "OTHER", + RETROPGF_2 = "RETROPGF_2", + GOVERNANCE_FUND = "GOVERNANCE_FUND", + PARTNER_FUND = "PARTNER_FUND", + REVENUE = "REVENUE", +} + +export enum EButtonState { + DEFAULT, + ADDED, + EDIT, + UPDATED, +} + +export enum EProjectState { + UNREGISTERED, + DEFAULT, + ADDED, + SUBMITTED, +} + +export interface ImpactMetrix { + url: string; + description: string; + number: number; +} + +export interface ContributionLink { + url: string; + type: EContributionType; + description: string; +} + +export interface FundingSource { + type: EFundingSourceType; + description: string; + currency: string; + amount: number; +} diff --git a/src/layouts/BaseLayout.tsx b/src/layouts/BaseLayout.tsx index ee4f9bcc..c3a9aebf 100644 --- a/src/layouts/BaseLayout.tsx +++ b/src/layouts/BaseLayout.tsx @@ -2,7 +2,15 @@ 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, useMemo } from "react"; +import { + type ReactNode, + type PropsWithChildren, + createContext, + useContext, + useEffect, + useCallback, + useMemo, +} from "react"; import { useAccount } from "wagmi"; import { Footer } from "~/components/Footer"; @@ -28,7 +36,9 @@ export interface LayoutProps { requireAuth?: boolean; eligibilityCheck?: boolean; showBallot?: boolean; -} + showInfo?: boolean; + showSubmitButton?: boolean; +}; export const BaseLayout = ({ header = null, @@ -50,12 +60,16 @@ export const BaseLayout = ({ const router = useRouter(); const { address, isConnecting } = useAccount(); - useEffect(() => { + const manageDisplay = useCallback(async () => { if (requireAuth && !address && !isConnecting) { - router.push("/"); + await router.push("/"); } }, [requireAuth, address, isConnecting, router]); + useEffect(() => { + manageDisplay(); + }, [manageDisplay]); + const wrappedSidebar = {sidebarComponent}; const contextValue = useMemo(() => ({ eligibilityCheck, showBallot }), [eligibilityCheck, showBallot]); @@ -91,11 +105,19 @@ export const BaseLayout = ({ - -
+
{header} - -
+
{sidebar === "left" ? wrappedSidebar : null}
{ const { address } = useAccount(); const appState = useAppState(); + const { ballot } = useBallot(); - const navLinks = [ - { - href: "/projects", - children: "Projects", - }, - { - href: "/info", - children: "Info", - }, - ]; + 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 ( }> @@ -61,6 +77,31 @@ export const Layout = ({ children = null, ...props }: Props): JSX.Element => { ); }; -export const LayoutWithBallot = ({ ...props }: Props): JSX.Element => ( - } {...props} /> -); +export function LayoutWithSidebar({ ...props }: Props) { + const { isRegistered } = useMaci(); + const { address } = useAccount(); + const { ballot } = useBallot(); + + return ( + + {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 555d3252..d385edfd 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"; const BallotConfirmationPage = (): JSX.Element | null => { - 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 cec67f7c..aaea11cd 100644 --- a/src/pages/ballot/index.tsx +++ b/src/pages/ballot/index.tsx @@ -7,12 +7,11 @@ 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 { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; import { AllocationFormWrapper } from "~/features/ballot/components/AllocationList"; import { BallotSchema, type Vote } from "~/features/ballot/types"; -import { LayoutWithBallot } from "~/layouts/DefaultLayout"; +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import { formatNumber } from "~/utils/formatNumber"; import { useAppState } from "~/utils/state"; import { EAppState } from "~/utils/types"; @@ -34,28 +33,23 @@ const ClearBallot = () => { return ( <> - - - -

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

- -
- -
-
+ Remove all projects +
+ + ); }; @@ -70,7 +64,7 @@ const EmptyBallot = () => (

-
@@ -78,50 +72,44 @@ const EmptyBallot = () => (
); -const TotalAllocation = () => { - const { sumBallot } = useBallot(); - const { initialVoiceCredits } = useMaci(); - const form = useFormContext<{ votes: Vote[] }>(); - const votes = form.watch("votes"); - const sum = sumBallot(votes); - - return
{`${formatNumber(sum)} / ${initialVoiceCredits} ${config.tokenName}`}
; -}; - const BallotAllocationForm = () => { const appState = useAppState(); - const { ballot } = useBallot(); - - return ( -
-

Review your vote

- -

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

+ const { ballot, sumBallot } = useBallot(); -
{ballot?.votes.length ? : null}
+ const sum = useMemo( + () => formatNumber(sumBallot(ballot?.votes)), + [ballot, sumBallot], + ); -
+ return ( +
+

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
- -
- -
+
+

Total votes:

+

{sum}

); -}; +} const BallotPage = (): JSX.Element => { const { address, isConnecting } = useAccount(); @@ -130,26 +118,31 @@ const BallotPage = (): JSX.Element => { useEffect(() => { if (!address && !isConnecting) { - // eslint-disable-next-line no-console - router.push("/").catch(console.error); + router.push("/").catch(console.log); } }, [address, isConnecting, router]); - const votes = useMemo(() => ballot?.votes.sort((a, b) => b.amount - a.amount), [ballot]); + const votes = useMemo( + () => ballot?.votes?.sort((a, b) => b.amount - a.amount), + [ballot], + ); if (!votes) { return ; } return ( - - null}> + + - -
- + ); -}; +} export default BallotPage; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 91e87ccf..669ea527 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,15 +1,8 @@ import { type GetServerSideProps } from "next"; -import { Layout } from "~/layouts/DefaultLayout"; - -const ProjectsPage = (): JSX.Element => ...; - -export default ProjectsPage; - -export const getServerSideProps: GetServerSideProps = async () => - Promise.resolve({ - redirect: { - destination: "/projects", - permanent: false, - }, - }); +export const getServerSideProps: GetServerSideProps = async () => ({ + redirect: { + destination: "/signup", + permanent: false, + }, +}); diff --git a/src/pages/projects/[projectId]/Project.tsx b/src/pages/projects/[projectId]/Project.tsx index 713d857c..1e3da589 100644 --- a/src/pages/projects/[projectId]/Project.tsx +++ b/src/pages/projects/[projectId]/Project.tsx @@ -1,12 +1,8 @@ import { type GetServerSideProps } from "next"; -import { ProjectAddToBallot } from "~/features/projects/components/AddToBallot"; -import { ProjectAwarded } from "~/features/projects/components/ProjectAwarded"; +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import ProjectDetails from "~/features/projects/components/ProjectDetails"; import { useProjectById } from "~/features/projects/hooks/useProjects"; -import { LayoutWithBallot } from "~/layouts/DefaultLayout"; -import { useAppState } from "~/utils/state"; -import { EAppState } from "~/utils/types"; export interface IProjectDetailsProps { projectId?: string; @@ -14,19 +10,18 @@ export interface IProjectDetailsProps { const ProjectDetailsPage = ({ projectId = "" }: IProjectDetailsProps): JSX.Element => { const projects = useProjectById(projectId); - const { name } = projects.data?.[0] ?? {}; - const appState = useAppState(); + const { name } = projects.data?.[0] ?? {};; - const action = - appState === EAppState.RESULTS ? ( - - ) : ( - - ); return ( - - - + + + ); }; diff --git a/src/pages/projects/index.tsx b/src/pages/projects/index.tsx index ab50c306..24d7f967 100644 --- a/src/pages/projects/index.tsx +++ b/src/pages/projects/index.tsx @@ -1,10 +1,12 @@ +import { LayoutWithSidebar } from "~/layouts/DefaultLayout"; import { Projects } from "~/features/projects/components/Projects"; -import { LayoutWithBallot } from "~/layouts/DefaultLayout"; -const ProjectsPage = (): JSX.Element => ( - - - -); +const ProjectsPage = (): JSX.Element => { + return ( + + + + ); +} export default ProjectsPage; diff --git a/src/pages/signup/index.tsx b/src/pages/signup/index.tsx new file mode 100644 index 00000000..fc4ba519 --- /dev/null +++ b/src/pages/signup/index.tsx @@ -0,0 +1,49 @@ +import { useAccount } from "wagmi"; +import Link from "next/link"; +import { format } from "date-fns"; + +import { Layout } from "~/layouts/DefaultLayout"; +import { config } from "~/config"; +import { ConnectButton } from "~/components/ConnectButton"; +import { JoinButton } from "~/components/JoinButton"; +import { Info } from "~/components/Info"; +import { EligibilityDialog } from "~/components/EligibilityDialog"; +import { useMaci } from "~/contexts/Maci"; +import { Button } from "~/components/ui/Button"; + +const SignupPage = (): JSX.Element => { + const { isConnected } = useAccount(); + const { isRegistered } = useMaci(); + + return ( + + + +
+

+ {config.eventName.toUpperCase()} +

+

+ {config.roundId.toUpperCase()} +

+

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

+ {isConnected && isRegistered && ( + + )} + {isConnected && !isRegistered && } + {!isConnected && } +
+ +
+
+
+ ); +} + +export default SignupPage; diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 506f65d2..bc08499b 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -1,22 +1,49 @@ -import { type Chain, getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit"; +import { + type Chain, + getDefaultConfig, + RainbowKitProvider, + type Theme, + lightTheme, +} from "@rainbow-me/rainbowkit"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ThemeProvider } from "next-themes"; import { useMemo, type PropsWithChildren } from "react"; import { http, WagmiProvider } from "wagmi"; + import { Toaster } from "~/components/Toaster"; import * as appConfig from "~/config"; import { BallotProvider } from "~/contexts/Ballot"; import { MaciProvider } from "~/contexts/Maci"; -export const Providers = ({ children }: PropsWithChildren): JSX.Element => { +const theme = lightTheme(); + +const customTheme: Theme = { + blurs: { + ...theme.blurs, + }, + colors: { + ...theme.colors, + }, + fonts: { + body: "Share Tech Mono", + }, + radii: { + ...theme.radii, + }, + shadows: { + ...theme.shadows, + }, +}; + +export function Providers({ children }: PropsWithChildren) { const { config, queryClient } = useMemo(() => createWagmiConfig(), []); return ( - + {children} diff --git a/src/styles/globals.css b/src/styles/globals.css index 80419286..e4a35015 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -10,3 +10,48 @@ -ms-overflow-style: none; scrollbar-width: 0; } + +@layer base { + @font-face { + font-family: "Share Tech Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/Share_Tech_Mono.woff2) format("woff2"); + } + + @font-face { + font-family: "DM Sans"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/DM_Sans.woff2) format("woff2"); + } + + html { + font-family: "Share Tech Mono", "DM Sans"; + } + + h1 { + font-size: 60px; + font-family: "Share Tech Mono"; + } + + h2 { + font-size: 40px; + } + + h3 { + font-family: "Share Tech Mono"; + font-size: 32px; + color: black; + text-transform: uppercase; + } + + h4 { + font-size: 16px; + font-weight: 800; + text-transform: uppercase; + color: #888888; + } +} diff --git a/src/utils/types.ts b/src/utils/types.ts index c145313f..2bf5bc62 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,8 +1,13 @@ export enum EAppState { LOADING = "LOADING", APPLICATION = "APPLICATION", - REVIEWING = "REVIEWING", VOTING = "VOTING", - RESULTS = "RESULTS", TALLYING = "TALLYING", + RESULTS = "RESULTS", +} + +export enum EInfoCardState { + PASSED = "PASSED", + ONGOING = "ONGOING", + UPCOMING = "UPCOMING", } diff --git a/tailwind.config.ts b/tailwind.config.ts index 2884f0af..a8dc0538 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -32,6 +32,37 @@ const customColors = { highlight: { 600: "#F3CF00", }, + gray: { + 50: "#F6F6F6", + 100: "#E7E7E7", + 200: "#D1D1D1", + 300: "#B0B0B0", + 400: "#888888", + 500: "#6D6D6D", + 600: "#5D5D5D", + 700: "#4F4F4F", + 800: "#454545", + 900: "#3D3D3D", + 950: "#0B0B0B", + }, + blue: { + 50: "#F0F7FE", + 100: "#DEECFB", + 200: "#C4E0F9", + 300: "#9BCCF5", + 400: "#6BB1EF", + 500: "#579BEA", + 600: "#3476DC", + 700: "#2B62CA", + 800: "#2950A4", + 900: "#264682", + 950: "#1B2B50", + }, + black: "#0B0B0B", + darkGray: "#5E5E5E", + lightGray: "#CDCDCD", + green: "#00FF00", + red: "#EF4444", }; export default { @@ -42,10 +73,14 @@ export default { colors: { ...colors, ...customColors, - gray: colors.stone, }, fontFamily: { - sans: ["var(--font-inter)", ...theme.fontFamily.sans], + sans: ["DM Sans", ...theme.fontFamily.sans], + mono: ["Share Tech Mono", ...theme.fontFamily.mono], + }, + width: { + "112": "28rem", + "128": "32rem", }, }, },