Skip to content

Commit 3ae62a7

Browse files
authored
Back project (#133)
* feat: initialize back project flow and project history demo page * fix: implement back project form * fix: fix back project flow
1 parent 2062c01 commit 3ae62a7

7 files changed

Lines changed: 546 additions & 11 deletions

File tree

app/globals.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,16 @@ button {
173173
.slim-scrollbar::-webkit-scrollbar-track {
174174
background-color: transparent;
175175
}
176+
177+
/* Hide arrows in Chrome, Safari, Edge, Opera */
178+
input[type='number']::-webkit-inner-spin-button,
179+
input[type='number']::-webkit-outer-spin-button {
180+
-webkit-appearance: none;
181+
margin: 0;
182+
}
183+
184+
/* Hide arrows in Firefox */
185+
input[type='number'] {
186+
-moz-appearance: textfield;
187+
appearance: textfield;
188+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
'use client';
2+
3+
import type React from 'react';
4+
import { useState } from 'react';
5+
import {
6+
Select,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
} from '@/components/ui/select';
12+
import { Checkbox } from '@/components/ui/checkbox';
13+
import { ArrowLeft, Check, Copy } from 'lucide-react';
14+
import { BoundlessButton } from '@/components/buttons';
15+
16+
interface BackProjectFormProps {
17+
onSubmit: (data: {
18+
amount: string;
19+
currency: string;
20+
token: string;
21+
network: string;
22+
walletAddress: string;
23+
keepAnonymous: boolean;
24+
}) => void;
25+
isLoading?: boolean;
26+
}
27+
28+
const QUICK_AMOUNTS = [10, 20, 30, 50, 100, 500, 1000];
29+
30+
export function BackProjectForm({
31+
onSubmit,
32+
isLoading = false,
33+
}: BackProjectFormProps) {
34+
const [amount, setAmount] = useState('');
35+
const [currency] = useState('USDT');
36+
const [token, setToken] = useState('');
37+
const [network, setNetwork] = useState('Stella / Soroban');
38+
const [walletAddress] = useState('GDS3...GB7');
39+
const [keepAnonymous, setKeepAnonymous] = useState(false);
40+
41+
const handleSubmit = (e: React.FormEvent) => {
42+
e.preventDefault();
43+
onSubmit({
44+
amount,
45+
currency,
46+
token,
47+
network,
48+
walletAddress,
49+
keepAnonymous,
50+
});
51+
};
52+
53+
const handleQuickAmount = (quickAmount: number) => {
54+
setAmount(quickAmount.toString());
55+
};
56+
57+
const handleCopyAddress = async (e: React.MouseEvent) => {
58+
e.preventDefault();
59+
try {
60+
await navigator.clipboard.writeText(walletAddress);
61+
// Could add toast notification here instead of state
62+
} catch (err) {
63+
// Fallback for browsers that don't support clipboard API
64+
const textArea = document.createElement('textarea');
65+
console.error(err);
66+
67+
textArea.value = walletAddress;
68+
document.body.appendChild(textArea);
69+
textArea.select();
70+
try {
71+
document.execCommand('copy');
72+
} catch (copyErr) {
73+
console.error('Failed to copy address:', copyErr);
74+
}
75+
document.body.removeChild(textArea);
76+
}
77+
};
78+
79+
const isFormValid = amount && currency && token && walletAddress;
80+
81+
return (
82+
<div className='text-white'>
83+
<div className='mb-5 flex gap-10 '>
84+
<ArrowLeft className='cursor-pointer' />
85+
<h1 className='text-white text-lg font-semibold'>Back Project</h1>
86+
</div>
87+
<div className='w-[500px] flex flex-col gap-3 pt-3 pb-6'>
88+
<div className='space-y-2'>
89+
<p className='text-orange-400 text-sm'>
90+
Funds will be held in escrow and released only upon milestone
91+
approvals.
92+
</p>
93+
</div>
94+
95+
<div className='flex flex-col gap-1'>
96+
<label className='text-xs text-card font-medium'>
97+
Amount <span className='text-red-500'>*</span>
98+
</label>
99+
<div className='w-full h-12 flex items-center gap-3 p-4 rounded-[12px] bg-stepper-foreground border border-stepper-border'>
100+
<span className='text-card text-sm'>{currency}</span>
101+
<input
102+
value={amount}
103+
onChange={e => setAmount(e.target.value)}
104+
type='number'
105+
className='w-full bg-transparent font-normal text-base text-placeholder focus:outline-none'
106+
placeholder='1000'
107+
disabled={isLoading}
108+
/>
109+
</div>
110+
<p className='text-card/60 text-xs'>min. amount: $10</p>
111+
112+
<div className='flex flex-wrap gap-2 mt-2'>
113+
{QUICK_AMOUNTS.map(quickAmount => (
114+
<button
115+
key={quickAmount}
116+
type='button'
117+
onClick={() => handleQuickAmount(quickAmount)}
118+
className='px-3 py-1 text-sm bg-stepper-foreground border border-stepper-border text-card rounded-[8px] hover:bg-stepper-foreground/80 transition-colors'
119+
disabled={isLoading}
120+
>
121+
${quickAmount}
122+
</button>
123+
))}
124+
</div>
125+
</div>
126+
127+
<div className='flex flex-col gap-1'>
128+
<label className='text-xs text-card font-medium'>
129+
Select Token <span className='text-red-500'>*</span>
130+
</label>
131+
<Select value={token} onValueChange={setToken} disabled={isLoading}>
132+
<SelectTrigger className='w-full !h-12 flex items-center !gap-3 p-4 rounded-[12px] bg-stepper-foreground border border-stepper-border focus:ring-0'>
133+
<SelectValue placeholder='Select' />
134+
</SelectTrigger>
135+
<SelectContent className='max-h-[200px] bg-background rounded-[12px] font-normal text-base text-placeholder border border-stepper-border overflow-y-auto'>
136+
<SelectItem value='XLM'>XLM</SelectItem>
137+
<SelectItem value='USDT'>USDT</SelectItem>
138+
<SelectItem value='USDC'>USDC</SelectItem>
139+
</SelectContent>
140+
</Select>
141+
</div>
142+
143+
<div className='flex flex-col gap-1'>
144+
<label className='text-xs text-card font-medium'>
145+
Network <span className='text-red-500'>*</span>
146+
</label>
147+
<div className='w-full h-12 flex items-center gap-3 p-4 rounded-[12px] bg-stepper-foreground border border-stepper-border'>
148+
<input
149+
value={network}
150+
onChange={e => setNetwork(e.target.value)}
151+
type='text'
152+
className='w-full bg-transparent font-normal text-base text-placeholder focus:outline-none'
153+
disabled={isLoading}
154+
/>
155+
</div>
156+
</div>
157+
158+
<div className='flex flex-col gap-1'>
159+
<label className='text-xs text-card font-medium'>
160+
Wallet Address <span className='text-red-500'>*</span>
161+
</label>
162+
<BoundlessButton
163+
type='button'
164+
onClick={handleCopyAddress}
165+
className='w-full h-12 flex items-center justify-center gap-3 p-4 rounded-[12px] bg-blue-600 hover:bg-blue-700 text-white transition-colors'
166+
disabled={isLoading}
167+
>
168+
<Check className='w-4 h-4' />
169+
<span className='font-normal text-base'>{walletAddress}</span>
170+
<Copy className='w-4 h-4' />
171+
</BoundlessButton>
172+
</div>
173+
174+
<div className='flex items-center gap-2 mt-2'>
175+
<Checkbox
176+
id='anonymous'
177+
checked={keepAnonymous}
178+
onCheckedChange={checked => setKeepAnonymous(checked as boolean)}
179+
disabled={isLoading}
180+
className='border-stepper-border data-[state=checked]:bg-primary data-[state=checked]:border-primary'
181+
/>
182+
<label htmlFor='anonymous' className='text-card text-sm font-normal'>
183+
Keep me anonymous
184+
</label>
185+
</div>
186+
187+
<BoundlessButton
188+
type='button'
189+
disabled={!isFormValid || isLoading}
190+
onClick={handleSubmit}
191+
className={`w-full mt-4 h-12 text-base font-medium transition-colors ${
192+
isFormValid && !isLoading
193+
? 'bg-primary text-background border border-primary hover:bg-primary/90'
194+
: 'bg-stepper-foreground text-card/30 border border-stepper-border cursor-not-allowed'
195+
}`}
196+
>
197+
Confirm Contribution
198+
</BoundlessButton>
199+
</div>
200+
</div>
201+
);
202+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { BoundlessButton } from '@/components/buttons';
5+
import { ProjectSubmissionSuccess } from '@/components/project';
6+
import BoundlessSheet from '@/components/sheet/boundless-sheet';
7+
import { ProjectSubmissionLoading } from '@/components/flows/back-project/project-submission-loading';
8+
import { BackProjectForm } from './back-project-form';
9+
10+
type BackProjectState = 'form' | 'loading' | 'success';
11+
12+
interface BackProjectData {
13+
amount: string;
14+
currency: string;
15+
token: string;
16+
network: string;
17+
walletAddress: string;
18+
keepAnonymous: boolean;
19+
}
20+
21+
const BackProject = () => {
22+
const [isSheetOpen, setIsSheetOpen] = useState(false);
23+
const [backProjectState, setBackProjectState] =
24+
useState<BackProjectState>('form');
25+
26+
const handleBackProject = (data: BackProjectData) => {
27+
setBackProjectState('loading');
28+
console.log(data);
29+
30+
// Simulate API call
31+
setTimeout(() => {
32+
setBackProjectState('success');
33+
}, 2000);
34+
};
35+
36+
// const handleContinue = () => {
37+
// setIsSheetOpen(false)
38+
// setBackProjectState("form")
39+
// }
40+
41+
// const handleViewHistory = () => {
42+
// // Navigate to history page or open history modal
43+
// setIsSheetOpen(false)
44+
// // TODO: Implement backing history modal or navigation
45+
// }
46+
47+
// const handleBack = () => {
48+
// if (backProjectState === "success") {
49+
// setBackProjectState("form")
50+
// }
51+
// }
52+
53+
const renderSheetContent = () => {
54+
if (backProjectState === 'success') {
55+
return (
56+
<div className='absolute inset-0 flex flex-col items-center justify-center w-full h-full'>
57+
<div className='flex items-center mb-4 w-full max-w-[500px]'>
58+
{/* <Button variant="ghost" size="icon" onClick={handleBack} className="text-white hover:bg-gray-800">
59+
<ArrowLeft className="w-5 h-5" />
60+
</Button> */}
61+
</div>
62+
<ProjectSubmissionSuccess
63+
title="You've Backed this Campaign!"
64+
description="Your funds have been securely locked in escrow. You'll be notified when milestones are completed."
65+
linkSection='You can see funding history for this campaign'
66+
linkName='Here'
67+
url='/'
68+
// onContinue={handleContinue}
69+
// onViewHistory={handleViewHistory}
70+
/>
71+
</div>
72+
);
73+
}
74+
75+
return (
76+
<div className='relative flex flex-col items-center justify-start min-h-full px-4'>
77+
<BackProjectForm
78+
onSubmit={handleBackProject}
79+
isLoading={backProjectState === 'loading'}
80+
/>
81+
82+
{backProjectState === 'loading' && (
83+
<div className='absolute inset-0 flex items-center justify-center w-full h-full bg-black/50 backdrop-blur-sm'>
84+
<ProjectSubmissionLoading />
85+
</div>
86+
)}
87+
</div>
88+
);
89+
};
90+
91+
return (
92+
<div className='min-h-screen space-x-2 w-full text-white bg-black'>
93+
<BoundlessSheet
94+
open={isSheetOpen}
95+
setOpen={setIsSheetOpen}
96+
// title="Back Project"
97+
showCloseButton={true}
98+
contentClassName={`h-[100vh]`}
99+
className='mx-4'
100+
>
101+
{renderSheetContent()}
102+
</BoundlessSheet>
103+
104+
<BoundlessButton onClick={() => setIsSheetOpen(true)}>
105+
Back Project
106+
</BoundlessButton>
107+
</div>
108+
);
109+
};
110+
111+
export default BackProject;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function ProjectSubmissionLoading() {
2+
return (
3+
<div className='flex flex-col items-center justify-center p-8 space-y-6'>
4+
<div className='relative'>
5+
{/* Outer spinning ring */}
6+
<div className='w-16 h-16 border-4 border-white/20 rounded-full'></div>
7+
{/* Inner spinning arc */}
8+
<div className='absolute top-0 left-0 w-16 h-16 border-4 border-transparent border-t-white rounded-full animate-spin'></div>
9+
</div>
10+
<p className='text-white text-lg font-medium'>
11+
Processing your contribution...
12+
</p>
13+
</div>
14+
);
15+
}

0 commit comments

Comments
 (0)