diff --git a/frontend/app/components/FAQ.tsx b/frontend/app/components/FAQ.tsx index 389077062..4ee70764d 100644 --- a/frontend/app/components/FAQ.tsx +++ b/frontend/app/components/FAQ.tsx @@ -1,7 +1,8 @@ -'use client'; +"use client"; import React, { useState, useRef, useEffect } from 'react'; import clsx from 'clsx'; +import { Button } from '@/app/components/ui/Button'; interface FAQItem { question: string; @@ -65,8 +66,10 @@ const FAQ: React.FC = () => { : "bg-white/[0.03] border-white/[0.08] hover:border-[rgba(0,212,192,0.3)]" )} > - +
{ } return ( - + ); }; @@ -170,11 +171,14 @@ const Navbar: React.FC = () => { + +
diff --git a/frontend/app/components/Newsletter.tsx b/frontend/app/components/Newsletter.tsx index 051efc97c..cd1d0d920 100644 --- a/frontend/app/components/Newsletter.tsx +++ b/frontend/app/components/Newsletter.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState } from "react"; +import { Button } from "./ui/Button"; const Newsletter: React.FC = () => { const [email, setEmail] = useState(""); @@ -8,8 +9,6 @@ const Newsletter: React.FC = () => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (email) { - // TODO: Implement newsletter subscription - // Send the email to your backend API setEmail(""); } }; @@ -29,9 +28,7 @@ const Newsletter: React.FC = () => { onSubmit={handleSubmit} >
- + { aria-describedby="newsletter-help" />
- + diff --git a/frontend/app/components/ThemeToggle.tsx b/frontend/app/components/ThemeToggle.tsx index 64bb90b50..a8cfb5fb3 100644 --- a/frontend/app/components/ThemeToggle.tsx +++ b/frontend/app/components/ThemeToggle.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import clsx from "clsx"; import { Check, ChevronDown, Monitor, Moon, Sun } from "lucide-react"; +import { Button } from '@/app/components/ui/Button'; import { type Theme, useTheme } from "../context/ThemeContext"; const themeOptions: Array<{ @@ -94,6 +95,7 @@ export default function ThemeToggle({ return (
+ +
+ ); +} diff --git a/frontend/app/components/dashboard/ContractDetailsCard.tsx b/frontend/app/components/dashboard/ContractDetailsCard.tsx index 896713d4a..453060e96 100644 --- a/frontend/app/components/dashboard/ContractDetailsCard.tsx +++ b/frontend/app/components/dashboard/ContractDetailsCard.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Copy, FileCode, Calendar } from "lucide-react"; +import { Button } from '@/app/components/ui/Button'; export type RiskLevel = "Low Risk" | "Medium Risk" | "High Risk"; @@ -108,17 +109,20 @@ const ContractDetailsCard: React.FC = ({ {truncateAddress(contract.contractId, 8)} - + {copied && (

Copied to clipboard

diff --git a/frontend/app/components/dashboard/FeaturedGoalCard.tsx b/frontend/app/components/dashboard/FeaturedGoalCard.tsx index 036d35884..c1616d688 100644 --- a/frontend/app/components/dashboard/FeaturedGoalCard.tsx +++ b/frontend/app/components/dashboard/FeaturedGoalCard.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Plane, Calendar, ChevronRight } from 'lucide-react'; +import { Button } from "../ui/Button"; import CircularProgress from './CircularProgress'; import Button from '../ui/Button'; @@ -83,6 +84,12 @@ const FeaturedGoalCard: React.FC = ({
+ + diff --git a/frontend/app/components/dashboard/GoalCard.tsx b/frontend/app/components/dashboard/GoalCard.tsx index 547a7bcfc..42823e9a3 100644 --- a/frontend/app/components/dashboard/GoalCard.tsx +++ b/frontend/app/components/dashboard/GoalCard.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Button } from '@/app/components/ui/Button'; import Link from "next/link"; import { Calendar, ChevronRight } from "lucide-react"; import Button from "../ui/Button"; @@ -112,6 +113,7 @@ export default function GoalCard({
+ diff --git a/frontend/app/components/dashboard/GoalOverviewCard.tsx b/frontend/app/components/dashboard/GoalOverviewCard.tsx index cde33b4be..c40fb6692 100644 --- a/frontend/app/components/dashboard/GoalOverviewCard.tsx +++ b/frontend/app/components/dashboard/GoalOverviewCard.tsx @@ -1,6 +1,7 @@ "use client"; import React from "react"; +import { Button } from "../ui/Button"; import { Shield, Calendar, @@ -220,6 +221,14 @@ const GoalOverviewCard: React.FC = ({ {/* ── Action buttons ── */}
+ +
- + {/* Modal Content - Scrollable */} @@ -252,29 +255,24 @@ const NetworkSwitchModal: React.FC = ({ {/* Modal Footer - Action Buttons */}
- - +
diff --git a/frontend/app/components/dashboard/PassedProposalCard.tsx b/frontend/app/components/dashboard/PassedProposalCard.tsx index b6dcb2b9b..31443bec2 100644 --- a/frontend/app/components/dashboard/PassedProposalCard.tsx +++ b/frontend/app/components/dashboard/PassedProposalCard.tsx @@ -1,6 +1,7 @@ "use client"; import React from "react"; +import { Button } from "../ui/Button"; import { CheckCircle2, ChevronRight } from "lucide-react"; import Button from "../ui/Button"; @@ -75,7 +76,7 @@ export default function PassedProposalCard({ proposal }: { proposal: PassedPropo
- @@ -129,10 +130,10 @@ export default function ProposalCard({ {/* Mobile-only full-width Vote button placed as the last row */}
diff --git a/frontend/app/components/dashboard/QuickActionsGrid.tsx b/frontend/app/components/dashboard/QuickActionsGrid.tsx index 410e94efd..6abee7935 100644 --- a/frontend/app/components/dashboard/QuickActionsGrid.tsx +++ b/frontend/app/components/dashboard/QuickActionsGrid.tsx @@ -2,6 +2,7 @@ import React from "react"; import { ArrowDownCircle, ArrowUpCircle, Repeat, Link } from "lucide-react"; +import { Button } from "../ui/Button"; const actions = [ { label: "Deposit", icon: ArrowDownCircle }, @@ -16,15 +17,17 @@ const QuickActionsGrid: React.FC = () => { {actions.map((a) => { const Icon = a.icon as React.ElementType; return ( - +
+ +
+
{a.label}
+ ); })}
diff --git a/frontend/app/components/dashboard/SavingsPoolCard.tsx b/frontend/app/components/dashboard/SavingsPoolCard.tsx index 5901fe903..1a05749e0 100644 --- a/frontend/app/components/dashboard/SavingsPoolCard.tsx +++ b/frontend/app/components/dashboard/SavingsPoolCard.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import Button from "../ui/Button"; +import { Button } from "../ui/Button"; export type RiskLevel = "Low Risk" | "Medium Risk" | "High Risk"; @@ -108,9 +108,10 @@ const SavingsPoolCard: React.FC = ({ {/* Deposit Button */} diff --git a/frontend/app/components/dashboard/Sidebar.tsx b/frontend/app/components/dashboard/Sidebar.tsx index 0a449f855..400e1f49b 100644 --- a/frontend/app/components/dashboard/Sidebar.tsx +++ b/frontend/app/components/dashboard/Sidebar.tsx @@ -21,6 +21,7 @@ import { Users, X, } from "lucide-react"; +import { Button } from '@/app/components/ui/Button'; const navLinks = [ { label: "Dashboard", href: "/dashboard", icon: Home }, @@ -74,13 +75,15 @@ const Sidebar: React.FC = () => { Nestera - +
diff --git a/frontend/app/hooks/useWalletCache.ts b/frontend/app/hooks/useWalletCache.ts new file mode 100644 index 000000000..518c8d566 --- /dev/null +++ b/frontend/app/hooks/useWalletCache.ts @@ -0,0 +1,94 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Horizon } from "@stellar/stellar-sdk"; +import { env } from "../lib/env"; + +interface Balance { + asset_code: string; + balance: string; + asset_type: string; + asset_issuer?: string; + usd_value: number; +} + +const COINGECKO_IDS: Record = { + XLM: "stellar", + USDC: "usd-coin", + AQUA: "aqua", +}; + +async function fetchPrices(): Promise> { + const ids = Object.values(COINGECKO_IDS).join(","); + const res = await fetch(`${env.coingeckoApi}/simple/price?ids=${ids}&vs_currencies=usd`); + if (!res.ok) throw new Error("Failed to fetch prices"); + const data = await res.json(); + const prices: Record = {}; + for (const [code, id] of Object.entries(COINGECKO_IDS)) { + prices[code] = data[id]?.usd ?? (code === "USDC" ? 1 : 0); + } + return prices; +} + +async function fetchBalances(address: string, horizonUrl: string): Promise { + const server = new Horizon.Server(horizonUrl); + const account = await server.loadAccount(address); + return account.balances.map((b: any) => ({ + asset_code: b.asset_type === "native" ? "XLM" : b.asset_code, + balance: b.balance, + asset_type: b.asset_type, + asset_issuer: b.asset_issuer, + usd_value: 0, // enriched below + })); +} + +/** Cached price data — refreshes every 5 minutes */ +export function usePrices() { + return useQuery({ + queryKey: ["prices"], + queryFn: fetchPrices, + staleTime: 5 * 60_000, // 5 minutes + gcTime: 10 * 60_000, + refetchInterval: 5 * 60_000, + }); +} + +/** Cached wallet balances — refreshes every 60 seconds, invalidated on disconnect */ +export function useWalletBalances( + address: string | null, + network: string | null, + horizonUrl: string, +) { + const { data: prices } = usePrices(); + + return useQuery({ + queryKey: ["balances", address], + queryFn: async () => { + if (!address) return []; + const rawBalances = await fetchBalances(address, horizonUrl); + let total = 0; + const enriched = rawBalances.map((b) => { + const price = prices?.[b.asset_code] ?? (b.asset_code === "USDC" ? 1 : 0); + const usdValue = parseFloat(b.balance) * price; + total += usdValue; + return { ...b, usd_value: usdValue }; + }); + return enriched; + }, + enabled: !!address, + staleTime: 30_000, // 30 seconds + gcTime: 5 * 60_000, + refetchOnWindowFocus: true, + refetchInterval: 60_000, // 1 minute + }); +} + +/** Call this to invalidate balance cache (e.g. after a transaction or on disconnect) */ +export function useInvalidateBalances() { + const queryClient = useQueryClient(); + return (address?: string | null) => { + if (address) { + queryClient.invalidateQueries({ queryKey: ["balances", address] }); + } else { + queryClient.invalidateQueries({ queryKey: ["balances"] }); + } + }; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 8335f405a..1882e1562 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -4,6 +4,8 @@ import type { Metadata } from "next"; import { ThemeProvider } from "./context/ThemeContext"; import { WalletProvider } from "./context/WalletContext"; import { ToastProvider } from "./context/ToastContext"; +import { WalletReconnectBanner } from "./components/WalletReconnectBanner"; +import { QueryProvider } from "./context/QueryProvider"; import QueryProvider from "./providers/QueryProvider"; import ErrorBoundary from "./components/ErrorBoundary"; import KeyboardShortcutsProvider from "./providers/KeyboardShortcutsProvider"; @@ -56,17 +58,16 @@ export default function RootLayout({ Skip to content - - + + - -
{children}
-
+ +
{children}
-
-
+ + ); diff --git a/frontend/app/savings/create-goal/components/CreateGoalForm.tsx b/frontend/app/savings/create-goal/components/CreateGoalForm.tsx index 4bfb978bd..b6ab6e2e9 100644 --- a/frontend/app/savings/create-goal/components/CreateGoalForm.tsx +++ b/frontend/app/savings/create-goal/components/CreateGoalForm.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { ChevronDown, X } from "lucide-react"; +import { Button } from "../../../components/ui/Button"; export default function CreateGoalForm() { const [formData, setFormData] = useState({ @@ -42,13 +43,9 @@ export default function CreateGoalForm() {

Create New Goal

- +
{/* Goal Name */} @@ -232,18 +229,12 @@ export default function CreateGoalForm() { {/* Footer Actions */}
- - + +
diff --git a/frontend/app/savings/loading.tsx b/frontend/app/savings/loading.tsx new file mode 100644 index 000000000..c86488cbd --- /dev/null +++ b/frontend/app/savings/loading.tsx @@ -0,0 +1,16 @@ +import { DashboardCardSkeleton, PoolCardSkeleton } from "../components/ui/LoadingState"; + +export default function GoalsLoading() { + return ( +
+
+ +
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+ ); +} diff --git a/frontend/app/savings/page.tsx b/frontend/app/savings/page.tsx index 394e63368..ad28aec64 100644 --- a/frontend/app/savings/page.tsx +++ b/frontend/app/savings/page.tsx @@ -19,6 +19,7 @@ import { Redo2, } from "lucide-react"; import GoalCard, { GoalStatus } from "./components/GoalCard"; +import { Button } from "../components/ui/Button"; import Button from "../components/ui/Button"; import { useUndoRedo } from "../hooks/useUndoRedo"; import { useToast } from "../context/ToastContext"; @@ -143,6 +144,7 @@ export default function GoalBasedSavingsPage() {

+ @@ -267,6 +269,19 @@ export default function GoalBasedSavingsPage() { + +
+ - +
{/* Undo / Redo */}
diff --git a/frontend/package.json b/frontend/package.json index f6118c76d..53f5748ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "dependencies": { "@stellar/freighter-api": "^3.1.0", "@stellar/stellar-sdk": "^15.0.1", + "@tanstack/query-sync-storage-persister": "5.80.2", + "@tanstack/react-query-persist-client": "5.80.2", "@tanstack/react-query": "^5.100.14", "clsx": "^2.1.1", "lucide-react": "^0.575.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fd39b646d..2a7f34aa7 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,15 @@ importers: '@stellar/stellar-sdk': specifier: ^15.0.1 version: 15.1.0 + '@tanstack/query-sync-storage-persister': + specifier: 5.80.2 + version: 5.80.2 + '@tanstack/react-query': + specifier: 5.80.2 + version: 5.80.2(react@19.2.3) + '@tanstack/react-query-persist-client': + specifier: 5.80.2 + version: 5.80.2(@tanstack/react-query@5.80.2(react@19.2.3))(react@19.2.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -593,6 +602,26 @@ packages: '@tailwindcss/postcss@4.3.0': resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} + '@tanstack/query-core@5.80.2': + resolution: {integrity: sha512-g2Es97uwFk7omkWiH9JmtLWSA8lTUFVseIyzqbjqJEEx7qN+Hg6jbBdDvelqtakamppaJtGORQ64hEJ5S6ojSg==} + + '@tanstack/query-persist-client-core@5.80.2': + resolution: {integrity: sha512-FAmLNf6PcKebtQ70wyESJoAAUdXBE1JUVdA56sVBhDKI2GIxemcD5jwgEaqFICBOVUltFYusU7FYkL1iePg8+Q==} + + '@tanstack/query-sync-storage-persister@5.80.2': + resolution: {integrity: sha512-+gsuRkVn8tpotXgnBteXQc/gM6pMsWAYuRW3PFnUoidV9wPAVG09SumW9DNGW8DBL537zwmGGbSsXckzanJndw==} + + '@tanstack/react-query-persist-client@5.80.2': + resolution: {integrity: sha512-PQGFGnVSfL4tdgi2Bb8y/Oacc1dEGQag/39tRcQTBICI8PVMIKPOv6KY1Ti1tJkl4lDl64gSV1ttelgOrk29MA==} + peerDependencies: + '@tanstack/react-query': ^5.80.2 + react: ^18 || ^19 + + '@tanstack/react-query@5.80.2': + resolution: {integrity: sha512-LfA0SVheJBOqC8RfJw/JbOW3yh2zuONQeWU5Prjm7yjUGUONeOedky1Bj39Cfj8MRdXrZV+DxNT7/DN/M907lQ==} + peerDependencies: + react: ^18 || ^19 + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -2699,6 +2728,28 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.3.0 + '@tanstack/query-core@5.80.2': {} + + '@tanstack/query-persist-client-core@5.80.2': + dependencies: + '@tanstack/query-core': 5.80.2 + + '@tanstack/query-sync-storage-persister@5.80.2': + dependencies: + '@tanstack/query-core': 5.80.2 + '@tanstack/query-persist-client-core': 5.80.2 + + '@tanstack/react-query-persist-client@5.80.2(@tanstack/react-query@5.80.2(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/query-persist-client-core': 5.80.2 + '@tanstack/react-query': 5.80.2(react@19.2.3) + react: 19.2.3 + + '@tanstack/react-query@5.80.2(react@19.2.3)': + dependencies: + '@tanstack/query-core': 5.80.2 + react: 19.2.3 + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1