Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add v2 frontend UI design #261

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions packages/interface/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,18 @@ NEXT_PUBLIC_WALLETCONNECT_ID=
# -----------------

# Event title for the round, just for display
NEXT_PUBLIC_EVENT_NAME="ETH GLOBAL"
NEXT_PUBLIC_EVENT_NAME="Add your event name"

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

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

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

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

# address that will approve applications and voters
# (leaving empty means anyone can do this)
NEXT_PUBLIC_ADMIN_ADDRESS=

# -----------------
Expand Down Expand Up @@ -80,9 +71,6 @@ NEXT_PUBLIC_MACI_START_BLOCK=

NEXT_PUBLIC_MACI_SUBGRAPH_URL=

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

# Whether the poll is in qv or non qv mode
NEXT_PUBLIC_POLL_MODE="non-qv"

Expand Down
8 changes: 6 additions & 2 deletions packages/interface/src/components/AddedProjects.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useBallot } from "~/contexts/Ballot";
import { useProjectCount } from "~/features/projects/hooks/useProjects";

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

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

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

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

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

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

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

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

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

<AddedProjects />
<AddedProjects roundId={roundId} />

<VotingUsage />
</div>
Expand Down
39 changes: 19 additions & 20 deletions packages/interface/src/components/EligibilityDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import { toast } from "sonner";
import { useAccount, useDisconnect } from "wagmi";

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

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

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

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

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

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

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

Expand All @@ -38,15 +42,11 @@ export const EligibilityDialog = (): JSX.Element | null => {
disconnect();
}, [disconnect]);

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

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

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

if (appState === EAppState.VOTING && isRegistered) {
/// TODO: edit X to real date
if (roundState === ERoundState.VOTING && isRegistered) {
return (
<Dialog
button="secondary"
buttonAction={handleGoToProjects}
buttonName="See all projects"
description={
<div className="flex flex-col gap-4">
<p>You have X voice credits to vote with.</p>
<p>You have X voice credits to vote for each round.</p>

<p>
Get started by adding projects to your ballot, then adding the amount of votes you want to allocate to
Expand All @@ -85,21 +84,21 @@ export const EligibilityDialog = (): JSX.Element | null => {
}
isOpen={openDialog}
size="sm"
title="You're all set to vote"
title="You're all set to vote for rounds!"
onOpenChange={handleCloseDialog}
/>
);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

{appState === EAppState.VOTING && pageName === "/ballot" && ballot.votes.length > 0 && (
{roundState === ERoundState.VOTING && link.href.includes("/ballot") && ballot.votes.length > 0 && (
<div className="ml-2 h-5 w-5 rounded-full border-2 border-blue-400 bg-blue-50 text-center text-sm leading-4 text-blue-400">
{ballot.votes.length}
</div>
Expand Down
73 changes: 35 additions & 38 deletions packages/interface/src/components/Info.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { tv } from "tailwind-variants";

import { createComponent } from "~/components/ui";
import { config } from "~/config";
import { useMaci } from "~/contexts/Maci";
import { useAppState } from "~/utils/state";
import { EInfoCardState, EAppState } from "~/utils/types";
import { useRound } from "~/contexts/Round";
import { useRoundState } from "~/utils/state";
import { EInfoCardState, ERoundState } from "~/utils/types";

import { InfoCard } from "./InfoCard";
import { RoundInfo } from "./RoundInfo";
Expand All @@ -23,39 +22,41 @@ const InfoContainer = createComponent(
}),
);

interface InfoProps {
interface IInfoProps {
size: string;
roundId: string;
showVotingInfo?: boolean;
}

export const Info = ({ size, showVotingInfo = false }: InfoProps): JSX.Element => {
const { votingEndsAt } = useMaci();
const appState = useAppState();
export const Info = ({ size, roundId, showVotingInfo = false }: IInfoProps): JSX.Element => {
const roundState = useRoundState(roundId);
const { getRound } = useRound();
const round = getRound(roundId);

const steps = [
{
label: "application",
state: EAppState.APPLICATION,
start: config.startsAt,
end: config.registrationEndsAt,
state: ERoundState.APPLICATION,
start: round?.startsAt ? new Date(round.startsAt) : new Date(),
end: round?.registrationEndsAt ? new Date(round.registrationEndsAt) : new Date(),
},
{
label: "voting",
state: EAppState.VOTING,
start: config.registrationEndsAt,
end: votingEndsAt,
state: ERoundState.VOTING,
start: round?.registrationEndsAt ? new Date(round.registrationEndsAt) : new Date(),
end: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(),
},
{
label: "tallying",
state: EAppState.TALLYING,
start: votingEndsAt,
end: config.resultsAt,
state: ERoundState.TALLYING,
start: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(),
end: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(),
},
{
label: "results",
state: EAppState.RESULTS,
start: config.resultsAt,
end: config.resultsAt,
state: ERoundState.RESULTS,
start: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(),
end: round?.votingEndsAt ? new Date(round.votingEndsAt) : new Date(),
},
];

Expand All @@ -64,34 +65,30 @@ export const Info = ({ size, showVotingInfo = false }: InfoProps): JSX.Element =
<InfoContainer size={size}>
{showVotingInfo && (
<div className="w-full">
<RoundInfo />
<RoundInfo roundId={roundId} />

{appState === EAppState.VOTING && <VotingInfo />}
{roundState === ERoundState.VOTING && <VotingInfo />}
</div>
)}

{steps.map(
(step) =>
step.start &&
step.end && (
<InfoCard
key={step.label}
end={step.end}
start={step.start}
state={defineState({ state: step.state, appState })}
title={step.label}
/>
),
)}
{steps.map((step) => (
<InfoCard
key={step.label}
end={step.end}
start={step.start}
state={defineState({ state: step.state, roundState })}
title={step.label}
/>
))}
</InfoContainer>
</div>
);
};

function defineState({ state, appState }: { state: EAppState; appState: EAppState }): EInfoCardState {
const statesOrder = [EAppState.APPLICATION, EAppState.VOTING, EAppState.TALLYING, EAppState.RESULTS];
function defineState({ state, roundState }: { state: ERoundState; roundState: ERoundState }): EInfoCardState {
const statesOrder = [ERoundState.APPLICATION, ERoundState.VOTING, ERoundState.TALLYING, ERoundState.RESULTS];
const currentStateOrder = statesOrder.indexOf(state);
const appStateOrder = statesOrder.indexOf(appState);
const appStateOrder = statesOrder.indexOf(roundState);

if (currentStateOrder < appStateOrder) {
return EInfoCardState.PASSED;
Expand Down
Loading
Loading