Skip to content

Commit 71f397a

Browse files
authored
Merge pull request #146 from henrypeters/feature/issue-19-implement-invoice
feat: implement invoice
2 parents e4d3266 + 2c9cfce commit 71f397a

4 files changed

Lines changed: 95 additions & 0 deletions

File tree

src/app/invoice/[id]/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
import { formatAmount, parseAmount, truncateAddress } from "@stellar-split/sdk";
1313
import PaymentProgress from "@/components/PaymentProgress";
1414
import CountdownTimer from "@/components/CountdownTimer";
15+
import RecipientPieChart from "@/components/RecipientPieChart";
16+
import InvoicePDF from "@/components/InvoicePDF";
1517
import InstallmentPanel from "@/components/InstallmentPanel";
1618
import CommentSection from "@/components/CommentSection";
1719
import StatusTimeline from "@/components/StatusTimeline";
@@ -253,13 +255,19 @@ export default function InvoiceDetailPage({ params }: Props) {
253255
>
254256
{invoice.status}
255257
</span>
258+
<<<<<<< feature/issue-19-implement-invoice
259+
<div className="ml-auto print:hidden">
260+
<InvoicePDF invoice={invoice} total={total} />
261+
</div>
262+
=======
256263
<button
257264
type="button"
258265
onClick={() => window.print()}
259266
className="sm:ml-auto min-h-11 px-3 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm transition-colors print:hidden self-start sm:self-auto"
260267
>
261268
Print Invoice
262269
</button>
270+
>>>>>>> main
263271
</div>
264272

265273
{/* Status Timeline */}

src/components/CountdownTimer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export default function CountdownTimer({ deadline, compact = false }: Props) {
3333
const hours = Math.floor((timeLeft % 86400) / 3600);
3434
const minutes = Math.floor((timeLeft % 3600) / 60);
3535
const seconds = timeLeft % 60;
36+
const pad = (n: number) => String(n).padStart(2, "0");
37+
const isUrgent = timeLeft < 86400;
3638

3739
const pad = (n: number) => String(n).padStart(2, "0");
3840
const isUrgent = timeLeft < 86400;
@@ -43,6 +45,7 @@ export default function CountdownTimer({ deadline, compact = false }: Props) {
4345
: `${pad(days)}:${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
4446

4547
return (
48+
<span className={`font-mono text-xs font-semibold tabular-nums ${isUrgent ? "text-red-400" : "text-gray-300"}`}>
4649
<span className={`font-mono text-xs font-semibold tabular-nums ${colorClass}`}>
4750
{display}
4851
</span>

src/components/InvoicePDF.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { formatAmount } from "@stellar-split/sdk";
2+
import type { Invoice } from "@stellar-split/sdk";
3+
4+
interface Props {
5+
invoice: Invoice;
6+
total: bigint;
7+
}
8+
9+
/**
10+
* InvoicePDF — print-only invoice layout + "Download PDF" button.
11+
* The button is hidden in print view via print:hidden.
12+
* The #invoice-print section is shown only in print via the global @media print styles.
13+
*/
14+
export default function InvoicePDF({ invoice, total }: Props) {
15+
const deadline = invoice.deadline > 0
16+
? new Date(invoice.deadline * 1000).toLocaleString()
17+
: "No deadline";
18+
19+
return (
20+
<>
21+
{/* Trigger button — hidden when printing */}
22+
<button
23+
type="button"
24+
onClick={() => window.print()}
25+
className="px-3 py-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors print:hidden"
26+
>
27+
Download PDF
28+
</button>
29+
30+
{/* Print-only content */}
31+
<div id="invoice-print" className="hidden print:block text-black bg-white p-8 text-sm leading-relaxed">
32+
<h1 className="text-2xl font-bold mb-1">Invoice #{invoice.id}</h1>
33+
<p className="text-gray-500 mb-6">StellarSplit On-Chain Invoice</p>
34+
35+
<table className="w-full mb-6 text-left border-collapse">
36+
<tbody>
37+
<tr className="border-b border-gray-200">
38+
<th className="py-1.5 pr-4 font-semibold w-32">Status</th>
39+
<td className="py-1.5">{invoice.status}</td>
40+
</tr>
41+
<tr className="border-b border-gray-200">
42+
<th className="py-1.5 pr-4 font-semibold">Creator</th>
43+
<td className="py-1.5 font-mono break-all">{invoice.creator}</td>
44+
</tr>
45+
<tr className="border-b border-gray-200">
46+
<th className="py-1.5 pr-4 font-semibold">Deadline</th>
47+
<td className="py-1.5">{deadline}</td>
48+
</tr>
49+
<tr>
50+
<th className="py-1.5 pr-4 font-semibold">Total</th>
51+
<td className="py-1.5">{formatAmount(total)} USDC</td>
52+
</tr>
53+
</tbody>
54+
</table>
55+
56+
<h2 className="font-semibold text-base mb-2">Recipients</h2>
57+
<table className="w-full border-collapse">
58+
<thead>
59+
<tr className="border-b-2 border-gray-300">
60+
<th className="text-left py-1.5 pr-4">Address</th>
61+
<th className="text-right py-1.5">Amount (USDC)</th>
62+
</tr>
63+
</thead>
64+
<tbody>
65+
{invoice.recipients.map((r, i) => (
66+
<tr key={i} className="border-b border-gray-100">
67+
<td className="py-1.5 pr-4 font-mono break-all">{r.address}</td>
68+
<td className="py-1.5 text-right">{formatAmount(r.amount)}</td>
69+
</tr>
70+
))}
71+
</tbody>
72+
</table>
73+
</div>
74+
</>
75+
);
76+
}

src/components/RecipientPieChart.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface Props {
1414
export default function RecipientPieChart({ recipients, total }: Props) {
1515
const data = recipients.map((r) => ({
1616
address: r.address,
17+
name: truncateAddress(r.address),
1718
label: truncateAddress(r.address),
1819
amount: r.amount,
1920
value: Number((r.amount * 10000n) / total) / 100,
@@ -22,6 +23,7 @@ export default function RecipientPieChart({ recipients, total }: Props) {
2223
return (
2324
<ResponsiveContainer width="100%" height={260}>
2425
<PieChart>
26+
<Pie dataKey="value" data={data} cx="50%" cy="45%" outerRadius="60%" isAnimationActive={false}>
2527
<Pie
2628
data={data}
2729
dataKey="value"
@@ -36,6 +38,7 @@ export default function RecipientPieChart({ recipients, total }: Props) {
3638
))}
3739
</Pie>
3840
<Tooltip
41+
formatter={(value: number, _: string, props: { payload?: { address: string; amount: bigint } }) => [
3942
formatter={(value: number, _name: string, props: { payload?: { address: string; amount: bigint } }) => [
4043
`${formatAmount(props.payload?.amount ?? 0n)} USDC (${value.toFixed(2)}%)`,
4144
props.payload?.address ?? "",
@@ -45,6 +48,11 @@ export default function RecipientPieChart({ recipients, total }: Props) {
4548
labelStyle={{ display: "none" }}
4649
/>
4750
<Legend
51+
wrapperStyle={{ fontSize: "11px", paddingTop: "8px" }}
52+
formatter={(value: string, entry: { payload?: Record<string, unknown> }) => {
53+
const amount = (entry.payload?.amount as bigint | undefined) ?? 0n;
54+
return `${value}${formatAmount(amount)} USDC`;
55+
}}
4856
formatter={(value: string, entry: { payload?: { amount: bigint } }) =>
4957
`${value}${formatAmount(entry.payload?.amount ?? 0n)} USDC`
5058
}

0 commit comments

Comments
 (0)