Skip to content

Commit

Permalink
feat: add organizer dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
kittybest committed Aug 29, 2024
1 parent 23eefef commit 3f25d13
Show file tree
Hide file tree
Showing 25 changed files with 841 additions and 62 deletions.
14 changes: 7 additions & 7 deletions packages/interface/src/components/ImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { ImageIcon } from "lucide-react";
import { type ComponentProps, useRef } from "react";
import { type ComponentProps, useRef, useCallback } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { toast } from "sonner";

Expand Down Expand Up @@ -33,17 +33,18 @@ export const ImageUpload = ({
},
});

const onClick = useCallback(() => {
ref.current?.click();
}, []);

return (
<Controller
control={control}
name={name}
render={({ field: { value, onChange, ...field } }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
className={clsx("relative cursor-pointer overflow-hidden", className)}
onClick={() => ref.current?.click()}
>
<IconButton className="absolute bottom-1 right-1" icon={ImageIcon} />
<div className={clsx("relative cursor-pointer overflow-hidden", className)} onClick={onClick}>
<IconButton className="absolute bottom-1 right-1" icon={ImageIcon} type="button" />

<div
className={clsx("h-full rounded-xl bg-gray-200 bg-cover bg-center bg-no-repeat")}
Expand All @@ -58,7 +59,6 @@ export const ImageUpload = ({
accept="image/png, image/jpeg"
className="hidden"
type="file"
// value={value?.[name]}
onChange={(event) => {
const [file] = event.target.files ?? [];
if (file) {
Expand Down
2 changes: 1 addition & 1 deletion packages/interface/src/components/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const Info = ({ size, roundId, showVotingInfo = false }: IInfoProps): JSX
<InfoContainer size={size}>
{showVotingInfo && (
<div className="w-full">
<RoundInfo roundId={roundId} />
<RoundInfo roundId={roundId} roundLogo={round ? round.roundLogo : undefined} />

{roundState === ERoundState.VOTING && <VotingInfo />}
</div>
Expand Down
8 changes: 3 additions & 5 deletions packages/interface/src/components/RoundInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import Image from "next/image";

import { Heading } from "~/components/ui/Heading";
import { config } from "~/config";

interface IRoundInfoProps {
roundId: string;
roundLogo?: string;
}

export const RoundInfo = ({ roundId }: IRoundInfoProps): JSX.Element => (
export const RoundInfo = ({ roundId, roundLogo = undefined }: IRoundInfoProps): JSX.Element => (
<div className="w-full border-b border-gray-200 pb-2">
<h4>Round</h4>

<div className="flex items-center gap-2">
{config.roundLogo && <Image alt="round logo" height="20" src={`/${config.roundLogo}`} width="20" />}
{roundLogo && <img alt="round logo" height="20" src={roundLogo} width="20" />}

<Heading as="h3" size="3xl">
{roundId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ export enum EStepState {
interface IStepCategoryProps {
title: string;
progress: EStepState;
isLast?: boolean;
}

interface IApplicationStepsProps {
interface IStepsProps {
step: number;
stepNames: string[];
}

const StepCategory = ({ title, progress }: IStepCategoryProps): JSX.Element => (
const Interline = (): JSX.Element => <div className="h-[1px] w-9 bg-gray-300" />;

const StepCategory = ({ title, progress, isLast = false }: IStepCategoryProps): JSX.Element => (
<div className="flex items-center gap-3">
{progress === EStepState.ACTIVE && (
<Image alt="circle-check-blue" height="22" src="/circle-check-blue.svg" width="22" />
Expand All @@ -29,21 +33,15 @@ const StepCategory = ({ title, progress }: IStepCategoryProps): JSX.Element => (
{progress === EStepState.DEFAULT && <div className="h-4 w-4 rounded-full border-2 border-gray-300" />}

<p className={clsx("text-md", progress === EStepState.ACTIVE ? "text-blue-400" : "text-gray-300")}>{title}</p>

{!isLast && <Interline />}
</div>
);

const Interline = (): JSX.Element => <div className="h-[1px] w-9 bg-gray-300" />;

export const ApplicationSteps = ({ step }: IApplicationStepsProps): JSX.Element => (
export const Steps = ({ step, stepNames }: IStepsProps): JSX.Element => (
<div className="mb-4 flex items-center gap-4">
<StepCategory progress={step} title="Project Profile" />

<Interline />

<StepCategory progress={step - 1} title="Contribution & Impact" />

<Interline />

<StepCategory progress={step - 2} title="Review & Submit" />
{stepNames.map((name, i) => (
<StepCategory key={name} isLast={i === stepNames.length - 1} progress={step - i} title={name} />
))}
</div>
);
7 changes: 7 additions & 0 deletions packages/interface/src/components/ui/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ export const ErrorMessage = createComponent("div", tv({ base: "pt-1 text-xs text

export const Textarea = createComponent("textarea", tv({ base: [...inputBase, "w-full"] }));

// eslint-disable-next-line react/display-name
export const DateInput = forwardRef(({ ...props }: ComponentPropsWithRef<typeof Input>, ref) => (
<InputWrapper>
<Input ref={ref} {...props} type="date" />
</InputWrapper>
));

export const SearchInput = forwardRef(({ ...props }: ComponentPropsWithRef<typeof Input>, ref) => (
<InputWrapper>
<InputIcon>
Expand Down
46 changes: 33 additions & 13 deletions packages/interface/src/contexts/Round.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,53 @@
import React, { createContext, useContext, useMemo, useCallback } from "react";
import React, { createContext, useContext, useMemo, useCallback, useState, useEffect } from "react";

import type { RoundContextType, RoundProviderProps } from "./types";
import type { Round } from "~/features/rounds/types";

export const RoundContext = createContext<RoundContextType | undefined>(undefined);

export const RoundProvider: React.FC<RoundProviderProps> = ({ children }: RoundProviderProps) => {
const rounds = [
{
roundId: "open-rpgf-1",
description: "This is the description of this round, please add your own description.",
startsAt: 1723477832000,
registrationEndsAt: 1723487832000,
votingEndsAt: 1724009826000,
tallyURL: "https://upblxu2duoxmkobt.public.blob.vercel-storage.com/tally.json",
},
];
const [rounds, setRounds] = useState<Round[] | undefined>(undefined);

const getRound = useCallback(
(roundId: string): Round | undefined => rounds.find((round) => round.roundId === roundId),
(roundId: string): Round | undefined => (rounds ? rounds.find((round) => round.roundId === roundId) : undefined),
[rounds],
);

const addRound = useCallback(
(round: Round): void => {
if (!rounds) {
setRounds([round]);
} else {
setRounds([...rounds, round]);
}
},
[rounds, setRounds],
);

useEffect(() => {
const storageData = localStorage.getItem("rounds");

if (storageData) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const storedRounds = JSON.parse(storageData);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setRounds(storedRounds);
}
}, []);

useEffect(() => {
if (rounds) {
localStorage.setItem("rounds", JSON.stringify(rounds));
}
}, [rounds]);

const value = useMemo(
() => ({
rounds,
getRound,
addRound,
}),
[rounds, getRound],
[rounds, getRound, addRound],
);

return <RoundContext.Provider value={value as RoundContextType}>{children}</RoundContext.Provider>;
Expand Down
3 changes: 2 additions & 1 deletion packages/interface/src/contexts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export interface BallotProviderProps {
}

export interface RoundContextType {
rounds: Round[];
rounds: Round[] | undefined;
getRound: (roundId: string) => Round | undefined;
addRound: (round: Round) => void;
}

export interface RoundProviderProps {
Expand Down
2 changes: 1 addition & 1 deletion packages/interface/src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ module.exports = createEnv({
NEXT_PUBLIC_WALLETCONNECT_ID: z.string().optional(),
NEXT_PUBLIC_ALCHEMY_ID: z.string().optional(),

NEXT_PUBLIC_MACI_ADDRESS: z.string().startsWith("0x"),
NEXT_PUBLIC_MACI_ADDRESS: z.string().startsWith("0x").optional(),
NEXT_PUBLIC_MACI_START_BLOCK: z.string().optional(),
NEXT_PUBLIC_MACI_SUBGRAPH_URL: z.string().url().optional(),

Expand Down
120 changes: 120 additions & 0 deletions packages/interface/src/features/admin/components/DeployContracts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useRouter } from "next/router";
import { useState, useCallback } from "react";
import { toast } from "sonner";
import { useAccount } from "wagmi";

import { Steps } from "~/components/Steps";
import { Form, FormSection, FormControl, Select } from "~/components/ui/Form";
import { Heading } from "~/components/ui/Heading";
import { DeploymentSchema, chainTypes, gatingStrategyTypes } from "~/features/rounds/types";
import { useIsCorrectNetwork } from "~/hooks/useIsCorrectNetwork";

import { useDeployContracts } from "../hooks/useDeployContracts";

import { EDeployContractsStep, DeployContractsButtons } from "./DeployContractsButtons";
import { VoiceCreditProxySelect } from "./VoiceCreditProxySelect";

export const DeployContracts = (): JSX.Element => {
const router = useRouter();
const [step, setStep] = useState<EDeployContractsStep>(EDeployContractsStep.CONFIGURE);

const { isCorrectNetwork, correctNetwork } = useIsCorrectNetwork();

const { address } = useAccount();

const handleNextStep = useCallback(() => {
if (step === EDeployContractsStep.CONFIGURE) {
setStep(EDeployContractsStep.REVIEW);
}
}, [step, setStep]);

const handleBackStep = useCallback(() => {
if (step === EDeployContractsStep.REVIEW) {
setStep(EDeployContractsStep.CONFIGURE);
}
}, [step, setStep]);

const create = useDeployContracts({
onSuccess: () => {
router.push(`/`);
},
onError: (err: Error) =>
toast.error("Contracts deploy error", {
description: err.message,
}),
});

const { error: createError } = create;

return (
<div>
<Heading size="4xl">Deploy Core Contracts</Heading>

<p className="text-gray-400">These initial MACI core contracts configuration will apply to all future rounds.</p>

<div className="dark:border-lighterBlack rounded-lg border border-gray-200 p-4">
<Steps step={step} stepNames={["Configure Contracts", "Review & Deploy"]} />

<Form
schema={DeploymentSchema}
onSubmit={(round) => {
create.mutate(round);
}}
>
<FormSection
className={step === EDeployContractsStep.CONFIGURE ? "block" : "hidden"}
description="Please select from the available options:"
title="Configure Contracts"
>
<FormControl required hint="Choose a chain to deploy your contracts" label="Chain to deploy" name="chain">
<Select>
{Object.entries(chainTypes).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</Select>
</FormControl>

<FormControl required hint="Choose a gating strategy" label="Gating strategy" name="gatingStrategy">
<Select>
{Object.entries(gatingStrategyTypes).map(([value, label]) => (
<option key={value} value={label}>
{label}
</option>
))}
</Select>
</FormControl>

<VoiceCreditProxySelect />
</FormSection>

{step === EDeployContractsStep.REVIEW && <div>Review your setup</div>}

{step === EDeployContractsStep.REVIEW && (
<div className="mb-2 w-full text-right text-sm italic text-blue-400">
{!address && <p>You must connect wallet to create an application</p>}

{!isCorrectNetwork && <p className="gap-2">You must be connected to {correctNetwork.name}</p>}

{createError && (
<p>
Make sure you&apos;re not connected to a VPN since this can cause problems with the RPC and your
wallet.
</p>
)}
</div>
)}

<DeployContractsButtons
isPending={create.isPending}
isUploading={create.isPending}
step={step}
onBackStep={handleBackStep}
onNextStep={handleNextStep}
/>
</Form>
</div>
</div>
);
};
Loading

0 comments on commit 3f25d13

Please sign in to comment.