diff --git a/src/app/groups/[id]/page.tsx b/src/app/groups/[id]/page.tsx index 02ab880..743940c 100644 --- a/src/app/groups/[id]/page.tsx +++ b/src/app/groups/[id]/page.tsx @@ -6,6 +6,7 @@ import { RoundProgress } from "@/components/RoundProgress"; import { ContributeModal } from "@/components/ContributeModal"; import { useState } from "react"; import { formatAmount, GroupStatus } from "@sorosave/sdk"; +import { exportContributionsCSV, exportGroupSummaryPDF, ContributionRecord } from "@/lib/export"; // TODO: Fetch real data from contract const MOCK_GROUP = { @@ -32,9 +33,37 @@ const MOCK_GROUP = { createdAt: 1700000000, }; +// Placeholder contribution history — will be fetched from contract/indexer +const MOCK_CONTRIBUTIONS: ContributionRecord[] = [ + { + date: "2024-01-15", + member: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFG", + amount: "1000", + round: 1, + status: "paid", + txHash: "abc123def456789", + }, + { + date: "2024-01-15", + member: "GEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJ", + amount: "1000", + round: 1, + status: "paid", + txHash: "def789ghi012345", + }, + { + date: "2024-01-15", + member: "GIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMN", + amount: "1000", + round: 1, + status: "pending", + }, +]; + export default function GroupDetailPage() { const [showContributeModal, setShowContributeModal] = useState(false); const group = MOCK_GROUP; + const contributions = MOCK_CONTRIBUTIONS; return ( <> @@ -83,6 +112,18 @@ export default function GroupDetailPage() { Join Group )} + + diff --git a/src/lib/export.ts b/src/lib/export.ts new file mode 100644 index 0000000..ce6b2e9 --- /dev/null +++ b/src/lib/export.ts @@ -0,0 +1,157 @@ +import { SavingsGroup, formatAmount, getStatusLabel } from "@sorosave/sdk"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface ContributionRecord { + date: string; + member: string; + amount: string; + round: number; + status: "paid" | "pending"; + txHash?: string; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function escapeCell(value: string): string { + if (value.includes(",") || value.includes('"') || value.includes("\n")) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +function toCSV(headers: string[], rows: string[][]): string { + const lines = [ + headers.map(escapeCell).join(","), + ...rows.map((row) => row.map(escapeCell).join(",")), + ]; + return lines.join("\n"); +} + +function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +function formatDate(timestamp: number): string { + return new Date(timestamp * 1000).toISOString().split("T")[0]; +} + +// --------------------------------------------------------------------------- +// CSV export +// --------------------------------------------------------------------------- + +export function exportContributionsCSV( + group: SavingsGroup, + contributions: ContributionRecord[] +): void { + const headers = ["Date", "Member", "Amount", "Round", "Status", "Tx Hash"]; + const rows = contributions.map((c) => [ + c.date, + c.member, + c.amount, + String(c.round), + c.status, + c.txHash ?? "", + ]); + + const csv = toCSV(headers, rows); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + downloadBlob(blob, `${group.name.replace(/\s+/g, "-")}-contributions.csv`); +} + +// --------------------------------------------------------------------------- +// PDF export — plain HTML → print window (no external deps) +// --------------------------------------------------------------------------- + +export function exportGroupSummaryPDF( + group: SavingsGroup, + contributions: ContributionRecord[] +): void { + const rows = contributions + .map( + (c) => ` + + ${c.date} + ${c.member.slice(0, 16)}… + ${c.amount} + ${c.round} + ${c.status} + ${c.txHash ? c.txHash.slice(0, 12) + "…" : "—"} + ` + ) + .join(""); + + const html = ` + + + +${group.name} — Group Summary + + + +

${group.name}

+

Generated ${new Date().toLocaleDateString()} · Status: ${getStatusLabel(group.status)}

+ +
+
+
Contribution Amount
+
${formatAmount(group.contributionAmount)} tokens
+
+
+
Members
+
${group.members.length} / ${group.maxMembers}
+
+
+
Round
+
${group.currentRound} / ${group.totalRounds || group.maxMembers}
+
+
+
Created
+
${formatDate(group.createdAt)}
+
+
+ +

Contribution History

+${ + contributions.length === 0 + ? "

No contributions recorded yet.

" + : ` + + + + ${rows} +
DateMemberAmountRoundStatusTx Hash
` +} + +`; + + const win = window.open("", "_blank"); + if (!win) return; + win.document.write(html); + win.document.close(); + win.focus(); + win.print(); +}