Skip to content

Commit

Permalink
Feature/enable bounty submission (#179)
Browse files Browse the repository at this point in the history
* feat: allow submit a bounty through the UI

* feat: refactor form to allow submitting a binary (tar.xz)

* Refactoring on validation for bundle input

* show error message when validating dropzone

* Fix: Proper handle bundle fields dependency on validation

* Remove unused constant
  • Loading branch information
claudioantonio authored Oct 28, 2024
1 parent 3502cf5 commit 761d238
Showing 3 changed files with 112 additions and 13 deletions.
120 changes: 111 additions & 9 deletions frontend/src/app/bounty/create/page.tsx
Original file line number Diff line number Diff line change
@@ -4,14 +4,17 @@ import { FC, useState, useEffect } from "react";
import {
Box,
Button,
Center,
Stack,
Title,
TextInput,
Textarea,
useMantineTheme,
Tabs,
Text,
Code,
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { FileWithPath } from "@mantine/dropzone";
import { isNotEmpty, useForm } from "@mantine/form";
import { isAddress, zeroAddress } from "viem";

@@ -21,6 +24,31 @@ import { CreateAppBounty } from "../../../model/inputs";
import { usePrepareCreateBounty } from "../../../hooks/bug-buster";
import { useBlockTimestamp } from "../../../hooks/block";
import { transactionStatus } from "../../../utils/transactionStatus";
import { readFile } from "fs";
import { FileDrop } from "../../../components/filedrop";
import { validate } from "graphql";

interface FileDropTextParams {
filename?: string;
}

const FileDropText: FC<FileDropTextParams> = ({ filename }) => {
if (filename) {
return (
<Box>
<Text size="lg">Bundle uploaded!</Text>
<Code>{filename}</Code>
</Box>
);
} else {
return (
<Box>
<Text size="lg">Drop your bounty bundle here!</Text>
<Code>*.tar.xz</Code>
</Box>
);
}
};

interface CreateBountyFormValues {
name?: string;
@@ -46,6 +74,29 @@ const CreateBountyForm: FC = () => {
}
}, [blockTimestamp]);

function isBountyBundleEmpty(bundle: string | undefined) {
return bundle === undefined || bundle.length === 0;
}

function validateBountyBundle(
zipBinary: string | undefined,
codeZipPath: string | undefined,
) {
if (
isBountyBundleEmpty(zipBinary) &&
isBountyBundleEmpty(codeZipPath)
) {
return "A bundle path or binary is required";
} else if (
!isBountyBundleEmpty(zipBinary) &&
!isBountyBundleEmpty(codeZipPath)
) {
return "Cannot set a bundle path and also a bundle binary";
} else {
return null;
}
}

const form = useForm({
initialValues: {} as CreateBountyFormValues,
transformValues: (values) => {
@@ -66,7 +117,12 @@ const CreateBountyForm: FC = () => {
name: isNotEmpty("A name is required"),
description: isNotEmpty("A description is required"),
deadline: isNotEmpty("A deadline is required"),
codeZipPath: isNotEmpty("A code path is required"),
codeZipBinary: (value, values) => {
return validateBountyBundle(value, values.codeZipPath);
},
codeZipPath: (value, values) => {
return validateBountyBundle(values.codeZipBinary, value);
},
token: (token) => {
if (token === undefined) {
return "A token address is required";
@@ -81,8 +137,28 @@ const CreateBountyForm: FC = () => {
},
});

useEffect(() => {
if (form.values.codeZipPath !== undefined)
form.validateField("codeZipBinary");
}, [form]);

const { name, description, deadline, token } = form.getTransformedValues();

const [filename, setFilename] = useState<string | undefined>();
const readFile = (f: FileWithPath | null) => {
if (f) {
f.arrayBuffer().then((buf) => {
const codeZipBinary = btoa(
Array.from(new Uint8Array(buf))
.map((b) => String.fromCharCode(b))
.join(""),
);
form.setFieldValue("codeZipBinary", codeZipBinary);
setFilename(f.name);
});
}
};

const bounty: CreateAppBounty = {
...form.values,
name: name ?? "",
@@ -152,13 +228,39 @@ const CreateBountyForm: FC = () => {
minDate={minDeadline}
{...form.getInputProps("deadline")}
/>
<TextInput
size="lg"
label="Bundle path"
placeholder="/path/to/bundle.tar.xz"
description="Path to the bounty bundle in the machine filesystem"
{...form.getInputProps("codeZipPath")}
/>

<Tabs defaultValue="file">
<Tabs.List>
<Tabs.Tab value="file">Upload</Tabs.Tab>
<Tabs.Tab value="path">Built-in</Tabs.Tab>
</Tabs.List>

<Tabs.Panel value="file">
<FileDrop
onDrop={(files) => readFile(files[0])}
accept={{
"application/octet-stream": [".tar.xz"],
}}
>
<FileDropText filename={filename} />
</FileDrop>
{form.errors.codeZipBinary && (
<Text size="sm" c="red">
{form.errors.codeZipBinary}
</Text>
)}
</Tabs.Panel>

<Tabs.Panel value="path">
<TextInput
size="lg"
label="Bundle path"
placeholder="/path/to/bundle.tar.xz"
description="Path to the bounty bundle in the machine filesystem"
{...form.getInputProps("codeZipPath")}
/>
</Tabs.Panel>
</Tabs>
<Button
size="lg"
disabled={
3 changes: 1 addition & 2 deletions frontend/src/app/explore/page.tsx
Original file line number Diff line number Diff line change
@@ -20,7 +20,6 @@ import { BountyStatusBadgeGroup } from "../../components/bountyStatus";
import { HasConnectedAccount } from "../../components/hasConnectedAccount";
import { useBlockTimestamp } from "../../hooks/block";
import { getBountyStatus } from "../../utils/bounty";
import { GOOGLE_BOUNTY_CREATION_FORM_URL } from "../../utils/links";

const Bounty: FC<{
index: number;
@@ -102,7 +101,7 @@ const Explore: FC = () => {
justify="flex-end"
visibleFrom="md"
>
<Link href={GOOGLE_BOUNTY_CREATION_FORM_URL}>
<Link href="/bounty/create">
<Button size="lg">Create bounty</Button>
</Link>
</Flex>
2 changes: 0 additions & 2 deletions frontend/src/utils/links.tsx
Original file line number Diff line number Diff line change
@@ -3,5 +3,3 @@ export const GH_MAIN_BRANCH = "main";
export const GH_README_URL = `${GH_REPO_URL}/blob/${GH_MAIN_BRANCH}/README.md`;
export const TELEGRAM_CHANNEL_URL = "https://t.me/+G_CPMEhCHC04MzA5";
export const X_ACCOUNT_URL = "https://x.com/BugBusterApp";
export const GOOGLE_BOUNTY_CREATION_FORM_URL =
"https://forms.gle/h4EePFXG41Zwv48a7";

0 comments on commit 761d238

Please sign in to comment.