diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b0c3f22..3263e75 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,31 +1,35 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; -import { Providers } from "@/components/providers"; -import { Sidebar } from "@/components/ui/sidebar"; -import { Toaster } from "@/components/ui/toaster"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "CoopFinance — Cooperative Finance on Stellar", - description: - "Open-source platform for savings groups, cooperatives, and rotating-credit associations built on Stellar.", - keywords: ["cooperative", "savings", "Stellar", "USDC", "Africa", "SACCO", "Ajo", "Esusu"], -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - -
- -
{children}
-
- -
- - - ); -} +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { Providers } from "@/components/providers"; +import { Sidebar } from "@/components/ui/sidebar"; +import { Header } from "@/components/ui/header"; +import { Toaster } from "@/components/ui/toaster"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "CoopFinance — Cooperative Finance on Stellar", + description: + "Open-source platform for savings groups, cooperatives, and rotating-credit associations built on Stellar.", + keywords: ["cooperative", "savings", "Stellar", "USDC", "Africa", "SACCO", "Ajo", "Esusu"], +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
+ +
+
+
{children}
+
+
+ +
+ + + ); +} diff --git a/src/components/providers.tsx b/src/components/providers.tsx index 9bba174..cfd1da1 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -1,20 +1,21 @@ -"use client"; - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useState } from "react"; - -export function Providers({ children }: { children: React.ReactNode }) { - const [queryClient] = useState( - () => new QueryClient({ - defaultOptions: { - queries: { staleTime: 30_000, retry: 2 }, - }, - }) - ); - - return ( - - {children} - - ); -} +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState } from "react"; +import { WalletProvider } from "@/hooks/use-wallet"; + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => new QueryClient({ + defaultOptions: { + queries: { staleTime: 30_000, retry: 2 }, + }, + }) + ); + + return ( + + {children} + + ); +} diff --git a/src/components/ui/header.tsx b/src/components/ui/header.tsx new file mode 100644 index 0000000..fccc4a8 --- /dev/null +++ b/src/components/ui/header.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { WalletButton } from "@/components/ui/wallet-button"; + +export function Header() { + return ( +
+ +
+ ); +} diff --git a/src/components/ui/wallet-button.tsx b/src/components/ui/wallet-button.tsx new file mode 100644 index 0000000..92dd58a --- /dev/null +++ b/src/components/ui/wallet-button.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Copy, ExternalLink, Loader2, Wallet } from "lucide-react"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { clsx } from "clsx"; +import { useWallet } from "@/hooks/use-wallet"; + +function shortenAddress(address: string): string { + if (address.length <= 10) return address; + return `${address.slice(0, 4)}...${address.slice(-4)}`; +} + +export function WalletButton() { + const { address, isConnecting, connect, disconnect } = useWallet(); + const [copied, setCopied] = useState(false); + + if (isConnecting) { + return ( + + ); + } + + if (!address) { + return ( + + ); + } + + const explorerUrl = `https://stellar.expert/explorer/testnet/account/${address}`; + + const copyAddress = async () => { + await navigator.clipboard.writeText(address); + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + }; + + return ( + + + + + + +
+

Connected address

+
+

{address}

+ +
+ {copied &&

Copied!

} +
+ + + + + View on Stellar Explorer + + + disconnect()} + className={clsx( + "flex items-center px-2 py-1.5 text-sm text-red-600 rounded-md", + "outline-none cursor-pointer hover:bg-red-50", + )} + > + Disconnect + +
+
+
+ ); +} diff --git a/src/hooks/use-wallet.ts b/src/hooks/use-wallet.ts index f8530fe..5a6a279 100644 --- a/src/hooks/use-wallet.ts +++ b/src/hooks/use-wallet.ts @@ -1,55 +1,89 @@ -"use client"; - -import { useState, useCallback } from "react"; -import { - StellarWalletsKit, - WalletNetwork, - FREIGHTER_ID, - LOBSTR_ID, - xBullWalletId, -} from "@creit.tech/stellar-wallets-kit"; - -const kit = new StellarWalletsKit({ - network: WalletNetwork.TESTNET, - selectedWalletId: FREIGHTER_ID, - wallets: [FREIGHTER_ID, LOBSTR_ID, xBullWalletId], -}); - -export function useWallet() { - const [address, setAddress] = useState(null); - const [isConnecting, setIsConnecting] = useState(false); - const [error, setError] = useState(null); - - const connect = useCallback(async () => { - setIsConnecting(true); - setError(null); - try { - await kit.openModal({ - onWalletSelected: async (option) => { - kit.setWallet(option.id); - const { address: addr } = await kit.getAddress(); - setAddress(addr); - }, - }); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to connect wallet"); - } finally { - setIsConnecting(false); - } - }, []); - - const disconnect = useCallback(() => { - setAddress(null); - }, []); - - const signTransaction = useCallback(async (xdr: string) => { - if (!address) throw new Error("Wallet not connected"); - const { signedTxXdr } = await kit.signTransaction(xdr, { - address, - networkPassphrase: WalletNetwork.TESTNET, - }); - return signedTxXdr; - }, [address]); - - return { address, isConnecting, error, connect, disconnect, signTransaction }; -} +"use client"; + +import { + createContext, + useCallback, + useContext, + useState, + type ReactNode, +} from "react"; +import { + StellarWalletsKit, + WalletNetwork, + FREIGHTER_ID, + LOBSTR_ID, + xBullWalletId, +} from "@creit.tech/stellar-wallets-kit"; + +const kit = new StellarWalletsKit({ + network: WalletNetwork.TESTNET, + selectedWalletId: FREIGHTER_ID, + wallets: [FREIGHTER_ID, LOBSTR_ID, xBullWalletId], +}); + +type WalletContextValue = { + address: string | null; + isConnecting: boolean; + error: string | null; + connect: () => Promise; + disconnect: () => void; + signTransaction: (xdr: string) => Promise; +}; + +const WalletContext = createContext(null); + +export function WalletProvider({ children }: { children: ReactNode }) { + const [address, setAddress] = useState(null); + const [isConnecting, setIsConnecting] = useState(false); + const [error, setError] = useState(null); + + const connect = useCallback(async () => { + setIsConnecting(true); + setError(null); + try { + await kit.openModal({ + onWalletSelected: async (option) => { + kit.setWallet(option.id); + const { address: addr } = await kit.getAddress(); + setAddress(addr); + }, + }); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to connect wallet"); + } finally { + setIsConnecting(false); + } + }, []); + + const disconnect = useCallback(() => { + setAddress(null); + }, []); + + const signTransaction = useCallback( + async (xdr: string) => { + if (!address) throw new Error("Wallet not connected"); + const { signedTxXdr } = await kit.signTransaction(xdr, { + address, + networkPassphrase: WalletNetwork.TESTNET, + }); + return signedTxXdr; + }, + [address], + ); + + return ( + + {children} + + ); +} + +export function useWallet(): WalletContextValue { + const ctx = useContext(WalletContext); + if (!ctx) { + throw new Error("useWallet must be used within WalletProvider"); + } + return ctx; +}