From dd7b61e4dc1064032d0cdd204a3edb29cb4ba2b0 Mon Sep 17 00:00:00 2001 From: Ishant5436 Date: Fri, 3 Jul 2026 14:39:34 +0530 Subject: [PATCH 1/2] feat: Add wallet connect button (#8) --- src/app/layout.tsx | 8 ++- src/components/ui/wallet-button.tsx | 101 ++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/wallet-button.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b0c3f22..2131416 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { Providers } from "@/components/providers"; import { Sidebar } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/toaster"; +import { WalletButton } from "@/components/ui/wallet-button"; const inter = Inter({ subsets: ["latin"] }); @@ -21,7 +22,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
{children}
+
+
+ +
+
{children}
+
diff --git a/src/components/ui/wallet-button.tsx b/src/components/ui/wallet-button.tsx new file mode 100644 index 0000000..6b2d539 --- /dev/null +++ b/src/components/ui/wallet-button.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useWallet } from "@/hooks/use-wallet"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { Copy, ExternalLink, LogOut, Loader2 } from "lucide-react"; +import { useState } from "react"; + +export function WalletButton() { + const { address, isConnecting, connect, disconnect } = useWallet(); + const [copied, setCopied] = useState(false); + + const formatAddress = (addr: string) => { + return `${addr.slice(0, 4)}...${addr.slice(-4)}`; + }; + + const copyToClipboard = async () => { + if (!address) return; + try { + await navigator.clipboard.writeText(address); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy address", err); + } + }; + + if (isConnecting) { + return ( + + ); + } + + if (!address) { + return ( + + ); + } + + return ( + + + + + + + +
+ {address} + +
+ + + + + View on Stellar Explorer + + + + + + + + Disconnect + +
+
+
+ ); +} From 3dabfcadf8d9c10a906a32cbe0d3a5db9e77452d Mon Sep 17 00:00:00 2001 From: Ishant5436 Date: Fri, 3 Jul 2026 16:22:53 +0530 Subject: [PATCH 2/2] feat: Build individual group detail page (#9) --- src/app/groups/[id]/page.tsx | 324 +++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 src/app/groups/[id]/page.tsx diff --git a/src/app/groups/[id]/page.tsx b/src/app/groups/[id]/page.tsx new file mode 100644 index 0000000..88b8853 --- /dev/null +++ b/src/app/groups/[id]/page.tsx @@ -0,0 +1,324 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import * as Tabs from "@radix-ui/react-tabs"; +import { Copy, Plus, ExternalLink, ShieldCheck, Users, Banknote, CreditCard, Activity } from "lucide-react"; +import { formatAmount, shortenAddress, server } from "@/lib/stellar"; +import { LoanCard } from "@/components/loans/loan-card"; +import { ProposalCard } from "@/components/governance/proposal-card"; +import type { Group, Member, Contribution, Loan, Proposal } from "@/types"; +import { useWallet } from "@/hooks/use-wallet"; + +// Mock Data +const MOCK_GROUP: Group = { + id: "g1", + name: "Nairobi Tech Cooperative", + description: "A savings group for developers in Nairobi to pool funds and invest.", + admin: "GBX73XGZ4B36O3O5G753W3U5H3P42S54F3L7YGBZ2V2XQWZ6LKT22XOY", + members: [], + totalContributions: 15000, + balance: 0, + isActive: true, + createdAt: "2024-01-01T00:00:00Z", + rules: { minContribution: 100, contributionPeriodDays: 30, maxLoanMultiplier: 3, loanInterestBps: 500, votingQuorum: 50, votingPeriodDays: 7, latePenaltyBps: 100 }, + contractAddresses: { + treasury: "CA3Z2HGB24KXXB2FUXH3P42S54F3L7YGBZ2V2XQWZ6LKT22XOYABCDEF", + loan: "", voting: "", governance: "", dividend: "" + } +}; + +const MOCK_MEMBERS: Member[] = [ + { address: "GBX73XGZ4B36O3O5G753W3U5H3P42S54F3L7YGBZ2V2XQWZ6LKT22XOY", displayName: "Alice (Admin)", totalContributed: 5000, joinedAt: "2024-01-01T00:00:00Z", isActive: true, loanBalance: 0 }, + { address: "GCTXYZABCDEF1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567", displayName: "Bob Builder", totalContributed: 4000, joinedAt: "2024-01-15T00:00:00Z", isActive: true, loanBalance: 1500 }, + { address: "GDABCD1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123", displayName: "Charlie P", totalContributed: 6000, joinedAt: "2024-02-01T00:00:00Z", isActive: true, loanBalance: 0 }, +]; + +const MOCK_CONTRIBUTIONS: Contribution[] = [ + { id: "c1", member: "GBX73XGZ4B36O3O5G753W3U5H3P42S54F3L7YGBZ2V2XQWZ6LKT22XOY", amount: 1000, period: 1, timestamp: "2024-01-10T00:00:00Z", txHash: "abc123hash" }, + { id: "c2", member: "GCTXYZABCDEF1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567", amount: 1000, period: 1, timestamp: "2024-01-11T00:00:00Z", txHash: "def456hash" }, +]; + +const MOCK_LOANS: Loan[] = [ + { id: 1, borrower: "GCTXYZABCDEF1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567", amount: 2000, interestBps: 500, repaymentDue: "2024-12-31T00:00:00Z", amountRepaid: 500, status: "Approved", purpose: "New laptop", requestedAt: "2024-03-01T00:00:00Z", approvedAt: "2024-03-05T00:00:00Z" } +]; + +const MOCK_PROPOSALS: Proposal[] = [ + { id: 1, proposer: "GBX73XGZ4B36O3O5G753W3U5H3P42S54F3L7YGBZ2V2XQWZ6LKT22XOY", type: "AddMember", title: "Add Dave to Group", description: "Dave wants to join and agrees to rules", votesFor: 2, votesAgainst: 0, quorum: 2, deadline: "2024-07-01T00:00:00Z", status: "Active", createdAt: "2024-06-25T00:00:00Z" } +]; + +// Fetchers +async function fetchGroup(id: string): Promise { + const url = process.env.NEXT_PUBLIC_API_URL ? `${process.env.NEXT_PUBLIC_API_URL}/api/groups/${id}` : ""; + if (!url) return Promise.resolve(MOCK_GROUP); + try { + const res = await fetch(url); + if (!res.ok) throw new Error("Not found"); + return res.json(); + } catch { + return Promise.resolve(MOCK_GROUP); + } +} + +async function fetchMembers(id: string): Promise { + return Promise.resolve(MOCK_MEMBERS); +} + +async function fetchContributions(id: string): Promise { + return Promise.resolve(MOCK_CONTRIBUTIONS); +} + +async function fetchLoans(id: string): Promise { + return Promise.resolve(MOCK_LOANS); +} + +async function fetchProposals(id: string): Promise { + return Promise.resolve(MOCK_PROPOSALS); +} + +export default function GroupDetailPage({ params }: { params: { id: string } }) { + const { id } = params; + const { address } = useWallet(); + const [liveTreasury, setLiveTreasury] = useState(null); + + const { data: group, isLoading: groupLoading, isError } = useQuery({ queryKey: ["group", id], queryFn: () => fetchGroup(id) }); + const { data: members = [] } = useQuery({ queryKey: ["members", id], queryFn: () => fetchMembers(id) }); + const { data: contributions = [] } = useQuery({ queryKey: ["contributions", id], queryFn: () => fetchContributions(id) }); + const { data: loans = [] } = useQuery({ queryKey: ["loans", id], queryFn: () => fetchLoans(id) }); + const { data: proposals = [] } = useQuery({ queryKey: ["proposals", id], queryFn: () => fetchProposals(id) }); + + useEffect(() => { + async function fetchTreasury() { + if (!group?.contractAddresses?.treasury) return; + try { + const horizonRes = await fetch(`https://horizon-testnet.stellar.org/accounts/${group.contractAddresses.treasury}`); + if (horizonRes.ok) { + const data = await horizonRes.json(); + const nativeBal = data.balances?.find((b: any) => b.asset_type === "native"); + if (nativeBal) { + setLiveTreasury(Number(nativeBal.balance)); + } else { + setLiveTreasury(0); + } + } else { + setLiveTreasury(0); + } + } catch (err) { + console.error("Failed to fetch live treasury:", err); + setLiveTreasury(0); + } + } + fetchTreasury(); + }, [group?.contractAddresses?.treasury]); + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + if (groupLoading) { + return
Loading group details...
; + } + + if (isError || !group) { + return ( +
+

Group Not Found

+

The group you are looking for does not exist.

+
+ ); + } + + const isAdmin = address === group.admin; + const activeLoansCount = loans.filter((l) => l.status === "Approved").length; + + return ( +
+ {/* HEADER */} +
+
+
+

{group.name}

+ {group.isActive ? ( + Active + ) : ( + Inactive + )} +
+

{group.description}

+
+ + Admin: + {shortenAddress(group.admin, 6)} + +
+
+ {isAdmin && ( + + )} +
+ + {/* STATS ROW */} +
+
+
+ +
+
+

Treasury Balance

+

+ {liveTreasury !== null ? `${liveTreasury.toLocaleString()} XLM` : "Loading..."} +

+
+
+ +
+
+ +
+
+

Total Members

+

{members.length}

+
+
+ +
+
+ +
+
+

Active Loans

+

{activeLoansCount}

+
+
+ +
+
+ +
+
+

Total Contributions

+

${group.totalContributions.toLocaleString()}

+
+
+
+ + {/* TABS */} + + + + Members + + + Contributions + + + Loans + + + Governance + + + + +
+ + + + + + + + + + + {members.map((m) => ( + + + + + + + ))} + +
AddressDisplay NameTotal ContributedJoin Date
+ {shortenAddress(m.address, 6)} + {m.displayName || "-"}${m.totalContributed.toLocaleString()}{new Date(m.joinedAt).toLocaleDateString()}
+ {members.length === 0 &&
No members found.
} +
+
+ + +
+ + + + + + + + + + + + {contributions.map((c) => ( + + + + + + + + ))} + +
MemberAmountPeriodDateTransaction
{shortenAddress(c.member, 6)}${c.amount.toLocaleString()}#{c.period}{new Date(c.timestamp).toLocaleDateString()} + + {c.txHash.slice(0, 8)}... + +
+ {contributions.length === 0 &&
No contributions found.
} +
+
+ + +
+ {loans.map((l) => ( + + ))} +
+ {loans.length === 0 &&
No active loans found.
} +
+ + +
+ {proposals.map((p) => ( + + ))} +
+ {proposals.length === 0 &&
No proposals found.
} +
+
+
+ ); +}