From 6ba3223af3bd66c037b408af9a0ad2a52e4c99a9 Mon Sep 17 00:00:00 2001 From: Esla kagbu Date: Fri, 20 Feb 2026 15:00:35 +0100 Subject: [PATCH 1/6] feat: implement dashboard wallet integration and real-time balances --- frontend/src/app/dashboard/page.tsx | 28 +++ frontend/src/components/ConnectWallet.tsx | 197 ++++++++++++++---- frontend/src/components/DashboardHeader.tsx | 41 ++++ frontend/src/components/Navigation.tsx | 14 +- .../components/hooks/useStellarBalances.ts | 113 ++++++++++ frontend/src/components/hooks/useWallet.ts | 68 ++++++ 6 files changed, 417 insertions(+), 44 deletions(-) create mode 100644 frontend/src/app/dashboard/page.tsx create mode 100644 frontend/src/components/DashboardHeader.tsx create mode 100644 frontend/src/components/hooks/useStellarBalances.ts create mode 100644 frontend/src/components/hooks/useWallet.ts diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..3cd8f1b --- /dev/null +++ b/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,28 @@ +import DashboardHeader from "@/components/DashboardHeader"; +import { PoolGrid } from "@/components/PoolGrid"; + +export default function DashboardPage() { + return ( +
+ + +
+ {/* Page heading */} +
+

Dashboard

+

+ Manage your donation pools and track contributions. +

+
+ + {/* Pools */} +
+

+ Active Pools +

+ +
+
+
+ ); +} diff --git a/frontend/src/components/ConnectWallet.tsx b/frontend/src/components/ConnectWallet.tsx index 1c0ef27..a02daec 100644 --- a/frontend/src/components/ConnectWallet.tsx +++ b/frontend/src/components/ConnectWallet.tsx @@ -1,60 +1,177 @@ "use client"; -import { useEffect, useState } from "react"; -import { getPublicKey, connect, disconnect } from "../app/stellar-wallets-kit"; +import { Loader2, Wallet, LogOut, ChevronDown } from "lucide-react"; +import { useWallet } from "./hooks/useWallet"; +import { useStellarBalances } from "./hooks/useStellarBalances"; +import { cn } from "@/lib/utils"; +import { useState, useRef, useEffect } from "react"; + +/** Shorten a Stellar public key to "GABCD…WXYZ" format. */ +function truncateKey(key: string): string { + if (key.length <= 12) return key; + return `${key.slice(0, 4)}…${key.slice(-4)}`; +} export default function ConnectWallet() { - const [publicKey, setPublicKey] = useState(null); - const [loading, setLoading] = useState(true); - - async function showConnected() { - const key = await getPublicKey(); - if (key) { - setPublicKey(key); - } else { - setPublicKey(null); - } - setLoading(false); - } + const { publicKey, isConnected, isLoading, connect, disconnect } = + useWallet(); + const { balances, isLoading: balancesLoading } = + useStellarBalances(publicKey); - async function showDisconnected() { - setPublicKey(null); - setLoading(false); - } + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + // Close dropdown on outside click useEffect(() => { - (async () => { - const key = await getPublicKey(); - if (key) { - setPublicKey(key); + function handleClickOutside(e: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(e.target as Node) + ) { + setDropdownOpen(false); } - setLoading(false); - })(); + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); }, []); + if (isLoading) { + return ( +
+ + Connecting… +
+ ); + } + + if (!isConnected || !publicKey) { + return ( + + ); + } + + const xlm = balances.find((b) => b.asset === "XLM"); + const usdc = balances.find((b) => b.asset === "USDC"); + return ( -
- {!loading && publicKey && ( - <> -
- Signed in as {publicKey} +
+ {/* Trigger button */} + + + {/* Dropdown */} + {dropdownOpen && ( +
+ {/* Address */} +
+

Wallet Address

+

+ {publicKey} +

+
+ + {/* Balances */} +
+

+ Balances +

+ {balancesLoading ? ( +
+ + Fetching balances… +
+ ) : ( + <> + {xlm && ( + + )} + {usdc ? ( + + ) : ( +

No USDC trustline

+ )} + + )}
- - - )} - {!loading && !publicKey && ( - <> + {/* Disconnect */} - +
)}
); } + +function BalanceRow({ + label, + value, + color, +}: { + label: string; + value: string; + color: string; +}) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/frontend/src/components/DashboardHeader.tsx b/frontend/src/components/DashboardHeader.tsx new file mode 100644 index 0000000..e1f4f52 --- /dev/null +++ b/frontend/src/components/DashboardHeader.tsx @@ -0,0 +1,41 @@ +"use client"; + +import Link from "next/link"; +import { LayoutDashboard } from "lucide-react"; +import ConnectWallet from "./ConnectWallet"; + +/** + * Top bar for the dashboard page. + * + * Contains the Nevo brand mark on the left and the wallet status widget + * (ConnectWallet) on the right. ConnectWallet handles all connect / + * disconnect / balance display logic internally. + */ +export default function DashboardHeader() { + return ( +
+
+ {/* Brand + page context */} +
+ +
+ N +
+ Nevo + + + {/* Breadcrumb separator */} + / + +
+ + Dashboard +
+
+ + {/* Wallet widget */} + +
+
+ ); +} diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx index 804f2ef..17185db 100644 --- a/frontend/src/components/Navigation.tsx +++ b/frontend/src/components/Navigation.tsx @@ -55,9 +55,12 @@ export default function Navigation() { ) )} - +
@@ -100,9 +103,12 @@ export default function Navigation() { ) )} - +
)} diff --git a/frontend/src/components/hooks/useStellarBalances.ts b/frontend/src/components/hooks/useStellarBalances.ts new file mode 100644 index 0000000..32f0f73 --- /dev/null +++ b/frontend/src/components/hooks/useStellarBalances.ts @@ -0,0 +1,113 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export interface StellarBalance { + asset: "XLM" | "USDC" | string; + balance: string; +} + +export interface StellarBalancesState { + balances: StellarBalance[]; + isLoading: boolean; + error: string | null; +} + +const HORIZON_URL = "https://horizon.stellar.org"; + +// USDC issuer on Stellar mainnet (Centre / Circle) +const USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"; + +/** + * Fetch XLM and USDC balances for a given Stellar public key from the + * Horizon REST API. No extra SDK is required – a plain `fetch` call is + * sufficient and keeps the bundle lean. + * + * Returns `isLoading: true` while the request is in-flight and re-fetches + * automatically whenever `publicKey` changes. + */ +export function useStellarBalances( + publicKey: string | null +): StellarBalancesState { + const [balances, setBalances] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!publicKey) { + setBalances([]); + setError(null); + setIsLoading(false); + return; + } + + let cancelled = false; + setIsLoading(true); + setError(null); + + (async () => { + try { + const res = await fetch( + `${HORIZON_URL}/accounts/${publicKey}` + ); + + if (!res.ok) { + if (res.status === 404) { + // Account not yet funded on the network + if (!cancelled) { + setBalances([{ asset: "XLM", balance: "0" }]); + setError(null); + } + return; + } + throw new Error(`Horizon error ${res.status}`); + } + + const data = await res.json(); + + const parsed: StellarBalance[] = ( + data.balances as Array<{ + asset_type: string; + asset_code?: string; + asset_issuer?: string; + balance: string; + }> + ) + .filter((b) => { + // Include native XLM always + if (b.asset_type === "native") return true; + // Include USDC (credit_alphanum4 / alphanum12) from the canonical issuer + if ( + b.asset_code === "USDC" && + b.asset_issuer === USDC_ISSUER + ) + return true; + return false; + }) + .map((b) => ({ + asset: b.asset_type === "native" ? "XLM" : (b.asset_code as string), + // Round to 4 decimal places for display + balance: parseFloat(b.balance).toFixed(4), + })); + + if (!cancelled) { + setBalances(parsed); + } + } catch (err) { + if (!cancelled) { + setError( + err instanceof Error ? err.message : "Failed to fetch balances" + ); + } + } finally { + if (!cancelled) setIsLoading(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [publicKey]); + + return { balances, isLoading, error }; +} diff --git a/frontend/src/components/hooks/useWallet.ts b/frontend/src/components/hooks/useWallet.ts new file mode 100644 index 0000000..e76561b --- /dev/null +++ b/frontend/src/components/hooks/useWallet.ts @@ -0,0 +1,68 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { + getPublicKey, + connect, + disconnect, +} from "@/app/stellar-wallets-kit"; + +export interface WalletState { + publicKey: string | null; + isConnected: boolean; + isLoading: boolean; + connect: () => Promise; + disconnect: () => Promise; +} + +/** + * Centralised wallet state hook. + * + * - Reads the persisted wallet selection from localStorage on mount. + * - Exposes `connect` / `disconnect` helpers that keep React state in sync. + * - All other components should consume this hook instead of calling the + * stellar-wallets-kit helpers directly. + */ +export function useWallet(): WalletState { + const [publicKey, setPublicKey] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Restore session on mount + useEffect(() => { + let cancelled = false; + (async () => { + try { + const key = await getPublicKey(); + if (!cancelled) setPublicKey(key); + } catch { + // Wallet not available or not connected – that's fine + } finally { + if (!cancelled) setIsLoading(false); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const handleConnect = useCallback(async () => { + await connect(async () => { + const key = await getPublicKey(); + setPublicKey(key); + }); + }, []); + + const handleDisconnect = useCallback(async () => { + await disconnect(async () => { + setPublicKey(null); + }); + }, []); + + return { + publicKey, + isConnected: publicKey !== null, + isLoading, + connect: handleConnect, + disconnect: handleDisconnect, + }; +} From 235bbac68f09f8f9543308957b9902ebb4548ee0 Mon Sep 17 00:00:00 2001 From: Esla kagbu Date: Fri, 20 Feb 2026 15:23:55 +0100 Subject: [PATCH 2/6] refactor: extract WalletDropdown and ensure ConnectWallet is under 150 lines --- frontend/src/components/ConnectWallet.tsx | 78 ++------------- frontend/src/components/WalletDropdown.tsx | 109 +++++++++++++++++++++ 2 files changed, 117 insertions(+), 70 deletions(-) create mode 100644 frontend/src/components/WalletDropdown.tsx diff --git a/frontend/src/components/ConnectWallet.tsx b/frontend/src/components/ConnectWallet.tsx index a02daec..d447aae 100644 --- a/frontend/src/components/ConnectWallet.tsx +++ b/frontend/src/components/ConnectWallet.tsx @@ -5,6 +5,7 @@ import { useWallet } from "./hooks/useWallet"; import { useStellarBalances } from "./hooks/useStellarBalances"; import { cn } from "@/lib/utils"; import { useState, useRef, useEffect } from "react"; +import WalletDropdown from "./WalletDropdown"; /** Shorten a Stellar public key to "GABCD…WXYZ" format. */ function truncateKey(key: string): string { @@ -101,77 +102,14 @@ export default function ConnectWallet() { {/* Dropdown */} {dropdownOpen && ( -
- {/* Address */} -
-

Wallet Address

-

- {publicKey} -

-
- - {/* Balances */} -
-

- Balances -

- {balancesLoading ? ( -
- - Fetching balances… -
- ) : ( - <> - {xlm && ( - - )} - {usdc ? ( - - ) : ( -

No USDC trustline

- )} - - )} -
- - {/* Disconnect */} - -
+ setDropdownOpen(false)} + /> )} ); } - -function BalanceRow({ - label, - value, - color, -}: { - label: string; - value: string; - color: string; -}) { - return ( -
- {label} - {value} -
- ); -} diff --git a/frontend/src/components/WalletDropdown.tsx b/frontend/src/components/WalletDropdown.tsx new file mode 100644 index 0000000..fe48b2a --- /dev/null +++ b/frontend/src/components/WalletDropdown.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { LogOut, Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface StellarBalance { + asset: string; + balance: string; +} + +interface WalletDropdownProps { + publicKey: string; + balances: StellarBalance[]; + balancesLoading: boolean; + onDisconnect: () => Promise; + onClose: () => void; +} + +/** + * Dropdown panel showing detailed wallet information and actions. + * Extracted from ConnectWallet to keep component sizes manageable. + */ +export default function WalletDropdown({ + publicKey, + balances, + balancesLoading, + onDisconnect, + onClose, +}: WalletDropdownProps) { + const xlm = balances.find((b) => b.asset === "XLM"); + const usdc = balances.find((b) => b.asset === "USDC"); + + return ( +
+ {/* Address section */} +
+

+ Wallet Address +

+

+ {publicKey} +

+
+ + {/* Balances section */} +
+

+ Asset Balances +

+ {balancesLoading ? ( +
+ + Updating balances… +
+ ) : ( +
+ {xlm && ( + + )} + {usdc ? ( + + ) : ( +

+ No USDC trustline found +

+ )} +
+ )} +
+ + {/* Action section */} + +
+ ); +} + +function BalanceRow({ + label, + value, + color, +}: { + label: string; + value: string; + color: string; +}) { + return ( +
+ {label} + {value} +
+ ); +} From e29bd59fa68f007a7d02f7635b76b9eca2bcc065 Mon Sep 17 00:00:00 2001 From: Esla kagbu Date: Fri, 20 Feb 2026 16:41:22 +0100 Subject: [PATCH 3/6] fix: resolve ESlint warnings --- frontend/src/app/contact-us/Page.tsx | 13 ++++++------- frontend/src/app/layout.tsx | 1 + frontend/src/components/ConnectWallet.tsx | 2 +- frontend/src/components/DonationModal.tsx | 7 +++---- frontend/src/components/FeaturesSection.tsx | 1 + frontend/src/components/HowItWorksSection.tsx | 1 + frontend/src/components/SecuritySection.tsx | 1 + 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/contact-us/Page.tsx b/frontend/src/app/contact-us/Page.tsx index 2630aab..6c5c75a 100644 --- a/frontend/src/app/contact-us/Page.tsx +++ b/frontend/src/app/contact-us/Page.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState } from "react"; +import React, { useState } from "react"; import { ArrowUpRight, ChevronDown } from "lucide-react"; import Navigation from "../../components/Navigation"; import Footer from "../../components/Footer"; @@ -27,7 +27,7 @@ export default function ContactPage() { setError(""); // Simulate successful submission - console.log({ fullName, subject, message, email }); + // console.log({ fullName, subject, message, email }); setSuccess(true); @@ -146,11 +146,10 @@ export default function ContactPage() { type="submit" disabled={!isFormValid} className={`flex items-center justify-center gap-2 px-8 py-3 rounded-t-lg rounded-b-[18px] font-semibold transition-all duration-300 - ${ - isFormValid - ? "bg-[#50C878] text-[#0F172A] cursor-pointer" - : "bg-[#50C878]/60 text-[#0F172A]/70 cursor-not-allowed" - }`} + ${isFormValid + ? "bg-[#50C878] text-[#0F172A] cursor-pointer" + : "bg-[#50C878]/60 text-[#0F172A]/70 cursor-not-allowed" + }`} > SEND MESSAGE diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 774189a..bbe0dc8 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,3 +1,4 @@ +import React from "react"; import type { Metadata } from "next"; // import { DM_Sans, Geist, Geist_Mono } from "next/font/google"; // import "./globals.css"; diff --git a/frontend/src/components/ConnectWallet.tsx b/frontend/src/components/ConnectWallet.tsx index d447aae..da1fa16 100644 --- a/frontend/src/components/ConnectWallet.tsx +++ b/frontend/src/components/ConnectWallet.tsx @@ -1,6 +1,6 @@ "use client"; -import { Loader2, Wallet, LogOut, ChevronDown } from "lucide-react"; +import { Loader2, Wallet, ChevronDown } from "lucide-react"; import { useWallet } from "./hooks/useWallet"; import { useStellarBalances } from "./hooks/useStellarBalances"; import { cn } from "@/lib/utils"; diff --git a/frontend/src/components/DonationModal.tsx b/frontend/src/components/DonationModal.tsx index fae09df..ead6b92 100644 --- a/frontend/src/components/DonationModal.tsx +++ b/frontend/src/components/DonationModal.tsx @@ -37,7 +37,7 @@ export const DonationModal: React.FC = ({ const handleDonate = () => { // Implement actual donation logic here later - console.log(`Donating ${amount} ${asset} to ${poolTitle}`); + // console.log(`Donating ${amount} ${asset} to ${poolTitle}`); onClose(); }; @@ -81,11 +81,10 @@ export const DonationModal: React.FC = ({