Skip to content

Commit

Permalink
feat: add v2 frontend UI design
Browse files Browse the repository at this point in the history
  • Loading branch information
kittybest committed Sep 4, 2024
1 parent 96ace2d commit 4723c97
Show file tree
Hide file tree
Showing 68 changed files with 771 additions and 623 deletions.
15 changes: 3 additions & 12 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
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
37 changes: 18 additions & 19 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, initialVoiceCredits, votingEndsAt } = 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,12 +65,11 @@ 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 {initialVoiceCredits} voice credits to vote with.</p>
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

0 comments on commit 4723c97

Please sign in to comment.