diff --git a/frontend/components/dashboard/my-groups.tsx b/frontend/components/dashboard/my-groups.tsx
index fab96ae..e1ab30c 100644
--- a/frontend/components/dashboard/my-groups.tsx
+++ b/frontend/components/dashboard/my-groups.tsx
@@ -1,4 +1,9 @@
-import { useRouter, useSearchParams } from "next/navigation"
+"use client"
+
+import { Card } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Skeleton } from "@/components/ui/skeleton"
import {
Pagination,
PaginationContent,
@@ -6,6 +11,228 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
+import { Users, TrendingUp, Calendar, ArrowRight } from "lucide-react"
+import Link from "next/link"
+import { motion } from "framer-motion"
+import { useState, useEffect, useCallback } from "react"
+import { useRouter, useSearchParams } from "next/navigation"
+import { useStellar } from "@/components/web3-provider"
+import { usePoolData } from "@/lib/data-layer/PoolDataProvider"
+import {
+ stroopsToXlm,
+ RotationalPoolState,
+ TargetPoolState,
+ FlexiblePoolState,
+} from "@/hooks/useJointSaveContracts"
+import { EmptyState } from "@/components/dashboard/empty-state"
+import { FirstPoolTooltip } from "@/components/dashboard/first-pool-tooltip"
+
+const PAGE_SIZE = 6
+
+interface Pool {
+ id: string
+ name: string
+ type: "rotational" | "target" | "flexible"
+ status: "active" | "completed" | "paused"
+ members_count: number
+ total_saved: number
+ progress: number
+ frequency?: string
+ next_payout?: string
+ contract_address: string
+ target_amount: number | null
+ contribution_amount: number | null
+ minimum_deposit: number | null
+}
+
+interface MyGroupsProps {
+ onCreateClick?: () => void
+}
+
+const container = {
+ hidden: { opacity: 0 },
+ show: { opacity: 1, transition: { staggerChildren: 0.1 } },
+}
+const item = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } }
+
+// ── Skeleton for a single pool card ──────────────────────────────────────────
+function PoolCardSkeleton() {
+ return (
+
+ {/* header row */}
+
+
+ {/* stat rows */}
+
+ {[0, 1, 2].map((i) => (
+
+
+
+
+ ))}
+
+
+ {/* progress bar */}
+
+
+ {/* button */}
+
+
+ )
+}
+
+// ── Per-pool card that hydrates live balances from the unified cache ──────────
+function PoolCard({ pool }: { pool: Pool }) {
+ const cacheKey =
+ pool.contract_address && pool.contract_address !== "pending_deployment"
+ ? pool.contract_address
+ : pool.id
+ const { data, isLoading } = usePoolData(cacheKey)
+
+ const getLiveStats = (): {
+ totalSaved: number
+ progress: number
+ progressLabel: string
+ } => {
+ const onchain = data?.onchain ?? null
+ if (pool.type === "rotational" && onchain) {
+ const s = onchain as RotationalPoolState
+ const totalMembers = s.members.length || pool.members_count || 1
+ const progress = Math.min(
+ 100,
+ Math.round((s.currentRound / totalMembers) * 100)
+ )
+ const perRound = (pool.contribution_amount || 0) * totalMembers
+ const totalSaved = s.currentRound * perRound
+ return {
+ totalSaved,
+ progress,
+ progressLabel: `Round ${s.currentRound + 1} of ${totalMembers}`,
+ }
+ }
+ if (pool.type === "target" && onchain) {
+ const s = onchain as TargetPoolState
+ const saved = stroopsToXlm(s.totalDeposited)
+ const target = pool.target_amount || stroopsToXlm(s.targetAmount) || 1
+ const progress = Math.min(100, Math.round((saved / target) * 100))
+ return {
+ totalSaved: saved,
+ progress,
+ progressLabel: `${saved.toFixed(2)} / ${target.toFixed(2)} XLM`,
+ }
+ }
+ if (pool.type === "flexible" && onchain) {
+ const s = onchain as FlexiblePoolState
+ const totalSaved = stroopsToXlm(s.totalBalance)
+ const softGoal = (pool.minimum_deposit || 0) * (pool.members_count || 1)
+ const progress =
+ softGoal > 0
+ ? Math.min(100, Math.round((totalSaved / softGoal) * 100))
+ : s.isActive
+ ? 50
+ : 100
+ return {
+ totalSaved,
+ progress,
+ progressLabel:
+ softGoal > 0
+ ? `${totalSaved.toFixed(2)} / ${softGoal.toFixed(2)} XLM`
+ : `${totalSaved.toFixed(2)} XLM saved`,
+ }
+ }
+ return {
+ totalSaved: pool.total_saved ?? 0,
+ progress: pool.progress ?? 0,
+ progressLabel: "",
+ }
+ }
+
+ const { totalSaved, progress, progressLabel } = getLiveStats()
+ const formatXlm = (n: number) => `${n.toFixed(2)} XLM`
+
+ return (
+
+
+
+
+
{pool.name}
+
+ {pool.type.charAt(0).toUpperCase() + pool.type.slice(1)}
+
+
+
+ {pool.status}
+
+
+
+
+
+
+ Members
+
+ {pool.members_count}
+
+
+
+
+ Total Saved
+
+
+ {isLoading && !data?.onchain ? (
+
+ ) : (
+ formatXlm(totalSaved)
+ )}
+
+
+
+
+
+ {pool.type === "rotational" ? "Frequency" : "Status"}
+
+ {pool.frequency || pool.status}
+
+
+
+
+ Progress
+ {progress.toFixed(1)}%
+
+
+
+
+ {progressLabel && (
+
{progressLabel}
+ )}
+
+
+
+
+ )
+}
+
+// ── Main MyGroups component ───────────────────────────────────────────────────
export function MyGroups({ onCreateClick }: MyGroupsProps) {
const { address } = useStellar()
const router = useRouter()
@@ -19,11 +246,14 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
const page = Math.max(0, parseInt(searchParams.get("page") || "0", 10))
const totalPages = Math.ceil(total / PAGE_SIZE)
- const setPage = useCallback((p: number) => {
- const params = new URLSearchParams(searchParams.toString())
- params.set("page", String(p))
- router.push(`?${params.toString()}`, { scroll: false })
- }, [router, searchParams])
+ const setPage = useCallback(
+ (p: number) => {
+ const params = new URLSearchParams(searchParams.toString())
+ params.set("page", String(p))
+ router.push(`?${params.toString()}`, { scroll: false })
+ },
+ [router, searchParams]
+ )
useEffect(() => {
if (!address) {
@@ -37,16 +267,12 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
try {
setLoading(true)
setError("")
-
const res = await fetch(
`/api/pools?creator=${address?.toLowerCase()}&page=${currentPage}`
)
-
if (!res.ok) throw new Error("Failed to fetch pools")
-
const json = await res.json()
const data: Pool[] = Array.isArray(json) ? json : (json.data ?? [])
-
setPools(data)
setTotal(json.total ?? data.length)
} catch (err) {
@@ -57,19 +283,26 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
}
}
- if (loading)
+ if (loading) {
return (
My Groups
+
-
-
+
+ {Array.from({ length: PAGE_SIZE }).map((_, i) => (
+
+ ))}
)
+ }
- if (error)
+ if (error) {
return (
@@ -80,6 +313,7 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
)
+ }
return (
@@ -122,7 +356,6 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
Showing {page * PAGE_SIZE + 1}–
{Math.min((page + 1) * PAGE_SIZE, total)} of {total} pools
-
@@ -136,7 +369,6 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
}
/>
-
setPage(page + 1)}
@@ -156,4 +388,4 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
)}
)
-}
\ No newline at end of file
+}
diff --git a/frontend/components/dashboard/profile.tsx b/frontend/components/dashboard/profile.tsx
index 706eb81..2702f47 100644
--- a/frontend/components/dashboard/profile.tsx
+++ b/frontend/components/dashboard/profile.tsx
@@ -2,8 +2,9 @@
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
+import { Skeleton } from "@/components/ui/skeleton"
import { useStellar } from "@/components/web3-provider"
-import { Wallet, Award, TrendingUp, Users, Loader2 } from "lucide-react"
+import { Wallet, Award, TrendingUp, Users } from "lucide-react"
import { useState, useEffect } from "react"
import { supabase } from "@/lib/supabase"
import {
@@ -136,7 +137,7 @@ export function Profile() {
Reputation Score
{loading ? (
-
+
) : (
{stats?.reputation ?? 50}%
)}
@@ -161,8 +162,16 @@ export function Profile() {
Savings Statistics
{loading ? (
-
-
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
) : (
@@ -203,8 +212,13 @@ export function Profile() {
Reputation Breakdown
{loading ? (
-
-
+
+ {[0, 1, 2].map((i) => (
+
+
+
+
+ ))}
) : (
diff --git a/frontend/components/group/group-actions.tsx b/frontend/components/group/group-actions.tsx
index 0a9fa89..85e0a99 100644
--- a/frontend/components/group/group-actions.tsx
+++ b/frontend/components/group/group-actions.tsx
@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { Skeleton } from "@/components/ui/skeleton";
import {
Loader2,
ArrowUpRight,
@@ -435,6 +436,34 @@ export function GroupActions({
Quick Actions {renderPendingBadge()}
+ {/* Skeleton while pool metadata is loading */}
+ {!poolData && !isPending && (
+
+ {/* deposit field + button */}
+
+
+
+
+
+
+ {/* withdraw field + button */}
+
+
+
+
+
+
+ {/* admin controls */}
+
+
+ )}
+
{error && (
@@ -449,6 +478,8 @@ export function GroupActions({
)}
+ {/* Actual form — shown once pool metadata resolves (or contract is pending) */}
+ {(poolData || isPending) && (
{/* Deposit / Contribute */}
@@ -690,6 +721,7 @@ export function GroupActions({
+ )}