diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4f9e0867..53f3a7b4 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -36,6 +36,7 @@ import {
import {
debounce,
filterBounties,
+ validateStellarPublicKey,
} from "./utils";
import {
type Bounty,
@@ -53,6 +54,7 @@ import ContributorProfilePage from "./ContributorProfilePage";
import ContributorDashboard from "./ContributorDashboard";
import ErrorBoundary from "./ErrorBoundary";
import SubmissionChecklistModal, { type SubmissionFormData } from "./SubmissionChecklistModal";
+import BountyCreationForm from "./BountyCreationForm";
const DARK_MODE_KEY = "stellar-bounty-board-theme";
@@ -80,26 +82,6 @@ function useDarkMode() {
return { dark, toggle: () => setDark((d) => !d) };
}
-const initialForm: CreateBountyPayload = {
- repo: "ritik4ever/stellar-stream",
- issueNumber: 48,
- title: "",
- summary: "",
- maintainer: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
- tokenSymbol: "XLM",
- amount: 150,
- deadlineDays: 14,
- labels: [{ name: "help wanted", color: "0075ca" }],
-};
-
-function validateStellarPublicKey(input: string): string | null {
- const value = input.trim();
- if (!value) return "Address is required.";
- if (!/^G[A-Z0-9]{55}$/.test(value))
- return "Enter a Stellar public key (starts with 'G', 56 characters)";
- return null;
-}
-
const contributorStatuses: Array = [
"all",
"reserved",
@@ -121,16 +103,12 @@ function App() {
const { dark, toggle: toggleDark } = useDarkMode();
const freighter = useFreighter();
const initialFilters = useMemo(() => readInitialFilters(), []);
- const [form, setForm] = useState(initialForm);
const [bounties, setBounties] = useState([]);
const [, setIssues] = useState([]);
const [loading, setLoading] = useState(true);
- const [submitting, setSubmitting] = useState(false);
const [showShortcutsOverlay, setShowShortcutsOverlay] = useState(false);
const [isFormDirty, setIsFormDirty] = useState(false);
- useBeforeUnload(isFormDirty);
-
useEffect(() => {
function goOnline() {
// setIsOffline(false);
@@ -605,41 +583,14 @@ function App() {
);
}
- async function handleCreate(event: FormEvent) {
- event.preventDefault();
- setSubmitting(true);
+ async function handleCreateBounty(payload: CreateBountyPayload) {
try {
- // Validate required fields
- if (!form.repo.trim()) {
- toast.error("Repository is required.");
- return;
- }
- if (!form.title.trim()) {
- toast.error("Title is required.");
- return;
- }
- if (form.amount <= 0) {
- toast.error("Reward amount must be greater than 0.");
- return;
- }
- const maintainerError = validateStellarPublicKey(form.maintainer);
- if (maintainerError) {
- toast.error(`Maintainer address: ${maintainerError}`);
- return;
- }
- await createBounty({
- ...form,
- maintainer: form.maintainer.trim(),
- labels: form.labels.filter(Boolean),
- });
- setForm({ ...initialForm, issueNumber: form.issueNumber + 1 });
- setIsFormDirty(false);
+ await createBounty(payload);
await refresh();
toast.success("Bounty created successfully!");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to create bounty.");
- } finally {
- setSubmitting(false);
+ throw err;
}
}
@@ -669,88 +620,10 @@ function App() {
A decentralized bounty platform powered by Stellar. Reserve tasks, submit solutions,
and get paid instantly.
-
+
diff --git a/frontend/src/BountyCreationForm.tsx b/frontend/src/BountyCreationForm.tsx
new file mode 100644
index 00000000..41d711bf
--- /dev/null
+++ b/frontend/src/BountyCreationForm.tsx
@@ -0,0 +1,160 @@
+import React, { useState, type FormEvent } from "react";
+import { toast } from "sonner";
+import { type CreateBountyPayload } from "./types";
+import { validateStellarPublicKey } from "./utils";
+import { useBeforeUnload } from "./useBeforeUnload";
+
+const initialForm: CreateBountyPayload = {
+ repo: "ritik4ever/stellar-stream",
+ issueNumber: 48,
+ title: "",
+ summary: "",
+ maintainer: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
+ tokenSymbol: "XLM",
+ amount: 150,
+ deadlineDays: 14,
+ labels: [{ name: "help wanted", color: "0075ca" }],
+};
+
+interface BountyCreationFormProps {
+ onSubmit: (payload: CreateBountyPayload) => Promise;
+ onDirtyChange?: (isDirty: boolean) => void;
+}
+
+export default function BountyCreationForm({ onSubmit, onDirtyChange }: BountyCreationFormProps) {
+ const [form, setForm] = useState(initialForm);
+ const [submitting, setSubmitting] = useState(false);
+ const [isFormDirty, setIsFormDirty] = useState(false);
+
+ useBeforeUnload(isFormDirty);
+
+ const updateDirtyState = (dirty: boolean) => {
+ setIsFormDirty(dirty);
+ onDirtyChange?.(dirty);
+ };
+
+ async function handleCreate(event: FormEvent) {
+ event.preventDefault();
+ setSubmitting(true);
+ try {
+ // Validate required fields
+ if (!form.repo.trim()) {
+ toast.error("Repository is required.");
+ return;
+ }
+ if (!form.title.trim()) {
+ toast.error("Title is required.");
+ return;
+ }
+ if (form.amount <= 0) {
+ toast.error("Reward amount must be greater than 0.");
+ return;
+ }
+ const maintainerError = validateStellarPublicKey(form.maintainer);
+ if (maintainerError) {
+ toast.error(`Maintainer address: ${maintainerError}`);
+ return;
+ }
+
+ const payload = {
+ ...form,
+ maintainer: form.maintainer.trim(),
+ labels: form.labels.filter(Boolean),
+ };
+
+ await onSubmit(payload);
+
+ setForm({ ...initialForm, issueNumber: form.issueNumber + 1 });
+ updateDirtyState(false);
+ } catch (err) {
+ // Allow parent to handle/toast error, keep form state
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index 8da1bc4b..1f3a61ce 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -324,3 +324,11 @@ export async function getXlmRate(): Promise {
return pendingRequest;
}
+
+export function validateStellarPublicKey(input: string): string | null {
+ const value = input.trim();
+ if (!value) return "Address is required.";
+ if (!/^G[A-Z0-9]{55}$/.test(value))
+ return "Enter a Stellar public key (starts with 'G', 56 characters)";
+ return null;
+}