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.

-
-
- - -
-
- -
-
- - -
-
- - {isFormDirty && ( - - )} -
-
+ 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 ( +
+
+ + +
+
+ +
+
+ + +
+
+ + {isFormDirty && ( + + )} +
+
+ ); +} 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; +}