Skip to content

Commit 41f22cc

Browse files
authored
Merge pull request #169 from jerrybarry/feature/form-validation-system-reusable-components
feat: implement form validation system with reusable field components
2 parents 73aaf15 + ad764aa commit 41f22cc

16 files changed

Lines changed: 718 additions & 79 deletions

frontend/package-lock.json

Lines changed: 1 addition & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"devDependencies": {
2828
"@eslint/js": "^9.39.1",
29+
"@playwright/test": "^1.58.2",
2930
"@sentry/react": "^10.45.0",
3031
"@sentry/vite-plugin": "^5.1.1",
3132
"@testing-library/dom": "^10.4.1",

frontend/src/components/VaultDashboard.tsx

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { hasCustomRpcConfig, networkConfig } from "../config/network";
44
import { useVault } from "../context/VaultContext";
55
import ApiStatusBanner from "./ApiStatusBanner";
66
import { useToast } from "../context/ToastContext";
7+
import { FormField, SubmitButton, useForm, type ValidationSchema } from "../forms";
78

89
interface VaultDashboardProps {
910
walletAddress: string | null;
@@ -13,30 +14,52 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
1314
const { formattedTvl, formattedApy, summary, error, isLoading } = useVault();
1415
const toast = useToast();
1516
const [activeTab, setActiveTab] = useState<"deposit" | "withdraw">("deposit");
16-
const [amount, setAmount] = useState("");
1717
const [isProcessing, setIsProcessing] = useState(false);
1818
const [fakeBalance, setFakeBalance] = useState(1250.5);
1919

2020
const yieldRate = formattedApy;
2121
const tvl = formattedTvl;
2222
const strategy = summary.strategy;
2323

24-
const handleTransaction = () => {
25-
if (!walletAddress || !amount || isNaN(Number(amount))) {
24+
const schema: ValidationSchema<{ amount: string }> = {
25+
amount: {
26+
required: "Enter an amount to continue.",
27+
custom: (value) => {
28+
const parsed = Number(value);
29+
if (Number.isNaN(parsed)) {
30+
return "Enter a valid number.";
31+
}
32+
if (parsed <= 0) {
33+
return "Amount must be greater than 0.";
34+
}
35+
return undefined;
36+
},
37+
},
38+
};
39+
40+
const { values, errors, handleChange, handleBlur, handleSubmit } = useForm(
41+
{ amount: "" },
42+
schema,
43+
);
44+
45+
const handleTransaction = async () => {
46+
if (!walletAddress) {
2647
toast.warning({
27-
title: "Enter a valid amount",
28-
description: "Choose a wallet and amount before submitting the transaction.",
48+
title: "Wallet required",
49+
description: "Connect your wallet before submitting a transaction.",
2950
});
3051
return;
3152
}
53+
3254
setIsProcessing(true);
3355

3456
// Simulate transaction delay
35-
setTimeout(() => {
36-
const value = Number(amount);
57+
await new Promise<void>((resolve) => {
58+
setTimeout(() => {
59+
const value = Number(values.amount);
3760
if (activeTab === "deposit") setFakeBalance(prev => prev + value);
3861
if (activeTab === "withdraw") setFakeBalance(prev => Math.max(0, prev - value));
39-
setAmount("");
62+
handleChange({ target: { name: "amount", value: "" } } as Parameters<typeof handleChange>[0]);
4063
setIsProcessing(false);
4164
toast.success({
4265
title: activeTab === "deposit" ? "Deposit queued" : "Withdrawal queued",
@@ -45,7 +68,9 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
4568
? `${value.toFixed(2)} USDC has been added to your pending vault activity.`
4669
: `${value.toFixed(2)} USDC has been added to your pending withdrawal activity.`,
4770
});
48-
}, 2000);
71+
resolve();
72+
}, 2000);
73+
});
4974
};
5075

5176
return (
@@ -188,24 +213,32 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
188213

189214
<div className="flex justify-between items-center" style={{ marginBottom: '16px' }}>
190215
<div style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
191-
{activeTab === 'deposit' ? 'Amount to deposit' : 'Amount to withdraw'}
216+
Transaction
192217
</div>
193218
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>
194219
Balance: <span style={{ color: 'var(--text-primary)', fontWeight: 600 }}>{walletAddress ? fakeBalance.toFixed(2) : '0.00'}</span>
195220
</div>
196221
</div>
197222

198-
<div className="input-group" style={{ marginBottom: '24px' }}>
199-
<div className="input-wrapper">
200-
<span style={{ color: 'var(--text-secondary)', paddingRight: '12px', borderRight: '1px solid var(--border-glass)', marginRight: '16px' }}>USDC</span>
201-
<input
202-
className="input-field"
223+
<form onSubmit={handleSubmit(handleTransaction)}>
224+
<div className="input-group" style={{ marginBottom: '24px' }}>
225+
<FormField
226+
label={activeTab === 'deposit' ? 'Amount to deposit' : 'Amount to withdraw'}
227+
name="amount"
203228
type="number"
204229
placeholder="0.00"
205-
value={amount}
206-
onChange={(e) => setAmount(e.target.value)}
230+
value={values.amount}
231+
onChange={handleChange}
232+
onBlur={handleBlur}
233+
error={errors.amount}
234+
style={{ fontSize: '1.25rem', fontFamily: 'var(--font-display)' }}
207235
/>
236+
</div>
237+
238+
<div className="flex justify-between" style={{ marginBottom: '24px' }}>
239+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Asset: USDC</span>
208240
<button
241+
type="button"
209242
style={{
210243
color: 'var(--accent-cyan)',
211244
fontSize: '0.8rem',
@@ -214,40 +247,38 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
214247
padding: '4px 10px',
215248
borderRadius: '6px'
216249
}}
217-
onClick={() => setAmount(fakeBalance.toString())}
250+
onClick={() => handleChange({ target: { name: 'amount', value: fakeBalance.toString() } } as Parameters<typeof handleChange>[0])}
218251
>
219252
MAX
220253
</button>
221254
</div>
222-
</div>
223255

224-
<div className="glass-panel" style={{ padding: '16px', background: 'var(--bg-muted)', marginBottom: '24px' }}>
225-
<div className="flex justify-between items-center">
226-
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>BENJI Strategy</span>
227-
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>
228-
{strategy.status === 'active' ? 'Active' : 'Inactive'}
229-
</span>
230-
</div>
231-
<div className="flex justify-between items-center" style={{ marginTop: '8px' }}>
232-
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Exchange Rate</span>
233-
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>
234-
1 yvUSDC = {summary.exchangeRate.toFixed(3)} USDC
235-
</span>
236-
</div>
237-
<div className="flex justify-between items-center" style={{ marginTop: '8px' }}>
238-
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Network Fee</span>
239-
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>{summary.networkFeeEstimate}</span>
256+
<div className="glass-panel" style={{ padding: '16px', background: 'var(--bg-muted)', marginBottom: '24px' }}>
257+
<div className="flex justify-between items-center">
258+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>BENJI Strategy</span>
259+
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>
260+
{strategy.status === 'active' ? 'Active' : 'Inactive'}
261+
</span>
262+
</div>
263+
<div className="flex justify-between items-center" style={{ marginTop: '8px' }}>
264+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Exchange Rate</span>
265+
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>
266+
1 yvUSDC = {summary.exchangeRate.toFixed(3)} USDC
267+
</span>
268+
</div>
269+
<div className="flex justify-between items-center" style={{ marginTop: '8px' }}>
270+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Network Fee</span>
271+
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>{summary.networkFeeEstimate}</span>
272+
</div>
240273
</div>
241-
</div>
242274

243-
<button
244-
className="btn btn-primary"
245-
style={{ width: '100%', padding: '16px', fontSize: '1.1rem' }}
246-
onClick={handleTransaction}
247-
disabled={isProcessing || !amount || Number(amount) <= 0}
248-
>
249-
{isProcessing ? 'Processing Transaction...' : (activeTab === 'deposit' ? 'Approve & Deposit' : 'Withdraw Funds')}
250-
</button>
275+
<SubmitButton
276+
loading={isProcessing}
277+
disabled={!values.amount || Number(values.amount) <= 0}
278+
label={activeTab === 'deposit' ? 'Approve & Deposit' : 'Withdraw Funds'}
279+
loadingLabel="Processing Transaction..."
280+
/>
281+
</form>
251282

252283
</div>
253284
</div>

0 commit comments

Comments
 (0)