diff --git a/src/app/groups/[id]/page.tsx b/src/app/groups/[id]/page.tsx index 02ab880..84e7968 100644 --- a/src/app/groups/[id]/page.tsx +++ b/src/app/groups/[id]/page.tsx @@ -1,50 +1,83 @@ "use client"; +import { useParams } from "next/navigation"; import { Navbar } from "@/components/Navbar"; import { MemberList } from "@/components/MemberList"; import { RoundProgress } from "@/components/RoundProgress"; import { ContributeModal } from "@/components/ContributeModal"; +import { useGroupPolling } from "@/hooks/useGroupPolling"; import { useState } from "react"; import { formatAmount, GroupStatus } from "@sorosave/sdk"; -// TODO: Fetch real data from contract -const MOCK_GROUP = { - id: 1, - name: "Lagos Savings Circle", - admin: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFG", - token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", - contributionAmount: 1000000000n, - cycleLength: 604800, - maxMembers: 5, - members: [ - "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFG", - "GEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJ", - "GIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMN", - ], - payoutOrder: [ - "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFG", - "GEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJ", - "GIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMN", - ], - currentRound: 1, - totalRounds: 3, - status: GroupStatus.Active, - createdAt: 1700000000, -}; - export default function GroupDetailPage() { + const params = useParams(); + const groupId = params.id as string; + const { group, isLoading, error, lastUpdated, isPolling, refresh } = + useGroupPolling(groupId); const [showContributeModal, setShowContributeModal] = useState(false); - const group = MOCK_GROUP; + + if (isLoading && !group) { + return ( + <> + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + ); + } + + if (error && !group) { + return ( + <> + +
+
+

Failed to load group

+

{error}

+ +
+
+ + ); + } + + if (!group) return null; return ( <>
-
-

{group.name}

-

- {formatAmount(group.contributionAmount)} tokens per cycle -

+
+
+

{group.name}

+

+ {formatAmount(group.contributionAmount)} tokens per cycle +

+
+ {error && ( +
+ + + + Using cached data — reconnecting... +
+ )}
@@ -52,8 +85,10 @@ export default function GroupDetailPage() { )} +
diff --git a/src/components/RoundProgress.tsx b/src/components/RoundProgress.tsx index 8105152..e2d1d69 100644 --- a/src/components/RoundProgress.tsx +++ b/src/components/RoundProgress.tsx @@ -5,6 +5,8 @@ interface RoundProgressProps { totalRounds: number; contributionsReceived: number; totalMembers: number; + isPolling?: boolean; + lastUpdated?: Date | null; } export function RoundProgress({ @@ -12,14 +14,28 @@ export function RoundProgress({ totalRounds, contributionsReceived, totalMembers, + isPolling = false, + lastUpdated = null, }: RoundProgressProps) { const roundProgress = totalRounds > 0 ? (currentRound / totalRounds) * 100 : 0; const contributionProgress = totalMembers > 0 ? (contributionsReceived / totalMembers) * 100 : 0; + const roundComplete = contributionsReceived >= totalMembers; return (
-

Progress

+
+

Progress

+ {isPolling && ( +
+ + + + + Live +
+ )} +
@@ -31,7 +47,7 @@ export function RoundProgress({
@@ -46,12 +62,25 @@ export function RoundProgress({
+ {roundComplete && ( +

+ All contributions received — payout ready +

+ )}
+ + {lastUpdated && ( +

+ Updated {lastUpdated.toLocaleTimeString()} +

+ )}
); } diff --git a/src/hooks/useGroupPolling.ts b/src/hooks/useGroupPolling.ts new file mode 100644 index 0000000..7f1b1ff --- /dev/null +++ b/src/hooks/useGroupPolling.ts @@ -0,0 +1,92 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { sorosaveClient } from "@/lib/sorosave"; +import { GroupStatus } from "@sorosave/sdk"; + +const POLL_INTERVAL = 10_000; + +export interface GroupData { + id: number; + name: string; + admin: string; + token: string; + contributionAmount: bigint; + cycleLength: number; + maxMembers: number; + members: string[]; + payoutOrder: string[]; + currentRound: number; + totalRounds: number; + status: GroupStatus; + createdAt: number; + contributionsReceived: number; +} + +interface UseGroupPollingResult { + group: GroupData | null; + isLoading: boolean; + error: string | null; + lastUpdated: Date | null; + isPolling: boolean; + refresh: () => Promise; +} + +export function useGroupPolling(groupId: string): UseGroupPollingResult { + const [group, setGroup] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdated, setLastUpdated] = useState(null); + const [isPolling, setIsPolling] = useState(false); + const intervalRef = useRef | null>(null); + const mountedRef = useRef(true); + + const fetchGroupData = useCallback(async () => { + try { + const data = await sorosaveClient.getGroup(Number(groupId)); + if (!mountedRef.current) return; + setGroup(data as GroupData); + setLastUpdated(new Date()); + setError(null); + } catch (err) { + if (!mountedRef.current) return; + setError(err instanceof Error ? err.message : "Failed to fetch group data"); + } + }, [groupId]); + + const refresh = useCallback(async () => { + setIsLoading(true); + await fetchGroupData(); + setIsLoading(false); + }, [fetchGroupData]); + + // Initial fetch + cleanup + useEffect(() => { + mountedRef.current = true; + setIsLoading(true); + fetchGroupData().finally(() => { + if (mountedRef.current) setIsLoading(false); + }); + return () => { mountedRef.current = false; }; + }, [fetchGroupData]); + + // Polling lifecycle: active/forming = poll, completed/failed = stop + useEffect(() => { + if (!group) return; + const shouldPoll = group.status === GroupStatus.Active || group.status === GroupStatus.Forming; + if (!shouldPoll) { + setIsPolling(false); + return; + } + setIsPolling(true); + intervalRef.current = setInterval(fetchGroupData, POLL_INTERVAL); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [group?.status, group?.currentRound, fetchGroupData]); + + return { group, isLoading, error, lastUpdated, isPolling, refresh }; +}