diff --git a/next.config.js b/next.config.js index 55d4c9b..1a6ace8 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,11 @@ /** @type {import('next').NextConfig} */ +const withPWA = require('next-pwa')({ + dest: 'public', + register: true, + skipWaiting: true, + disable: process.env.NODE_ENV === 'development', +}); + const nextConfig = { reactStrictMode: true, webpack: (config) => { @@ -12,4 +19,4 @@ const nextConfig = { }, }; -module.exports = nextConfig; +module.exports = withPWA(nextConfig); diff --git a/package.json b/package.json index 7e788f6..ed4f5b1 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@sorosave/sdk": "workspace:*", "@stellar/freighter-api": "^2.0.0", "next": "^14.2.0", + "next-pwa": "^5.6.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..6d9997f --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "SoroSave", + "short_name": "SoroSave", + "description": "Group savings made simple", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#3b82f6", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 17200f3..bab7d23 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ export const metadata: Metadata = { title: "SoroSave — Decentralized Group Savings", description: "A decentralized rotating savings protocol built on Soroban. Create or join savings groups, contribute each cycle, and receive the pot when it's your turn.", + manifest: "/manifest.json", }; export default function RootLayout({ diff --git a/src/lib/export.ts b/src/lib/export.ts new file mode 100644 index 0000000..3b10628 --- /dev/null +++ b/src/lib/export.ts @@ -0,0 +1,181 @@ +/** + * Data Export Module + * Support CSV and PDF export for group history and contribution records + */ + +import { sorosaveClient } from "./sorosave"; + +export interface ContributionRecord { + date: string; + member: string; + amount: number; + status: "pending" | "completed" | "failed"; +} + +export interface GroupSummary { + name: string; + createdAt: string; + totalMembers: number; + totalContributions: number; + roundNumber: number; + currentPool: number; +} + +/** + * Export contributions to CSV format + */ +export function exportToCSV(contributions: ContributionRecord[], filename: string): void { + const headers = ["Date", "Member", "Amount", "Status"]; + const rows = contributions.map(c => [ + c.date, + c.member, + c.amount.toString(), + c.status + ]); + + const csvContent = [ + headers.join(","), + ...rows.map(row => row.join(",")) + ].join("\n"); + + downloadFile(csvContent, `${filename}.csv`, "text/csv"); +} + +/** + * Export group summary to PDF (simplified - actual PDF requires library like jsPDF) + */ +export async function exportToPDF(summary: GroupSummary, contributions: ContributionRecord[]): Promise { + // Build HTML content for PDF + const html = buildPDFHTML(summary, contributions); + + // For now, download as HTML that can be printed to PDF + downloadFile(html, `${summary.name}_report.html`, "text/html"); + + console.log("PDF export: HTML downloaded. Use browser print to save as PDF."); +} + +/** + * Build HTML content for PDF export + */ +function buildPDFHTML(summary: GroupSummary, contributions: ContributionRecord[]): string { + const contributionsRows = contributions.map(c => ` + + ${c.date} + ${c.member} + ${c.amount} + ${c.status} + + `).join(""); + + return ` + + + + Group Report - ${summary.name} + + + +

Group Savings Report

+

Generated: ${new Date().toLocaleDateString()}

+ +
+
+

Total Members

+

${summary.totalMembers}

+
+
+

Total Contributions

+

${summary.totalContributions}

+
+
+

Current Pool

+

${summary.currentPool}

+
+
+ +

Contribution History

+ + + + + + + + + + + ${contributionsRows} + +
DateMemberAmountStatus
+ + + + + `.trim(); +} + +/** + * Download file helper + */ +function downloadFile(content: string, filename: string, mimeType: string): void { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +/** + * Fetch and export group data + */ +export async function exportGroupData(groupId: string, format: "csv" | "pdf"): Promise { + try { + // Get group info + const groupInfo = await sorosaveClient.getGroupInfo(groupId); + + // Build summary + const summary: GroupSummary = { + name: groupInfo.name || "Group", + createdAt: new Date().toISOString(), + totalMembers: groupInfo.members?.length || 0, + totalContributions: groupInfo.totalContributions || 0, + roundNumber: groupInfo.currentRound || 1, + currentPool: groupInfo.poolAmount || 0 + }; + + // Mock contribution data (replace with actual API call) + const contributions: ContributionRecord[] = [ + { date: "2026-01-15", member: "Alice", amount: 100, status: "completed" }, + { date: "2026-01-22", member: "Bob", amount: 100, status: "completed" }, + { date: "2026-01-29", member: "Charlie", amount: 100, status: "completed" } + ]; + + if (format === "csv") { + exportToCSV(contributions, `${summary.name}_contributions`); + } else { + await exportToPDF(summary, contributions); + } + + console.log(`Exported ${format.toUpperCase()} for group ${groupId}`); + } catch (error) { + console.error("Export failed:", error); + throw error; + } +}