@@ -15,6 +15,51 @@ import {
1515} from "./services/api" ;
1616import { Campaign , CampaignEvent , OpenIssue } from "./types/campaign" ;
1717
18+ function round ( value : number ) : number {
19+ return Number ( value . toFixed ( 2 ) ) ;
20+ }
21+
22+ function delay ( ms : number ) : Promise < void > {
23+ return new Promise ( ( resolve ) => {
24+ setTimeout ( resolve , ms ) ;
25+ } ) ;
26+ }
27+
28+ function toOptimisticPledgedCampaign ( campaign : Campaign , amount : number ) : Campaign {
29+ const nowInSeconds = Math . floor ( Date . now ( ) / 1000 ) ;
30+ const nextPledgedAmount = round ( campaign . pledgedAmount + amount ) ;
31+ const deadlineReached = nowInSeconds >= campaign . deadline ;
32+ const status =
33+ campaign . claimedAt !== undefined
34+ ? "claimed"
35+ : nextPledgedAmount >= campaign . targetAmount
36+ ? "funded"
37+ : deadlineReached
38+ ? "failed"
39+ : "open" ;
40+
41+ return {
42+ ...campaign ,
43+ pledgedAmount : nextPledgedAmount ,
44+ progress : {
45+ ...campaign . progress ,
46+ status,
47+ percentFunded : round ( ( nextPledgedAmount / campaign . targetAmount ) * 100 ) ,
48+ remainingAmount : round ( Math . max ( 0 , campaign . targetAmount - nextPledgedAmount ) ) ,
49+ pledgeCount : campaign . progress . pledgeCount + 1 ,
50+ canPledge : campaign . claimedAt === undefined && ! deadlineReached ,
51+ canClaim :
52+ campaign . claimedAt === undefined &&
53+ deadlineReached &&
54+ nextPledgedAmount >= campaign . targetAmount ,
55+ canRefund :
56+ campaign . claimedAt === undefined &&
57+ deadlineReached &&
58+ nextPledgedAmount < campaign . targetAmount ,
59+ } ,
60+ } ;
61+ }
62+
1863function App ( ) {
1964 const [ campaigns , setCampaigns ] = useState < Campaign [ ] > ( [ ] ) ;
2065 const [ issues , setIssues ] = useState < OpenIssue [ ] > ( [ ] ) ;
@@ -23,6 +68,7 @@ function App() {
2368 const [ createError , setCreateError ] = useState < string | null > ( null ) ;
2469 const [ actionError , setActionError ] = useState < string | null > ( null ) ;
2570 const [ actionMessage , setActionMessage ] = useState < string | null > ( null ) ;
71+ const [ pendingPledgeCampaignId , setPendingPledgeCampaignId ] = useState < string | null > ( null ) ;
2672
2773 async function refreshCampaigns ( nextSelectedId ?: string | null ) {
2874 const data = await listCampaigns ( ) ;
@@ -98,13 +144,55 @@ function App() {
98144 setActionError ( null ) ;
99145 setActionMessage ( null ) ;
100146
147+ const previousCampaigns = campaigns ;
148+ const previousHistory = history ;
149+ const optimisticTimestamp = Math . floor ( Date . now ( ) / 1000 ) ;
150+ const optimisticEvent : CampaignEvent = {
151+ id : - Date . now ( ) ,
152+ campaignId,
153+ eventType : "pledged" ,
154+ timestamp : optimisticTimestamp ,
155+ actor : contributor ,
156+ amount,
157+ metadata : { pending : true } ,
158+ } ;
159+
160+ setCampaigns ( ( currentCampaigns ) =>
161+ currentCampaigns . map ( ( campaign ) =>
162+ campaign . id === campaignId ? toOptimisticPledgedCampaign ( campaign , amount ) : campaign ,
163+ ) ,
164+ ) ;
165+ setPendingPledgeCampaignId ( campaignId ) ;
166+ if ( selectedCampaignId === campaignId ) {
167+ setHistory ( ( currentHistory ) => [ optimisticEvent , ...currentHistory ] ) ;
168+ }
169+ setActionMessage ( "Submitting pledge..." ) ;
170+
171+ const pendingStartedAt = Date . now ( ) ;
172+ const minimumPendingMs = 800 ;
173+
101174 try {
102175 await addPledge ( campaignId , { contributor, amount } ) ;
176+ const elapsedMs = Date . now ( ) - pendingStartedAt ;
177+ if ( elapsedMs < minimumPendingMs ) {
178+ await delay ( minimumPendingMs - elapsedMs ) ;
179+ }
103180 await refreshCampaigns ( campaignId ) ;
104181 await refreshHistory ( campaignId ) ;
182+ setPendingPledgeCampaignId ( null ) ;
105183 setActionMessage ( "Pledge recorded in the local goal vault." ) ;
106184 } catch ( error ) {
185+ const elapsedMs = Date . now ( ) - pendingStartedAt ;
186+ if ( elapsedMs < minimumPendingMs ) {
187+ await delay ( minimumPendingMs - elapsedMs ) ;
188+ }
189+ setCampaigns ( previousCampaigns ) ;
190+ if ( selectedCampaignId === campaignId ) {
191+ setHistory ( previousHistory ) ;
192+ }
193+ setPendingPledgeCampaignId ( null ) ;
107194 setActionError ( error instanceof Error ? error . message : "Failed to add pledge." ) ;
195+ setActionMessage ( null ) ;
108196 }
109197 }
110198
@@ -172,6 +260,7 @@ function App() {
172260 campaign = { selectedCampaign }
173261 actionError = { actionError }
174262 actionMessage = { actionMessage }
263+ isPledgePending = { pendingPledgeCampaignId === selectedCampaignId }
175264 onPledge = { handlePledge }
176265 onClaim = { handleClaim }
177266 onRefund = { handleRefund }
0 commit comments