Skip to content

Commit ac80c4c

Browse files
committed
feat: edit application
1 parent c09a157 commit ac80c4c

20 files changed

+650
-50
lines changed

packages/interface/src/components/ImageUpload.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import { toast } from "sonner";
1010
import { IconButton } from "~/components/ui/Button";
1111

1212
export interface IImageUploadProps extends ComponentProps<"img"> {
13+
defaultValue?: string;
1314
name?: string;
1415
maxSize?: number;
1516
}
1617

1718
export const ImageUpload = forwardRef(
1819
(
1920
{
21+
defaultValue = "none",
2022
name = "",
2123
maxSize = 1024 * 1024, // 1 MB
2224
className,
@@ -93,7 +95,7 @@ export const ImageUpload = forwardRef(
9395
<div
9496
className={clsx("h-full rounded-xl bg-gray-200 bg-cover bg-center bg-no-repeat")}
9597
style={{
96-
backgroundImage: select.data ? `url("${value}")` : "none",
98+
backgroundImage: select.data ? `url("${value}")` : `url(${defaultValue})`,
9799
}}
98100
/>
99101

packages/interface/src/components/ui/Badge.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const Badge = createComponent(
1111
default: "bg-gray-100",
1212
success: "bg-[#BBF7D0] text-[#14532D] dark:bg-[#031E0C] dark:text-[#4ADE80]",
1313
pending: "bg-[#FFEDD5] text-[#4E1D0D] dark:bg-[#4E1D0D] dark:text-[#F1B37A]",
14+
reject: "bg-[#FEE2E2] text-[#5E1414] dark:bg-[#3D0B0B] dark:text-[#F87171]",
15+
notice: "bg-[#6BB1EF] text-white dark:bg-[#0D172D] dark:text-[#2B62CA]",
1416
},
1517
size: {
1618
md: "px-3 py-1.5",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { useRouter } from "next/router";
2+
import { useState, useCallback } from "react";
3+
import { toast } from "sonner";
4+
import { useAccount } from "wagmi";
5+
6+
import { ImageUpload } from "~/components/ImageUpload";
7+
import { FieldArray, Form, FormControl, FormSection, Select, Textarea } from "~/components/ui/Form";
8+
import { Input } from "~/components/ui/Input";
9+
import { useIsCorrectNetwork } from "~/hooks/useIsCorrectNetwork";
10+
11+
import type { IRecipient } from "~/utils/types";
12+
13+
import { useEditProposal } from "../hooks/useEditProposal";
14+
import { MetadataSchema, contributionTypes, fundingSourceTypes, type Metadata } from "../types";
15+
16+
import { ImpactTags } from "./ImpactTags";
17+
import { MetadataButtons, EMetadataStep } from "./MetadataButtons";
18+
import { MetadataSteps } from "./MetadataSteps";
19+
import { ReviewProposalDetails } from "./ReviewProposalDetails";
20+
21+
interface IEditMetadataFormProps {
22+
pollId: string;
23+
project?: IRecipient;
24+
}
25+
26+
export const EditMetadataForm = ({ pollId, project = undefined }: IEditMetadataFormProps): JSX.Element => {
27+
const { isCorrectNetwork, correctNetwork } = useIsCorrectNetwork();
28+
29+
const { address } = useAccount();
30+
31+
const router = useRouter();
32+
33+
/**
34+
* There are 3 steps for editing a project proposal.
35+
* The first step is to edit the project introduction (profile);
36+
* the second step is to edit the contributions, impacts, and funding sources (advanced);
37+
* the last step is to review the input values, allow editing by going back to previous steps (review).
38+
*/
39+
const [step, setStep] = useState<EMetadataStep>(EMetadataStep.PROFILE);
40+
41+
const handleNextStep = useCallback(() => {
42+
if (step === EMetadataStep.PROFILE) {
43+
setStep(EMetadataStep.ADVANCED);
44+
} else if (step === EMetadataStep.ADVANCED) {
45+
setStep(EMetadataStep.REVIEW);
46+
}
47+
}, [step, setStep]);
48+
49+
const handleBackStep = useCallback(() => {
50+
if (step === EMetadataStep.REVIEW) {
51+
setStep(EMetadataStep.ADVANCED);
52+
} else if (step === EMetadataStep.ADVANCED) {
53+
setStep(EMetadataStep.PROFILE);
54+
}
55+
}, [step, setStep]);
56+
57+
const create = useEditProposal({
58+
onSuccess: (id: bigint) => {
59+
router.push(`/rounds/${pollId}/proposals/confirmation?index=${id.toString()}`);
60+
},
61+
onError: (err: { message: string }) =>
62+
toast.error("Edit proposal error", {
63+
description: err.message,
64+
}),
65+
pollId,
66+
recipientId: project?.id,
67+
recipientIndex: project?.index,
68+
});
69+
70+
const handleSubmit = useCallback(
71+
(metadata: Metadata) => {
72+
create.mutate(metadata);
73+
},
74+
[create],
75+
);
76+
77+
const { error: createError } = create;
78+
79+
return (
80+
<div className="dark:border-lighterBlack rounded-lg border border-gray-200 p-4">
81+
<MetadataSteps step={step} />
82+
83+
<Form
84+
defaultValues={{
85+
payoutAddress: project?.metadata?.payoutAddress,
86+
name: project?.metadata?.name,
87+
bio: project?.metadata?.bio,
88+
websiteUrl: project?.metadata?.websiteUrl,
89+
twitter: project?.metadata?.twitter,
90+
github: project?.metadata?.github,
91+
contributionDescription: project?.metadata?.contributionDescription,
92+
impactDescription: project?.metadata?.impactDescription,
93+
impactCategory: project?.metadata?.impactCategory,
94+
profileImageUrl: project?.metadata?.profileImageUrl,
95+
bannerImageUrl: project?.metadata?.bannerImageUrl,
96+
contributionLinks: project?.metadata?.contributionLinks,
97+
fundingSources: project?.metadata?.fundingSources,
98+
}}
99+
schema={MetadataSchema}
100+
onSubmit={handleSubmit}
101+
>
102+
<FormSection
103+
className={step === EMetadataStep.PROFILE ? "block" : "hidden"}
104+
description="Please provide information about your project."
105+
title="Project Profile"
106+
>
107+
<FormControl required hint="This is the name of your project" label="Project name" name="name">
108+
<Input placeholder="Type your project name" />
109+
</FormControl>
110+
111+
<FormControl required label="Description" name="bio">
112+
<Textarea placeholder="Type project description" rows={4} />
113+
</FormControl>
114+
115+
<small className="-mt-4">*Markdown Supported</small>
116+
117+
<div className="gap-4 md:flex">
118+
<FormControl required className="flex-1" label="Website" name="websiteUrl">
119+
<Input placeholder="https://" />
120+
</FormControl>
121+
122+
<FormControl required className="flex-1" label="Payout address" name="payoutAddress">
123+
<Input placeholder="0x..." />
124+
</FormControl>
125+
</div>
126+
127+
<div className="gap-4 md:flex">
128+
<FormControl className="flex-1" label="X(Twitter)" name="twitter" required={false}>
129+
<Input placeholder="Type your twitter username" />
130+
</FormControl>
131+
132+
<FormControl
133+
className="flex-1"
134+
hint="Provide your github of this project"
135+
label="Github"
136+
name="github"
137+
required={false}
138+
>
139+
<Input placeholder="Type your github username" />
140+
</FormControl>
141+
</div>
142+
143+
<div className="mb-4 flex flex-col gap-4 sm:flex-row">
144+
<FormControl
145+
required
146+
hint="The size should be smaller than 1MB."
147+
label="Project avatar"
148+
name="profileImageUrl"
149+
>
150+
<ImageUpload
151+
className="h-48 w-48"
152+
defaultValue={project?.metadata?.profileImageUrl}
153+
name="profileImageUrl"
154+
/>
155+
</FormControl>
156+
157+
<FormControl
158+
required
159+
className="flex-1"
160+
hint="The size should be smaller than 1MB."
161+
label="Project background image"
162+
name="bannerImageUrl"
163+
>
164+
<ImageUpload className="h-48" defaultValue={project?.metadata?.bannerImageUrl} name="bannerImageUrl" />
165+
</FormControl>
166+
</div>
167+
</FormSection>
168+
169+
<FormSection
170+
className={step === EMetadataStep.ADVANCED ? "block" : "hidden"}
171+
description="Describe the contribution and impact of your project."
172+
title="Contribution & Impact"
173+
>
174+
<FormControl required label="Contribution description" name="contributionDescription">
175+
<Textarea placeholder="What have your project contributed to?" rows={4} />
176+
</FormControl>
177+
178+
<small className="-mt-4">*Markdown Supported</small>
179+
180+
<FormControl required label="Impact description" name="impactDescription">
181+
<Textarea placeholder="What impact has your project had?" rows={4} />
182+
</FormControl>
183+
184+
<small className="-mt-4">*Markdown Supported</small>
185+
186+
<ImpactTags />
187+
188+
<FieldArray
189+
description="Where can we find your contributions?"
190+
name="contributionLinks"
191+
renderField={(field, i) => (
192+
<div className="mb-4 flex flex-wrap gap-2">
193+
<FormControl required className="min-w-96" name={`contributionLinks.${i}.description`}>
194+
<Input placeholder="Type the description of your contribution" />
195+
</FormControl>
196+
197+
<FormControl required className="min-w-72" name={`contributionLinks.${i}.url`}>
198+
<Input placeholder="https://" />
199+
</FormControl>
200+
201+
<FormControl required name={`contributionLinks.${i}.type`}>
202+
<Select>
203+
{Object.entries(contributionTypes).map(([value, label]) => (
204+
<option key={value} value={value}>
205+
{label}
206+
</option>
207+
))}
208+
</Select>
209+
</FormControl>
210+
</div>
211+
)}
212+
title="Contribution links"
213+
/>
214+
215+
<FieldArray
216+
description="From what sources have you received funding?"
217+
name="fundingSources"
218+
renderField={(field, i) => (
219+
<div className="mb-4 flex flex-wrap gap-2">
220+
<FormControl required className="min-w-96" name={`fundingSources.${i}.description`}>
221+
<Input placeholder="Type the name of your funding source" />
222+
</FormControl>
223+
224+
<FormControl required valueAsNumber className="w-32" name={`fundingSources.${i}.amount`}>
225+
<Input placeholder="Amount" type="number" />
226+
</FormControl>
227+
228+
<FormControl required className="w-32" name={`fundingSources.${i}.currency`}>
229+
<Input placeholder="e.g. USD" />
230+
</FormControl>
231+
232+
<FormControl required name={`fundingSources.${i}.type`}>
233+
<Select>
234+
{Object.entries(fundingSourceTypes).map(([value, label]) => (
235+
<option key={value} value={value}>
236+
{label}
237+
</option>
238+
))}
239+
</Select>
240+
</FormControl>
241+
</div>
242+
)}
243+
title="Funding sources"
244+
/>
245+
</FormSection>
246+
247+
{step === EMetadataStep.REVIEW && <ReviewProposalDetails />}
248+
249+
{step === EMetadataStep.REVIEW && (
250+
<div className="mb-2 w-full text-right text-sm italic text-blue-400">
251+
{!address && <p>You must connect wallet to create a proposal of a project</p>}
252+
253+
{!isCorrectNetwork && <p className="gap-2">You must be connected to {correctNetwork.name}</p>}
254+
255+
{createError && (
256+
<p>
257+
Make sure you&apos;re not connected to a VPN since this can cause problems with the RPC and your wallet.
258+
</p>
259+
)}
260+
</div>
261+
)}
262+
263+
<MetadataButtons
264+
isPending={create.isPending}
265+
isUploading={create.isPending}
266+
step={step}
267+
onBackStep={handleBackStep}
268+
onNextStep={handleNextStep}
269+
/>
270+
</Form>
271+
</div>
272+
);
273+
};

packages/interface/src/features/proposals/components/MetadataForm.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const MetadataForm = ({ pollId }: IMetadataFormProps): JSX.Element => {
5353

5454
const create = useCreateProposal({
5555
onSuccess: (id: bigint) => {
56-
router.push(`/rounds/${pollId}/proposals/confirmation?id=${id.toString()}`);
56+
router.push(`/rounds/${pollId}/proposals/confirmation?index=${id.toString()}`);
5757
},
5858
onError: (err: { message: string }) =>
5959
toast.error("Proposal create error", {
@@ -130,7 +130,7 @@ export const MetadataForm = ({ pollId }: IMetadataFormProps): JSX.Element => {
130130
label="Project avatar"
131131
name="profileImageUrl"
132132
>
133-
<ImageUpload className="h-48 w-48" />
133+
<ImageUpload className="h-48 w-48" name="profileImageUrl" />
134134
</FormControl>
135135

136136
<FormControl
@@ -140,7 +140,7 @@ export const MetadataForm = ({ pollId }: IMetadataFormProps): JSX.Element => {
140140
label="Project background image"
141141
name="bannerImageUrl"
142142
>
143-
<ImageUpload className="h-48" />
143+
<ImageUpload className="h-48" name="bannerImageUrl" />
144144
</FormControl>
145145
</div>
146146
</FormSection>

packages/interface/src/features/proposals/components/ProposalItem.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ import { ProjectAvatar } from "~/features/projects/components/ProjectAvatar";
1010
import { useMetadata } from "~/hooks/useMetadata";
1111
import { removeMarkdown } from "~/utils/removeMarkdown";
1212
import { formatDate } from "~/utils/time";
13+
import { type IRecipient, type IRecipientContract, ERequestType } from "~/utils/types";
1314

1415
import type { TRequestToApprove, Metadata } from "../types";
15-
import type { IRecipient, IRecipientContract } from "~/utils/types";
1616

1717
export interface IProposalItemProps {
1818
index: string;
1919
recipient: IRecipient | IRecipientContract;
2020
isApproved?: boolean;
2121
isLoading?: boolean;
2222
pollId: string;
23+
type: ERequestType;
2324
}
2425

2526
export const ProposalItem = ({
@@ -28,6 +29,7 @@ export const ProposalItem = ({
2829
isApproved = false,
2930
isLoading = false,
3031
pollId,
32+
type,
3133
}: IProposalItemProps): JSX.Element => {
3234
const metadata = useMetadata<Metadata>(recipient.metadataUrl);
3335

@@ -80,6 +82,14 @@ export const ProposalItem = ({
8082
</Skeleton>
8183
</div>
8284

85+
<div className="flex-[2]">
86+
{type.toString() === "Add" && <Badge variant="notice">New</Badge>}
87+
88+
{type.toString() === "Change" && <Badge variant="pending">Edit</Badge>}
89+
90+
{type.toString() === "Remove" && <Badge variant="reject">Delete</Badge>}
91+
</div>
92+
8393
<div className="flex-[2]">
8494
{isApproved && <Badge variant="success">Approved</Badge>}
8595

0 commit comments

Comments
 (0)