Skip to content

Commit c813009

Browse files
authored
Merge branch 'main' into feat/url-driven-filters-issue-129
2 parents fa4cad8 + 41f22cc commit c813009

16 files changed

Lines changed: 750 additions & 38 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: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useVault } from "../context/VaultContext";
55
import ApiStatusBanner from "./ApiStatusBanner";
66
import { useToast } from "../context/ToastContext";
77
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
8+
import { FormField, SubmitButton, useForm, type ValidationSchema } from "../forms";
89

910
interface VaultDashboardProps {
1011
walletAddress: string | null;
@@ -15,6 +16,8 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
1516
const toast = useToast();
1617
const [amount, setAmount] = useState("");
1718
const [isProcessing, setIsProcessing] = useState<"deposit" | "withdraw" | null>(null);
19+
const [activeTab, setActiveTab] = useState<"deposit" | "withdraw">("deposit");
20+
const [isProcessing, setIsProcessing] = useState(false);
1821
const [fakeBalance, setFakeBalance] = useState(1250.5);
1922

2023
const yieldRate = formattedApy;
@@ -23,9 +26,32 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
2326

2427
const handleTransaction = (actionType: "deposit" | "withdraw") => {
2528
if (!walletAddress || !amount || isNaN(Number(amount))) {
29+
const schema: ValidationSchema<{ amount: string }> = {
30+
amount: {
31+
required: "Enter an amount to continue.",
32+
custom: (value) => {
33+
const parsed = Number(value);
34+
if (Number.isNaN(parsed)) {
35+
return "Enter a valid number.";
36+
}
37+
if (parsed <= 0) {
38+
return "Amount must be greater than 0.";
39+
}
40+
return undefined;
41+
},
42+
},
43+
};
44+
45+
const { values, errors, handleChange, handleBlur, handleSubmit } = useForm(
46+
{ amount: "" },
47+
schema,
48+
);
49+
50+
const handleTransaction = async () => {
51+
if (!walletAddress) {
2652
toast.warning({
27-
title: "Enter a valid amount",
28-
description: "Choose a wallet and amount before submitting the transaction.",
53+
title: "Wallet required",
54+
description: "Connect your wallet before submitting a transaction.",
2955
});
3056
return;
3157
}
@@ -38,14 +64,27 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
3864
if (actionType === "withdraw") setFakeBalance(prev => Math.max(0, prev - value));
3965
setAmount("");
4066
setIsProcessing(null);
67+
68+
setIsProcessing(true);
69+
70+
// Simulate transaction delay
71+
await new Promise<void>((resolve) => {
72+
setTimeout(() => {
73+
const value = Number(values.amount);
74+
if (activeTab === "deposit") setFakeBalance(prev => prev + value);
75+
if (activeTab === "withdraw") setFakeBalance(prev => Math.max(0, prev - value));
76+
handleChange({ target: { name: "amount", value: "" } } as Parameters<typeof handleChange>[0]);
77+
setIsProcessing(false);
4178
toast.success({
4279
title: actionType === "deposit" ? "Deposit queued" : "Withdrawal queued",
4380
description:
4481
actionType === "deposit"
4582
? `${value.toFixed(2)} USDC has been added to your pending vault activity.`
4683
: `${value.toFixed(2)} USDC has been added to your pending withdrawal activity.`,
4784
});
48-
}, 2000);
85+
resolve();
86+
}, 2000);
87+
});
4988
};
5089

5190
return (
@@ -195,6 +234,43 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
195234
style={{ width: '100%', padding: '16px', fontSize: '1.1rem' }}
196235
onClick={() => handleTransaction('deposit')}
197236
disabled={isProcessing !== null || !amount || Number(amount) <= 0}
237+
<div className="flex justify-between items-center" style={{ marginBottom: '16px' }}>
238+
<div style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
239+
Transaction
240+
</div>
241+
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>
242+
Balance: <span style={{ color: 'var(--text-primary)', fontWeight: 600 }}>{walletAddress ? fakeBalance.toFixed(2) : '0.00'}</span>
243+
</div>
244+
</div>
245+
246+
<form onSubmit={handleSubmit(handleTransaction)}>
247+
<div className="input-group" style={{ marginBottom: '24px' }}>
248+
<FormField
249+
label={activeTab === 'deposit' ? 'Amount to deposit' : 'Amount to withdraw'}
250+
name="amount"
251+
type="number"
252+
placeholder="0.00"
253+
value={values.amount}
254+
onChange={handleChange}
255+
onBlur={handleBlur}
256+
error={errors.amount}
257+
style={{ fontSize: '1.25rem', fontFamily: 'var(--font-display)' }}
258+
/>
259+
</div>
260+
261+
<div className="flex justify-between" style={{ marginBottom: '24px' }}>
262+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Asset: USDC</span>
263+
<button
264+
type="button"
265+
style={{
266+
color: 'var(--accent-cyan)',
267+
fontSize: '0.8rem',
268+
fontWeight: 600,
269+
background: 'var(--accent-cyan-dim)',
270+
padding: '4px 10px',
271+
borderRadius: '6px'
272+
}}
273+
onClick={() => handleChange({ target: { name: 'amount', value: fakeBalance.toString() } } as Parameters<typeof handleChange>[0])}
198274
>
199275
{isProcessing === 'deposit' ? 'Processing Transaction...' : 'Approve & Deposit'}
200276
</button>
@@ -241,6 +317,34 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({ walletAddress }) => {
241317
</button>
242318
</TabsContent>
243319
</Tabs>
320+
</div>
321+
322+
<div className="glass-panel" style={{ padding: '16px', background: 'var(--bg-muted)', marginBottom: '24px' }}>
323+
<div className="flex justify-between items-center">
324+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>BENJI Strategy</span>
325+
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>
326+
{strategy.status === 'active' ? 'Active' : 'Inactive'}
327+
</span>
328+
</div>
329+
<div className="flex justify-between items-center" style={{ marginTop: '8px' }}>
330+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Exchange Rate</span>
331+
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>
332+
1 yvUSDC = {summary.exchangeRate.toFixed(3)} USDC
333+
</span>
334+
</div>
335+
<div className="flex justify-between items-center" style={{ marginTop: '8px' }}>
336+
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Network Fee</span>
337+
<span style={{ fontSize: '0.9rem', fontWeight: 500 }}>{summary.networkFeeEstimate}</span>
338+
</div>
339+
</div>
340+
341+
<SubmitButton
342+
loading={isProcessing}
343+
disabled={!values.amount || Number(values.amount) <= 0}
344+
label={activeTab === 'deposit' ? 'Approve & Deposit' : 'Withdraw Funds'}
345+
loadingLabel="Processing Transaction..."
346+
/>
347+
</form>
244348

245349
</div>
246350
</div>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { describe, expect, it } from "vitest";
3+
import FormField from "./FormField";
4+
5+
describe("FormField", () => {
6+
it("renders label and input", () => {
7+
render(<FormField label="Amount" name="amount" value="" onChange={() => {}} />);
8+
9+
expect(screen.getByText("Amount")).toBeInTheDocument();
10+
expect(screen.getByRole("textbox", { name: "Amount" })).toBeInTheDocument();
11+
});
12+
13+
it("shows error and aria-invalid when error is provided", () => {
14+
render(
15+
<FormField
16+
label="Amount"
17+
name="amount"
18+
value=""
19+
onChange={() => {}}
20+
error="Amount is required."
21+
/>,
22+
);
23+
24+
const input = screen.getByLabelText("Amount");
25+
const error = screen.getByText("Amount is required.");
26+
27+
expect(input).toHaveAttribute("aria-invalid", "true");
28+
expect(input).toHaveAttribute("aria-describedby", "amount-error");
29+
expect(error).toHaveAttribute("id", "amount-error");
30+
});
31+
32+
it("does not render an error element when there is no error", () => {
33+
render(<FormField label="Amount" name="amount" value="" onChange={() => {}} />);
34+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
35+
});
36+
});

0 commit comments

Comments
 (0)