Skip to content

Commit 40b8b37

Browse files
committed
feat: TVL ticker, USDC approval wizard, yield chart, tx date range filter
1 parent 67be593 commit 40b8b37

9 files changed

Lines changed: 673 additions & 8 deletions

File tree

frontend/src/components/Navbar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NavLink } from "react-router-dom";
33
import WalletConnect from "./WalletConnect";
44
import type { DisconnectReason } from "./WalletConnect";
55
import ThemeToggle from "./ThemeToggle";
6+
import TvlTicker from "./TvlTicker";
67
import { Layers } from "./icons";
78
import { useTranslation } from "../i18n";
89
import { networkConfig } from "../config/network";
@@ -154,6 +155,7 @@ const Navbar: FC<NavbarProps> = ({
154155
</div>
155156

156157
<div className="flex items-center gap-md">
158+
<TvlTicker />
157159
{walletAddress ? (
158160
<span
159161
aria-label="Network badge"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from "react";
2+
import { Activity, AlertCircle } from "./icons";
3+
import { useTvlTicker } from "../hooks/useTvlTicker";
4+
import { formatCurrency } from "../lib/formatters";
5+
6+
/**
7+
* Live TVL ticker for the dashboard header / navbar.
8+
* Polls every 15 s, animates number changes, shows a stale indicator
9+
* when the last successful poll is older than 60 s.
10+
*/
11+
const TvlTicker: React.FC = () => {
12+
const { displayTvl, isStale } = useTvlTicker();
13+
14+
return (
15+
<div
16+
aria-live="polite"
17+
aria-label={`Total Value Locked: ${formatCurrency(displayTvl, "USD", 0)}`}
18+
style={{
19+
display: "flex",
20+
alignItems: "center",
21+
gap: "6px",
22+
padding: "6px 12px",
23+
borderRadius: "999px",
24+
border: isStale
25+
? "1px solid rgba(255, 159, 10, 0.45)"
26+
: "1px solid rgba(0, 240, 255, 0.25)",
27+
background: isStale
28+
? "rgba(255, 159, 10, 0.08)"
29+
: "rgba(0, 240, 255, 0.06)",
30+
fontSize: "0.78rem",
31+
fontWeight: 600,
32+
letterSpacing: "0.02em",
33+
transition: "border-color 0.3s, background 0.3s",
34+
whiteSpace: "nowrap",
35+
}}
36+
>
37+
{isStale ? (
38+
<AlertCircle
39+
size={12}
40+
color="rgba(255, 159, 10, 0.9)"
41+
aria-label="Data may be stale"
42+
/>
43+
) : (
44+
<Activity
45+
size={12}
46+
color="var(--accent-cyan)"
47+
style={{ animation: "pulse 2s ease-in-out infinite" }}
48+
aria-hidden="true"
49+
/>
50+
)}
51+
<span style={{ color: "var(--text-secondary)" }}>TVL</span>
52+
<span
53+
style={{
54+
color: isStale ? "rgba(255, 159, 10, 0.9)" : "var(--accent-cyan)",
55+
fontFamily: "var(--font-display)",
56+
transition: "color 0.3s",
57+
}}
58+
>
59+
{formatCurrency(displayTvl, "USD", 0)}
60+
</span>
61+
{isStale && (
62+
<span
63+
style={{ color: "rgba(255, 159, 10, 0.7)", fontSize: "0.7rem" }}
64+
title="Data may be outdated"
65+
>
66+
stale
67+
</span>
68+
)}
69+
</div>
70+
);
71+
};
72+
73+
export default TvlTicker;

frontend/src/components/VaultDashboard.tsx

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
22
import {
33
Activity,
44
AlertCircle,
5+
Check,
56
ShieldCheck,
67
TrendingUp,
78
Wallet as WalletIcon,
@@ -17,6 +18,7 @@ import { useToast } from "../context/ToastContext";
1718
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs";
1819
import { FormField } from "../forms";
1920
import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
21+
import { useTokenAllowance } from "../hooks/useTokenAllowance";
2022
import CopyButton from "./CopyButton";
2123
import { copyTextToClipboard } from "../lib/clipboard";
2224

@@ -153,6 +155,14 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
153155

154156
const depositMutation = useDepositMutation();
155157
const withdrawMutation = useWithdrawMutation();
158+
const { approvalStatus, needsApproval, approve, resetApproval } =
159+
useTokenAllowance(walletAddress);
160+
161+
// Reset approval when deposit amount changes
162+
useEffect(() => {
163+
resetApproval();
164+
// eslint-disable-next-line react-hooks/exhaustive-deps
165+
}, [amount]);
156166

157167
useEffect(() => {
158168
const handleTrigger = () => {
@@ -632,6 +642,131 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
632642
</div>
633643
</div>
634644

645+
{/* Approval wizard — only shown for deposit tab when allowance is insufficient */}
646+
{tab === "deposit" && isValidAmount && needsApproval(enteredAmount) && (
647+
<div
648+
className="glass-panel"
649+
style={{
650+
padding: "14px 16px",
651+
marginBottom: "12px",
652+
border: approvalStatus === "confirmed"
653+
? "1px solid rgba(0, 240, 255, 0.4)"
654+
: "1px solid rgba(255, 159, 10, 0.4)",
655+
background: approvalStatus === "confirmed"
656+
? "rgba(0, 240, 255, 0.05)"
657+
: "rgba(255, 159, 10, 0.05)",
658+
}}
659+
>
660+
{/* Step indicators */}
661+
<div className="flex items-center gap-sm" style={{ marginBottom: "10px" }}>
662+
{/* Step 1 */}
663+
<div
664+
className="flex items-center gap-xs"
665+
style={{
666+
fontSize: "0.78rem",
667+
fontWeight: 600,
668+
color: approvalStatus === "confirmed"
669+
? "var(--accent-cyan)"
670+
: "rgba(255, 159, 10, 0.9)",
671+
}}
672+
>
673+
<div
674+
style={{
675+
width: "20px",
676+
height: "20px",
677+
borderRadius: "50%",
678+
display: "flex",
679+
alignItems: "center",
680+
justifyContent: "center",
681+
background: approvalStatus === "confirmed"
682+
? "var(--accent-cyan)"
683+
: "rgba(255, 159, 10, 0.2)",
684+
border: approvalStatus === "confirmed"
685+
? "none"
686+
: "1px solid rgba(255, 159, 10, 0.6)",
687+
fontSize: "0.7rem",
688+
color: approvalStatus === "confirmed" ? "#000" : "rgba(255, 159, 10, 0.9)",
689+
}}
690+
>
691+
{approvalStatus === "confirmed" ? <Check size={12} /> : "1"}
692+
</div>
693+
Approve USDC
694+
</div>
695+
<div style={{ flex: 1, height: "1px", background: "var(--border-glass)" }} />
696+
{/* Step 2 */}
697+
<div
698+
className="flex items-center gap-xs"
699+
style={{
700+
fontSize: "0.78rem",
701+
fontWeight: 600,
702+
color: approvalStatus === "confirmed"
703+
? "var(--text-primary)"
704+
: "var(--text-secondary)",
705+
}}
706+
>
707+
<div
708+
style={{
709+
width: "20px",
710+
height: "20px",
711+
borderRadius: "50%",
712+
display: "flex",
713+
alignItems: "center",
714+
justifyContent: "center",
715+
background: "rgba(255,255,255,0.05)",
716+
border: "1px solid var(--border-glass)",
717+
fontSize: "0.7rem",
718+
}}
719+
>
720+
2
721+
</div>
722+
Deposit
723+
</div>
724+
</div>
725+
726+
{approvalStatus !== "confirmed" && (
727+
<p style={{ fontSize: "0.82rem", color: "var(--text-secondary)", marginBottom: "10px" }}>
728+
You need to approve the vault contract to spend{" "}
729+
<strong style={{ color: "var(--text-primary)" }}>
730+
{enteredAmount.toFixed(2)} USDC
731+
</strong>{" "}
732+
before depositing.
733+
</p>
734+
)}
735+
736+
{approvalStatus === "error" && (
737+
<p style={{ fontSize: "0.82rem", color: "var(--text-error)", marginBottom: "10px" }}>
738+
Approval failed. Please try again.
739+
</p>
740+
)}
741+
742+
{approvalStatus !== "confirmed" && (
743+
<button
744+
type="button"
745+
className="btn btn-outline"
746+
style={{ width: "100%", padding: "10px", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px" }}
747+
disabled={approvalStatus === "pending" || !walletAddress}
748+
onClick={async () => {
749+
try {
750+
await approve(enteredAmount);
751+
toast.success({ title: "USDC Approved", description: "You can now proceed with your deposit." });
752+
} catch {
753+
toast.error({ title: "Approval Failed", description: "Could not approve USDC. Please try again." });
754+
}
755+
}}
756+
>
757+
{approvalStatus === "pending" ? (
758+
<>
759+
<Loader2 size={14} style={{ animation: "spin 0.9s linear infinite" }} />
760+
Approving...
761+
</>
762+
) : (
763+
"Approve USDC"
764+
)}
765+
</button>
766+
)}
767+
</div>
768+
)}
769+
635770
<button
636771
className="btn btn-primary"
637772
style={{
@@ -643,7 +778,10 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
643778
gap: "8px",
644779
}}
645780
type="submit"
646-
disabled={isSubmitDisabled}
781+
disabled={
782+
isSubmitDisabled ||
783+
(tab === "deposit" && isValidAmount && needsApproval(enteredAmount) && approvalStatus !== "confirmed")
784+
}
647785
>
648786
{isProcessing === tab ? (
649787
<>
@@ -655,7 +793,7 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
655793
Waiting for confirmation...
656794
</>
657795
) : tab === "deposit" ? (
658-
isCapReached ? "Vault is full" : "Approve & Deposit"
796+
isCapReached ? "Vault is full" : "Deposit"
659797
) : (
660798
"Withdraw Funds"
661799
)}

0 commit comments

Comments
 (0)