Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -12,4 +19,4 @@ const nextConfig = {
},
};

module.exports = nextConfig;
module.exports = withPWA(nextConfig);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
21 changes: 21 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
181 changes: 181 additions & 0 deletions src/lib/export.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
// 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 => `
<tr>
<td>${c.date}</td>
<td>${c.member}</td>
<td>${c.amount}</td>
<td>${c.status}</td>
</tr>
`).join("");

return `
<!DOCTYPE html>
<html>
<head>
<title>Group Report - ${summary.name}</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f5f5f5; }
.summary { display: flex; gap: 20px; margin: 20px 0; }
.summary-item { background: #f9f9f9; padding: 15px; border-radius: 8px; }
.summary-item h3 { margin: 0 0 5px 0; font-size: 14px; color: #666; }
.summary-item p { margin: 0; font-size: 24px; font-weight: bold; }
@media print { button { display: none; } }
</style>
</head>
<body>
<h1>Group Savings Report</h1>
<p>Generated: ${new Date().toLocaleDateString()}</p>

<div class="summary">
<div class="summary-item">
<h3>Total Members</h3>
<p>${summary.totalMembers}</p>
</div>
<div class="summary-item">
<h3>Total Contributions</h3>
<p>${summary.totalContributions}</p>
</div>
<div class="summary-item">
<h3>Current Pool</h3>
<p>${summary.currentPool}</p>
</div>
</div>

<h2>Contribution History</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Member</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${contributionsRows}
</tbody>
</table>

<button onclick="window.print()" style="margin-top: 20px; padding: 10px 20px; cursor: pointer;">
Print / Save as PDF
</button>
</body>
</html>
`.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<void> {
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;
}
}