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
158 changes: 158 additions & 0 deletions app/frontend/src/app/bulk/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use client";

import Link from "next/link";
import { NetworkBadge } from "@/components/NetworkBadge";
import { useBulkInvoice } from "@/hooks/useBulkInvoice";
import { CSVDropZone } from "@/components/bulk/CSVDropZone";
import { ReviewTable } from "@/components/bulk/ReviewTable";
import { BatchProgress } from "@/components/bulk/BatchProgress";
import { BatchSuccess } from "@/components/bulk/BatchSuccess";

export default function BulkInvoicing() {
const {
step,
rows,
errors,
hasErrors,
generatedLinks,
progress,
parseFile,
removeRow,
updateRow,
getRowErrors,
generateBatch,
downloadCSV,
reset,
} = useBulkInvoice();

return (
<div className="relative min-h-screen text-white selection:bg-indigo-500/30 overflow-x-hidden">
<NetworkBadge />

{/* Background glows */}
<div className="fixed top-[-20%] left-[-30%] w-[60%] h-[60%] bg-indigo-500/10 blur-[120px] rounded-full" />
<div className="fixed bottom-[-20%] right-[-30%] w-[50%] h-[50%] bg-purple-500/5 blur-[100px] rounded-full" />

{/* Sidebar */}
<aside className="hidden md:flex w-72 h-screen fixed left-0 top-0 border-r border-white/5 bg-black/20 backdrop-blur-3xl flex-col z-20">
<nav className="flex-1 px-4 py-20 space-y-2">
<Link
href="/dashboard"
className="flex items-center gap-3 px-4 py-3 text-neutral-500 hover:text-white hover:bg-white/5 rounded-2xl font-semibold"
>
<span>📊</span> Dashboard
</Link>
<Link
href="/generator"
className="flex items-center gap-3 px-4 py-3 text-neutral-500 hover:text-white hover:bg-white/5 rounded-2xl font-semibold"
>
<span>⚡</span> Link Generator
</Link>
<Link
href="/bulk"
className="flex items-center gap-3 px-4 py-3 bg-white/5 text-white rounded-2xl font-bold border border-white/5 shadow-inner"
>
<span className="text-indigo-400">📦</span> Bulk Invoicing
</Link>
</nav>
</aside>

{/* Main content */}
<main className="relative z-10 px-4 sm:px-6 md:px-12 pt-10 md:ml-72">
{/* Header */}
<header className="mb-10 sm:mb-16 max-w-3xl">
<nav className="flex items-center gap-2 text-xs font-black text-neutral-600 uppercase tracking-widest mb-4">
<span>Services</span>
<span>/</span>
<span className="text-neutral-400">Bulk Invoicing</span>
</nav>

<h1 className="text-4xl sm:text-5xl md:text-6xl font-black tracking-tight mb-4">
Bulk Invoice
<br />
<span className="bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
Generator.
</span>
</h1>

<p className="text-neutral-500 text-lg max-w-xl">
Upload a CSV with payment details, review & edit, then generate all your payment links in one batch.
</p>
</header>

{/* Step indicator */}
<div className="flex items-center gap-4 mb-12 max-w-xl">
{(["upload", "review", "processing", "success"] as const).map((s, i) => {
const labels = ["Upload", "Review", "Generate", "Done"];
const icons = ["📤", "📋", "⚡", "✅"];
const isActive = s === step;
const isPast =
["upload", "review", "processing", "success"].indexOf(step) >
["upload", "review", "processing", "success"].indexOf(s);

return (
<div key={s} className="flex items-center gap-3 flex-1">
<div
className={`
w-10 h-10 rounded-xl flex items-center justify-center text-sm font-bold transition-all
${isActive ? "bg-indigo-500/20 border border-indigo-500/30 scale-110" : ""}
${isPast ? "bg-green-500/10 border border-green-500/20" : ""}
${!isActive && !isPast ? "bg-white/5 border border-white/5" : ""}
`}
>
{isPast ? "✓" : icons[i]}
</div>
<span
className={`text-xs font-bold tracking-wide hidden sm:block ${
isActive ? "text-white" : isPast ? "text-green-400/60" : "text-neutral-600"
}`}
>
{labels[i]}
</span>
{i < 3 && (
<div
className={`flex-1 h-px ${isPast ? "bg-green-500/30" : "bg-white/5"}`}
/>
)}
</div>
);
})}
</div>

{/* Dynamic content */}
<div className="pb-20">
{step === "upload" && <CSVDropZone onFileSelected={parseFile} />}

{step === "review" && (
<ReviewTable
rows={rows}
errors={errors}
onUpdateRow={updateRow}
onRemoveRow={removeRow}
onGenerate={generateBatch}
onBack={reset}
getRowErrors={getRowErrors}
hasErrors={hasErrors}
/>
)}

{step === "processing" && (
<BatchProgress
progress={progress}
total={rows.length}
generatedLinks={generatedLinks}
/>
)}

{step === "success" && (
<BatchSuccess
generatedLinks={generatedLinks}
onDownload={downloadCSV}
onReset={reset}
/>
)}
</div>
</main>
</div>
);
}
5 changes: 5 additions & 0 deletions app/frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ body {
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
63 changes: 63 additions & 0 deletions app/frontend/src/components/bulk/BatchProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client";

import type { GeneratedLink } from "@/hooks/useBulkInvoice";

type BatchProgressProps = {
progress: number;
total: number;
generatedLinks: GeneratedLink[];
};

export function BatchProgress({ progress, total, generatedLinks }: BatchProgressProps) {
const completed = generatedLinks.length;

return (
<div className="w-full max-w-xl mx-auto flex flex-col items-center justify-center py-16 gap-10">
{/* Animated icon */}
<div className="relative">
<div className="w-28 h-28 rounded-3xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" className="text-indigo-400 animate-pulse">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="absolute -inset-4 bg-indigo-500/10 blur-2xl rounded-full animate-pulse" />
</div>

{/* Title */}
<div className="text-center">
<h2 className="text-3xl font-black tracking-tight mb-2">Generating Links</h2>
<p className="text-neutral-500 text-sm">
Processing {completed} of {total} invoices...
</p>
</div>

{/* Progress bar */}
<div className="w-full space-y-3">
<div className="w-full h-3 bg-neutral-900/50 rounded-full border border-white/5 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full transition-all duration-300 ease-out relative"
style={{ width: `${progress}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-[shimmer_1.5s_infinite] rounded-full" />
</div>
</div>
<div className="flex justify-between text-xs font-bold text-neutral-600">
<span>{completed} completed</span>
<span className="text-indigo-400">{progress}%</span>
</div>
</div>

{/* Live counter */}
<div className="grid grid-cols-2 gap-4 w-full">
<div className="p-5 rounded-2xl bg-neutral-900/40 border border-white/5 text-center">
<p className="text-3xl font-black text-white">{completed}</p>
<p className="text-[10px] text-neutral-600 font-bold uppercase tracking-widest mt-1">Generated</p>
</div>
<div className="p-5 rounded-2xl bg-neutral-900/40 border border-white/5 text-center">
<p className="text-3xl font-black text-neutral-600">{total - completed}</p>
<p className="text-[10px] text-neutral-600 font-bold uppercase tracking-widest mt-1">Remaining</p>
</div>
</div>
</div>
);
}
115 changes: 115 additions & 0 deletions app/frontend/src/components/bulk/BatchSuccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"use client";

import type { GeneratedLink } from "@/hooks/useBulkInvoice";

type BatchSuccessProps = {
generatedLinks: GeneratedLink[];
onDownload: () => void;
onReset: () => void;
};

export function BatchSuccess({ generatedLinks, onDownload, onReset }: BatchSuccessProps) {
const totalValue = generatedLinks.reduce((sum, l) => sum + Number(l.amount), 0);

return (
<div className="w-full max-w-5xl mx-auto space-y-8">
{/* Success banner */}
<div className="text-center py-8">
<div className="w-20 h-20 rounded-3xl bg-green-500/10 border border-green-500/20 flex items-center justify-center mx-auto mb-6">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" className="text-green-400">
<path d="M20 6L9 17l-5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<h2 className="text-4xl font-black tracking-tight mb-2">Batch Complete!</h2>
<p className="text-neutral-500 text-sm">
All {generatedLinks.length} payment links have been generated successfully.
</p>
</div>

{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="p-6 rounded-2xl bg-neutral-900/40 border border-white/5 text-center">
<p className="text-3xl font-black text-white">{generatedLinks.length}</p>
<p className="text-[10px] text-neutral-600 font-bold uppercase tracking-widest mt-1">Links Generated</p>
</div>
<div className="p-6 rounded-2xl bg-neutral-900/40 border border-white/5 text-center">
<p className="text-3xl font-black text-white">
{totalValue.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</p>
<p className="text-[10px] text-neutral-600 font-bold uppercase tracking-widest mt-1">Total Value</p>
</div>
<div className="p-6 rounded-2xl bg-green-500/10 border border-green-500/20 text-center">
<p className="text-3xl font-black text-green-400">100%</p>
<p className="text-[10px] text-green-400/60 font-bold uppercase tracking-widest mt-1">Success Rate</p>
</div>
</div>

{/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button
onClick={onDownload}
className="px-8 py-4 bg-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-indigo-500/20 hover:bg-indigo-400 hover:scale-105 active:scale-95 transition flex items-center justify-center gap-2"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" strokeLinecap="round" strokeLinejoin="round" />
<polyline points="7 10 12 15 17 10" strokeLinecap="round" strokeLinejoin="round" />
<line x1="12" y1="15" x2="12" y2="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Download All as CSV
</button>
<button
onClick={onReset}
className="px-8 py-4 bg-white/5 text-neutral-400 font-bold rounded-xl border border-white/5 hover:bg-white/10 hover:text-white transition"
>
Generate More
</button>
</div>

{/* Links preview table */}
<div className="rounded-3xl bg-black/40 border border-white/5 backdrop-blur-2xl shadow-2xl overflow-hidden">
<div className="p-6 sm:p-8 border-b border-white/5">
<h3 className="text-lg font-black">Generated Links</h3>
<p className="text-xs text-neutral-600 mt-1">Click any link to copy</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left min-w-[650px]">
<thead>
<tr className="text-[9px] font-black text-neutral-600 uppercase tracking-widest border-b border-white/5">
<th className="px-6 py-4">#</th>
<th className="px-6 py-4">Destination</th>
<th className="px-6 py-4">Amount</th>
<th className="px-6 py-4">Link</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{generatedLinks.map((link, i) => (
<tr key={i} className="hover:bg-white/[0.02] transition">
<td className="px-6 py-4">
<span className="w-7 h-7 rounded-lg bg-white/5 flex items-center justify-center text-[10px] text-neutral-500 font-mono">
{i + 1}
</span>
</td>
<td className="px-6 py-4 font-mono text-xs text-neutral-400 max-w-40 truncate">
{link.destination}
</td>
<td className="px-6 py-4 font-bold">
{link.amount} <span className="text-neutral-500 text-xs">{link.asset}</span>
</td>
<td className="px-6 py-4">
<button
onClick={() => navigator.clipboard.writeText(link.link)}
className="text-indigo-400 hover:text-indigo-300 text-sm font-mono underline underline-offset-2 transition truncate block max-w-60"
title="Click to copy"
>
{link.link}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
Loading