From a1c514642a5daccc893310991e131706a03d107f Mon Sep 17 00:00:00 2001 From: yu-zhen Date: Wed, 11 Dec 2024 03:14:46 +0800 Subject: [PATCH] feat: display the projects created by the user --- .../components/ApplicationForm.tsx | 11 +- .../components/ReviewApplicationDetails.tsx | 5 + .../applications/hooks/useApplicationById.ts | 4 + .../hooks/useCreateApplication.ts | 5 +- .../src/features/applications/types/index.ts | 1 + .../projects/components/ProjectItem.tsx | 100 ++++++++++-------- .../projects/components/ProjectsResults.tsx | 9 +- .../features/rounds/components/Projects.tsx | 52 ++++++--- packages/interface/src/hooks/useRegistry.ts | 2 +- .../[pollId]/applications/confirmation.tsx | 5 +- .../src/server/api/routers/applications.ts | 3 + packages/interface/src/utils/types.ts | 2 + 12 files changed, 125 insertions(+), 74 deletions(-) diff --git a/packages/interface/src/features/applications/components/ApplicationForm.tsx b/packages/interface/src/features/applications/components/ApplicationForm.tsx index 5d1924e7..3bec4fc4 100644 --- a/packages/interface/src/features/applications/components/ApplicationForm.tsx +++ b/packages/interface/src/features/applications/components/ApplicationForm.tsx @@ -8,6 +8,7 @@ import { ImageUpload } from "~/components/ImageUpload"; import { FieldArray, Form, FormControl, FormSection, Select, Textarea } from "~/components/ui/Form"; import { Input } from "~/components/ui/Input"; import { useIsCorrectNetwork } from "~/hooks/useIsCorrectNetwork"; +import { MY_APPS_KEY } from "~/utils/types"; import { useCreateApplication } from "../hooks/useCreateApplication"; import { ApplicationSchema, contributionTypes, fundingSourceTypes, type Application } from "../types"; @@ -22,7 +23,7 @@ interface IApplicationFormProps { } export const ApplicationForm = ({ pollId }: IApplicationFormProps): JSX.Element => { - const clearDraft = useLocalStorage("application-draft")[2]; + const [myApps, setMyApps] = useLocalStorage(`${MY_APPS_KEY}-${pollId}`); const { isCorrectNetwork, correctNetwork } = useIsCorrectNetwork(); @@ -56,7 +57,13 @@ export const ApplicationForm = ({ pollId }: IApplicationFormProps): JSX.Element const create = useCreateApplication({ onSuccess: (id: bigint) => { - clearDraft(); + if (myApps) { + myApps.push(id.toString()); + setMyApps(myApps); + } else { + setMyApps([id.toString()]); + } + router.push(`/rounds/${pollId}/applications/confirmation?id=${id.toString()}`); }, onError: (err: { message: string }) => diff --git a/packages/interface/src/features/applications/components/ReviewApplicationDetails.tsx b/packages/interface/src/features/applications/components/ReviewApplicationDetails.tsx index d7977eaa..877e6f14 100644 --- a/packages/interface/src/features/applications/components/ReviewApplicationDetails.tsx +++ b/packages/interface/src/features/applications/components/ReviewApplicationDetails.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import { useMemo, type ReactNode } from "react"; import { useFormContext } from "react-hook-form"; +import { useAccount } from "wagmi"; import { Heading } from "~/components/ui/Heading"; import { Tag } from "~/components/ui/Tag"; @@ -41,6 +42,8 @@ export const ReviewApplicationDetails = (): JSX.Element => { const application = useMemo(() => form.getValues(), [form]); + const { address } = useAccount(); + return (
@@ -54,6 +57,8 @@ export const ReviewApplicationDetails = (): JSX.Element => { + +
diff --git a/packages/interface/src/features/applications/hooks/useApplicationById.ts b/packages/interface/src/features/applications/hooks/useApplicationById.ts index 2cf017c6..504e7b8e 100644 --- a/packages/interface/src/features/applications/hooks/useApplicationById.ts +++ b/packages/interface/src/features/applications/hooks/useApplicationById.ts @@ -6,3 +6,7 @@ import type { IRequest } from "~/utils/types"; export function useApplicationById(registryAddress: string, id: string): UseTRPCQueryResult { return api.applications.getById.useQuery({ registryAddress, id }); } + +export function useApplicationsByIds(registryAddress: string, ids: string[]): UseTRPCQueryResult { + return api.applications.getByIds.useQuery({ registryAddress, ids }); +} diff --git a/packages/interface/src/features/applications/hooks/useCreateApplication.ts b/packages/interface/src/features/applications/hooks/useCreateApplication.ts index f9e51e67..df2e6c61 100644 --- a/packages/interface/src/features/applications/hooks/useCreateApplication.ts +++ b/packages/interface/src/features/applications/hooks/useCreateApplication.ts @@ -34,7 +34,7 @@ export function useCreateApplication(options: { }): TUseCreateApplicationReturn { const upload = useUploadMetadata(); - const { chain } = useAccount(); + const { chain, address } = useAccount(); const { getRoundByPollId } = useRound(); const roundData = getRoundByPollId(options.pollId); @@ -44,7 +44,7 @@ export function useCreateApplication(options: { const mutation = useMutation({ mutationFn: async (values: Application) => { - if (!signer || !chain) { + if (!signer || !chain || !address) { throw new Error("Please connect your wallet first"); } @@ -71,6 +71,7 @@ export function useCreateApplication(options: { profileImageUrl: profileImageUrl.url, bannerImageUrl: bannerImageUrl.url, submittedAt: Date.now().valueOf(), + creator: address, }; const uploadRes = await upload.mutateAsync(metadataValues); diff --git a/packages/interface/src/features/applications/types/index.ts b/packages/interface/src/features/applications/types/index.ts index 683369be..138153c6 100644 --- a/packages/interface/src/features/applications/types/index.ts +++ b/packages/interface/src/features/applications/types/index.ts @@ -25,6 +25,7 @@ export const fundingSourceTypes = { export const ApplicationSchema = z.object({ name: z.string().min(3), bio: z.string().min(3), + creator: z.string().optional(), profileImageUrl: z.string().optional(), bannerImageUrl: z.string().optional(), submittedAt: z.number().optional(), diff --git a/packages/interface/src/features/projects/components/ProjectItem.tsx b/packages/interface/src/features/projects/components/ProjectItem.tsx index db4142ca..ef7b9088 100644 --- a/packages/interface/src/features/projects/components/ProjectItem.tsx +++ b/packages/interface/src/features/projects/components/ProjectItem.tsx @@ -1,4 +1,6 @@ import Image from "next/image"; +import Link from "next/link"; +import { useAccount } from "wagmi"; import { Button } from "~/components/ui/Button"; import { Heading } from "~/components/ui/Heading"; @@ -22,6 +24,7 @@ export interface IProjectItemProps { recipient: IRecipient; isLoading: boolean; state?: EProjectState; + checkIsMine?: boolean; action?: (e: Event) => void; } @@ -30,63 +33,70 @@ export const ProjectItem = ({ recipient, isLoading, state = undefined, + checkIsMine = false, action = undefined, }: IProjectItemProps): JSX.Element => { const metadata = useProjectMetadata(recipient.metadataUrl); const roundState = useRoundState({ pollId }); + const { address } = useAccount(); return ( -
-
- +
+
+ + + +
- -
+
+ + {metadata.data?.name} + -
- - {metadata.data?.name} - +
+ + {metadata.data?.bio} + +
-
- - {metadata.data?.bio} + + -
- - - - - {!isLoading && state !== undefined && action && roundState === ERoundState.VOTING && ( -
- - {state === EProjectState.DEFAULT && ( - - )} - - {state === EProjectState.ADDED && ( - - )} - - {state === EProjectState.SUBMITTED && ( - - )} - -
- )} -
-
+ {!isLoading && state !== undefined && action && roundState === ERoundState.VOTING && ( +
+ + {state === EProjectState.DEFAULT && ( + + )} + + {state === EProjectState.ADDED && ( + + )} + + {state === EProjectState.SUBMITTED && ( + + )} + +
+ )} +
+ + ); }; diff --git a/packages/interface/src/features/projects/components/ProjectsResults.tsx b/packages/interface/src/features/projects/components/ProjectsResults.tsx index 5b112b4d..4ef3c166 100644 --- a/packages/interface/src/features/projects/components/ProjectsResults.tsx +++ b/packages/interface/src/features/projects/components/ProjectsResults.tsx @@ -1,5 +1,4 @@ import clsx from "clsx"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useCallback, useMemo } from "react"; import { type Hex, zeroAddress } from "viem"; @@ -43,11 +42,7 @@ export const ProjectsResults = ({ pollId }: IProjectsResultsProps): JSX.Element ( - +
{!results.isLoading && roundState === ERoundState.RESULTS ? ( ) : null} @@ -59,7 +54,7 @@ export const ProjectsResults = ({ pollId }: IProjectsResultsProps): JSX.Element recipient={item} state={EProjectState.SUBMITTED} /> - +
)} /> ); diff --git a/packages/interface/src/features/rounds/components/Projects.tsx b/packages/interface/src/features/rounds/components/Projects.tsx index 606e902f..b9e98348 100644 --- a/packages/interface/src/features/rounds/components/Projects.tsx +++ b/packages/interface/src/features/rounds/components/Projects.tsx @@ -2,6 +2,7 @@ import clsx from "clsx"; import Link from "next/link"; import { useCallback, useMemo } from "react"; import { FiAlertCircle } from "react-icons/fi"; +import { useLocalStorage } from "react-use"; import { Hex, zeroAddress } from "viem"; import { InfiniteLoading } from "~/components/InfiniteLoading"; @@ -12,9 +13,10 @@ import { Heading } from "~/components/ui/Heading"; import { useBallot } from "~/contexts/Ballot"; import { useMaci } from "~/contexts/Maci"; import { useRound } from "~/contexts/Round"; +import { useApplicationsByIds } from "~/features/applications/hooks/useApplicationById"; import { useResults } from "~/hooks/useResults"; import { useRoundState } from "~/utils/state"; -import { ERoundState } from "~/utils/types"; +import { ERoundState, MY_APPS_KEY, type IRecipient } from "~/utils/types"; import { ProjectItem, ProjectItemAwarded } from "../../projects/components/ProjectItem"; import { useSearchProjects } from "../../projects/hooks/useProjects"; @@ -43,6 +45,12 @@ export const Projects = ({ pollId = "" }: IProjectsProps): JSX.Element => { const ballot = useMemo(() => getBallot(pollId), [pollId, getBallot]); + const [myApps] = useLocalStorage(`${MY_APPS_KEY}-${pollId}`); + + const applications = useApplicationsByIds(round?.registryAddress ?? zeroAddress, myApps ?? []); + + const myApplications = useMemo(() => applications.data, [applications]); + const handleAction = useCallback( (projectIndex: number, projectId: string) => (e: Event) => { e.preventDefault(); @@ -119,23 +127,41 @@ export const Projects = ({ pollId = "" }: IProjectsProps): JSX.Element => {
{roundState === ERoundState.APPLICATION && ( -
- - - +
+
+ My Projects + + + + +
+ +
+ {myApplications && + myApplications.length > 0 && + myApplications.map((project) => ( + + ))} + + {(!myApplications || myApplications.length === 0) && ( +

Create your application by clicking the button

+ )} +
)} ( - +
{!results.isLoading && roundState === ERoundState.RESULTS ? ( ) : null} @@ -147,7 +173,7 @@ export const Projects = ({ pollId = "" }: IProjectsProps): JSX.Element => { recipient={item} state={defineState(Number.parseInt(item.index, 10))} /> - +
)} />
diff --git a/packages/interface/src/hooks/useRegistry.ts b/packages/interface/src/hooks/useRegistry.ts index 7d2efdbb..b9151e08 100644 --- a/packages/interface/src/hooks/useRegistry.ts +++ b/packages/interface/src/hooks/useRegistry.ts @@ -22,7 +22,7 @@ interface SubmitApplicationArgs { */ registryAddress: Hex; /** - * The recipient of the attestation + * The recipient of the application */ recipient: Hex; } diff --git a/packages/interface/src/pages/rounds/[pollId]/applications/confirmation.tsx b/packages/interface/src/pages/rounds/[pollId]/applications/confirmation.tsx index 49b932a7..49a15e9b 100644 --- a/packages/interface/src/pages/rounds/[pollId]/applications/confirmation.tsx +++ b/packages/interface/src/pages/rounds/[pollId]/applications/confirmation.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useMemo } from "react"; import { FiAlertCircle } from "react-icons/fi"; @@ -83,9 +82,7 @@ const ConfirmProjectPage = ({ pollId }: { pollId: string }): JSX.Element => {
)} - - - + diff --git a/packages/interface/src/server/api/routers/applications.ts b/packages/interface/src/server/api/routers/applications.ts index dfe3d57c..022f1c7c 100644 --- a/packages/interface/src/server/api/routers/applications.ts +++ b/packages/interface/src/server/api/routers/applications.ts @@ -18,6 +18,9 @@ export const applicationsRouter = createTRPCRouter({ getById: publicProcedure .input(z.object({ registryAddress: z.string(), id: z.string() })) .query(async ({ input }) => fetchApplicationById(input.registryAddress, input.id)), + getByIds: publicProcedure + .input(z.object({ registryAddress: z.string(), ids: z.array(z.string()) })) + .query(async ({ input }) => Promise.all(input.ids.map((id) => fetchApplicationById(input.registryAddress, id)))), getByProjectId: publicProcedure .input(z.object({ registryAddress: z.string(), projectId: z.string() })) .query(async ({ input }) => fetchApplicationByProjectId(input.projectId, input.registryAddress)), diff --git a/packages/interface/src/utils/types.ts b/packages/interface/src/utils/types.ts index ab9a9a36..8df0bb6a 100644 --- a/packages/interface/src/utils/types.ts +++ b/packages/interface/src/utils/types.ts @@ -1,6 +1,8 @@ import type { IGetPollData } from "maci-cli/sdk"; import type { Address, Hex } from "viem"; +export const MY_APPS_KEY = "my-apps"; + export enum ERoundState { LOADING = "LOADING", APPLICATION = "APPLICATION",