Skip to content

Commit 09aacd5

Browse files
Bruno755Stephanie Nwankwoclaude
authored
feat: UI enhancements for split-app (#222-225) (#265)
- #225: DRAFT watermark for non-Released invoices - #222: Drag-and-drop reordering with keyboard shortcuts Co-authored-by: Stephanie Nwankwo <badviruscoder@gmail.com> Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 6f37778 commit 09aacd5

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

src/components/BatchPayQueue.tsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { formatAmount, parseAmount } from "@stellar-split/sdk";
44
import type { Invoice } from "@stellar-split/sdk";
5+
import { useState } from "react";
56

67
interface QueueEntry {
78
invoice: Invoice;
@@ -12,14 +13,40 @@ interface Props {
1213
queue: QueueEntry[];
1314
onAmountChange: (invoiceId: string, value: string) => void;
1415
onRemove: (invoiceId: string) => void;
16+
onReorder?: (newQueue: QueueEntry[]) => void;
1517
}
1618

17-
export default function BatchPayQueue({ queue, onAmountChange, onRemove }: Props) {
19+
export default function BatchPayQueue({ queue, onAmountChange, onRemove, onReorder }: Props) {
20+
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
21+
1822
const runningTotal = queue.reduce((sum, entry) => {
1923
const parsed = parseFloat(entry.amount);
2024
return sum + (isNaN(parsed) ? 0 : parsed);
2125
}, 0);
2226

27+
const handleDrop = (targetIndex: number) => {
28+
if (draggedIndex === null || draggedIndex === targetIndex) return;
29+
const newQueue = [...queue];
30+
const [dragged] = newQueue.splice(draggedIndex, 1);
31+
newQueue.splice(targetIndex, 0, dragged);
32+
onReorder?.(newQueue);
33+
setDraggedIndex(null);
34+
};
35+
36+
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
37+
if (e.altKey && e.key === "ArrowUp" && index > 0) {
38+
e.preventDefault();
39+
const newQueue = [...queue];
40+
[newQueue[index - 1], newQueue[index]] = [newQueue[index], newQueue[index - 1]];
41+
onReorder?.(newQueue);
42+
} else if (e.altKey && e.key === "ArrowDown" && index < queue.length - 1) {
43+
e.preventDefault();
44+
const newQueue = [...queue];
45+
[newQueue[index], newQueue[index + 1]] = [newQueue[index + 1], newQueue[index]];
46+
onReorder?.(newQueue);
47+
}
48+
};
49+
2350
if (queue.length === 0) {
2451
return (
2552
<p className="text-gray-500 text-sm py-4 text-center">
@@ -30,13 +57,23 @@ export default function BatchPayQueue({ queue, onAmountChange, onRemove }: Props
3057

3158
return (
3259
<div className="flex flex-col gap-3">
33-
{queue.map(({ invoice, amount }) => {
60+
{queue.map(({ invoice, amount }, index) => {
3461
const total = invoice.recipients.reduce((s, r) => s + r.amount, 0n);
3562
return (
3663
<div
3764
key={invoice.id}
38-
className="bg-gray-900 rounded-xl px-4 py-3 flex flex-col sm:flex-row sm:items-center gap-3"
65+
draggable
66+
onDragStart={() => setDraggedIndex(index)}
67+
onDragOver={(e) => e.preventDefault()}
68+
onDrop={() => handleDrop(index)}
69+
onDragEnd={() => setDraggedIndex(null)}
70+
onKeyDown={(e) => handleKeyDown(index, e)}
71+
tabIndex={0}
72+
className={`bg-gray-900 rounded-xl px-4 py-3 flex flex-col sm:flex-row sm:items-center gap-3 cursor-grab ${
73+
draggedIndex === index ? "opacity-50" : ""
74+
}`}
3975
>
76+
<span className="text-gray-500 text-xs select-none">⋮⋮</span>
4077
<div className="flex-1 min-w-0">
4178
<p className="text-sm font-semibold truncate">Invoice #{invoice.id}</p>
4279
<p className="text-xs text-gray-500 mt-0.5">

src/components/InvoicePDF.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ export default function InvoicePDF({ invoice, total, locale = "en" }: Props) {
3030
</button>
3131

3232
{/* Print-only content */}
33-
<div id="invoice-print" className="hidden print:block text-black bg-white p-8 text-sm leading-relaxed">
33+
<div id="invoice-print" className="hidden print:block text-black bg-white p-8 text-sm leading-relaxed relative">
34+
{invoice.status !== "Released" && (
35+
<div className="fixed inset-0 pointer-events-none print:fixed print:inset-0 print:pointer-events-none z-0 flex items-center justify-center opacity-10">
36+
<div className="transform -rotate-45 text-9xl font-bold text-gray-800 whitespace-nowrap">DRAFT</div>
37+
</div>
38+
)}
39+
<div className="relative z-10">
3440
<h1 className="text-2xl font-bold mb-1">
3541
{t(locale, "invoice")} #{invoice.id}
3642
</h1>
@@ -74,6 +80,7 @@ export default function InvoicePDF({ invoice, total, locale = "en" }: Props) {
7480
))}
7581
</tbody>
7682
</table>
83+
</div>
7784
</div>
7885
</>
7986
);

0 commit comments

Comments
 (0)