import { useState, useRef, useCallback } from “react”;
// ─── Constants ────────────────────────────────────────────────────────────────
const TEMPLATES = {
modern: { name: “Modern”, accent: “#6366f1”, headerBg: “#6366f1”, headerText: “#fff”, labelColor: “#6366f1”, borderRadius: “12px”, fontFamily: “‘Inter’, sans-serif” },
corporate: { name: “Corporate”, accent: “#1e40af”, headerBg: “#1e3a8a”, headerText: “#fff”, labelColor: “#1e40af”, borderRadius: “4px”, fontFamily: “‘Georgia’, serif” },
minimal: { name: “Minimal”, accent: “#374151”, headerBg: “#fff”, headerText: “#111827”, labelColor: “#6b7280”, borderRadius: “0px”, fontFamily: “‘Helvetica Neue’, sans-serif” },
professional: { name: “Professional”, accent: “#0f766e”, headerBg: “#134e4a”, headerText: “#fff”, labelColor: “#0f766e”, borderRadius: “8px”, fontFamily: “‘system-ui’, sans-serif” },
creative: { name: “Creative”, accent: “#7c3aed”, headerBg: “linear-gradient(135deg,#7c3aed 0%,#db2777 100%)”, headerText: “#fff”, labelColor: “#7c3aed”, borderRadius: “16px”, fontFamily: “‘system-ui’, sans-serif” },
};
const CURRENCIES = [
{ code: “USD”, symbol: “$”, name: “US Dollar” },
{ code: “EUR”, symbol: “€”, name: “Euro” },
{ code: “GBP”, symbol: “£”, name: “British Pound” },
{ code: “PKR”, symbol: “₨”, name: “Pakistani Rupee” },
{ code: “AED”, symbol: “د.إ”, name: “UAE Dirham” },
{ code: “SAR”, symbol: “﷼”, name: “Saudi Riyal” },
{ code: “INR”, symbol: “₹”, name: “Indian Rupee” },
{ code: “CAD”, symbol: “CA$”, name: “Canadian Dollar” },
];
const FAQ_DATA = [
{ q: “How do I create my first invoice?”, a: “Click ‘New Invoice’ in the sidebar. Fill in your business info, client details, add line items, then hit Save. Switch to Preview to see the final result and use Print/PDF to export.” },
{ q: “How do I download the invoice as PDF?”, a: “Go to the Preview tab and click ‘Print / Save as PDF’. Your browser’s print dialog will open — choose ‘Save as PDF’ as the destination to get a PDF file.” },
{ q: “Can I add my company logo?”, a: “Yes! In the Editor under ‘Your Business’, click the logo upload box. Supported formats are PNG, JPG, and SVG. The logo will appear on all invoice templates.” },
{ q: “What currencies are supported?”, a: “InvoicePro supports USD, EUR, GBP, PKR, AED, SAR, INR, and CAD. Select your currency in the Invoice Details section of the editor.” },
{ q: “How does per-item tax work?”, a: “Each line item has its own Tax % column. Enter the applicable tax rate for each item — the tax is calculated on that item’s subtotal and included in its Total column.” },
{ q: “Can I apply a discount to the whole invoice?”, a: “Yes. In Invoice Details, enter a Discount amount and choose between Percent (%) or Fixed amount. The discount is applied to the overall subtotal.” },
{ q: “How do I duplicate a line item?”, a: “In the line items table, each row has a copy icon on the right. Click it to instantly duplicate that item below the current row.” },
{ q: “Where are my invoices saved?”, a: “Invoices are saved in your browser’s memory for this session. Click the ‘Saved’ tab to view, load, or delete them. Note: they reset if you refresh the page.” },
{ q: “Can I switch templates after creating an invoice?”, a: “Absolutely. The Template picker at the top of the Editor lets you switch between Modern, Corporate, Minimal, Professional, and Creative — the live preview updates instantly.” },
{ q: “How do I mark an invoice as paid?”, a: “In the Preview tab, click ‘Mark as Paid’. You can also change the status directly in the Invoice Details section of the Editor using the Status dropdown.” },
];
const GUIDE_STEPS = [
{ icon: “🏢”, title: “Set up your business”, desc: “Fill in your business name, logo, email, phone, and address in the ‘Your Business’ section. This info appears on every invoice you create.”, tip: “Upload a high-resolution PNG or SVG logo for the sharpest print quality.” },
{ icon: “👤”, title: “Add client details”, desc: “Enter your client’s name, company, email, and address in the ‘Bill To’ section. These details identify who the invoice is for.”, tip: “Keep client names consistent across invoices for easier searching later.” },
{ icon: “📋”, title: “Configure invoice details”, desc: “Set the invoice number (auto-generated), date, due date, currency, and any discount. The due date is pre-set to 30 days from today.”, tip: “Use a consistent prefix like ‘INV-2024-‘ to keep invoices organised by year.” },
{ icon: “➕”, title: “Add line items”, desc: “Click ‘Add Item’ to add products or services. Each row has Name, Description, Quantity, Unit Price, and Tax %. Totals calculate automatically.”, tip: “Use the duplicate button (copy icon) to quickly clone a similar item row.” },
{ icon: “🎨”, title: “Pick a template”, desc: “Choose from 5 professional templates: Modern, Corporate, Minimal, Professional, and Creative. The live preview on the right updates instantly.”, tip: “The ‘Minimal’ template works best for simple freelance invoices.” },
{ icon: “💾”, title: “Save and export”, desc: “Click Save to store the invoice in this session. Switch to the Preview tab to see the final invoice, then use Print / Save as PDF to export.”, tip: “Use Ctrl+P (or Cmd+P on Mac) from the Preview tab for quick printing.” },
];
const genId = () => Math.random().toString(36).slice(2, 9);
const genInvNum = () => `INV-${new Date().getFullYear()}-${String(Math.floor(Math.random() * 9000) + 1000)}`;
const defaultItem = () => ({ id: genId(), name: “”, description: “”, qty: 1, price: 0, tax: 0 });
const defaultInvoice = () => ({
number: genInvNum(),
date: new Date().toISOString().split(“T”)[0],
dueDate: new Date(Date.now() + 30 * 86400000).toISOString().split(“T”)[0],
currency: “USD”, taxRate: 0, discount: 0, discountType: “percent”,
notes: “Thank you for your business!”, terms: “Payment is due within 30 days.”,
from: { name: “”, email: “”, phone: “”, address: “”, city: “”, country: “”, website: “” },
to: { name: “”, company: “”, email: “”, phone: “”, address: “”, city: “”, country: “” },
items: [defaultItem()], template: “modern”, status: “draft”, logo: null,
});
function formatCurrency(amount, currencyCode) {
const cur = CURRENCIES.find((c) => c.code === currencyCode) || CURRENCIES[0];
return `${cur.symbol}${Number(amount).toLocaleString(“en-US”, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function calcTotals(invoice) {
const subtotal = invoice.items.reduce((sum, item) => {
const lineTotal = Number(item.qty) * Number(item.price);
return sum + lineTotal + lineTotal * (Number(item.tax) / 100);
}, 0);
const discountAmt = invoice.discountType === “percent” ? subtotal * (Number(invoice.discount) / 100) : Number(invoice.discount);
return { subtotal, discountAmt, total: Math.max(0, subtotal – discountAmt) };
}
// ─── Form primitives ──────────────────────────────────────────────────────────
function Input({ label, value, onChange, type = “text”, placeholder, class= “”, dark }) {
const base = dark
? “border border-gray-600 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-gray-700 text-gray-100 placeholder-gray-400”
: “border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white text-gray-800 placeholder-gray-400”;
return (
{label}}
);
}
// ─── Invoice Preview ──────────────────────────────────────────────────────────
function InvoicePreview({ invoice, printRef }) {
const t = TEMPLATES[invoice.template] || TEMPLATES.modern;
const { subtotal, discountAmt, total } = calcTotals(invoice);
const isMinimal = invoice.template === “minimal”;
return (
{invoice.from.website &&
}
{invoice.from.email &&
}
{invoice.from.phone &&
}
{invoice.date}
{invoice.dueDate}
{data.company &&
}
{data.email &&
}
))}
| {h} | ||||
|---|---|---|---|---|
|
{item.name || “—”}
{item.description && {item.description}
} |
{item.qty} | {formatCurrency(item.price, invoice.currency)} | {item.tax}% | {formatCurrency(lt + lineTax, invoice.currency)} |
{formatCurrency(subtotal, invoice.currency)}
{discountAmt > 0 && (
-{formatCurrency(discountAmt, invoice.currency)}
)}
{formatCurrency(total, invoice.currency)}
{(invoice.notes || invoice.terms) && (
)}
{invoice.terms && (
)}
)}
);
}
// ─── Items Table ──────────────────────────────────────────────────────────────
function ItemsTable({ items, onChange, currency, dark }) {
const updateItem = (id, field, value) => onChange(items.map((item) => (item.id === id ? { …item, [field]: value } : item)));
const addItem = () => onChange([…items, defaultItem()]);
const removeItem = (id) => onChange(items.filter((item) => item.id !== id));
const dupItem = (id) => {
const idx = items.findIndex((i) => i.id === id);
const dup = { …items[idx], id: genId() };
const next = […items]; next.splice(idx + 1, 0, dup); onChange(next);
};
const cellCls = dark ? “bg-transparent text-gray-100 border-none outline-none focus:bg-gray-600 rounded px-1 py-0.5 w-full text-sm” : “bg-transparent text-gray-800 border-none outline-none focus:bg-indigo-50 rounded px-1 py-0.5 w-full text-sm”;
const rowBg = (i) => dark ? (i % 2 === 0 ? “bg-gray-800” : “bg-gray-750”) : (i % 2 === 0 ? “bg-white” : “bg-gray-50/50”);
return (
Line Items
| Item / Description | Qty | Price | Tax % | Total | |
|---|---|---|---|---|---|
| updateItem(item.id, “description”, e.target.value)” placeholder=”Description (optional)” class={`${cellCls} text-xs mt-0.5 opacity-60`} /> | {formatCurrency(lt + lineTax, currency)} |
{items.length > 1 && ( )} |
);
}
function TotalsSummary({ invoice, dark }) {
const { subtotal, discountAmt, total } = calcTotals(invoice);
const t = TEMPLATES[invoice.template] || TEMPLATES.modern;
return (
{formatCurrency(subtotal, invoice.currency)}
{discountAmt > 0 && (
-{formatCurrency(discountAmt, invoice.currency)}
)}
{formatCurrency(total, invoice.currency)}
);
}
function TemplatePicker({ value, onChange, dark }) {
return (
{t.name}
))}
);
}
function usePrint(printRef) {
return useCallback(() => {
const content = printRef.current;
if (!content) return;
const win = window.open(“”, “_blank”, “width=900,height=700”);
win.document.write(`
`);
win.document.close(); win.focus();
setTimeout(() => { win.print(); }, 500);
}, [printRef]);
}
// ─── Sidebar ──────────────────────────────────────────────────────────────────
const NAV_ITEMS = [
{ id: “editor”, label: “Invoice Editor”, icon: (
)},
{ id: “preview”, label: “Preview”, icon: (
)},
{ id: “saved”, label: “Saved Invoices”, icon: (
)},
{ id: “guide”, label: “User Guide”, icon: (
)},
{ id: “faq”, label: “FAQ”, icon: (
)},
];
function Sidebar({ activeTab, setActiveTab, savedCount, dark, toggleDark, newInvoice, saveInvoice, handlePrint }) {
const sidebarBg = dark ? “bg-gray-900 border-gray-700” : “bg-white border-gray-200”;
const textMuted = dark ? “text-gray-400” : “text-gray-500”;
const textPrimary = dark ? “text-gray-100” : “text-gray-800”;
return (
);
}
// ─── Guide Page ───────────────────────────────────────────────────────────────
function GuidePage({ dark }) {
const cardCls = dark ? “bg-gray-800 border-gray-700” : “bg-white border-gray-200”;
const textPrimary = dark ? “text-gray-100” : “text-gray-900”;
const textSecondary = dark ? “text-gray-300” : “text-gray-600”;
const textMuted = dark ? “text-gray-400” : “text-gray-500”;
const tipCls = dark ? “bg-indigo-900/40 border-indigo-700 text-indigo-300” : “bg-indigo-50 border-indigo-200 text-indigo-700”;
return (
User Guide
Everything you need to create professional invoices
Welcome to InvoicePro! Follow the steps below to create, customise, and export your first invoice in under 2 minutes.
{step.title}
{step.desc}
Pro tip: {step.tip}
))}
Keyboard shortcuts
[“Ctrl + P”, “Print / Save as PDF”],
[“Tab”, “Move between form fields”],
[“Enter”, “Confirm input changes”],
[“Esc”, “Cancel editing”],
].map(([key, desc]) => (
{key}
))}
Choosing the right template
—
{key === “modern” && “Best for tech, startups, and creative professionals.”}
{key === “corporate” && “Ideal for large businesses, legal, and finance.”}
{key === “minimal” && “Clean and simple — great for freelancers.”}
{key === “professional” && “Versatile for consultants and agencies.”}
{key === “creative” && “Bold and vibrant for design and media work.”}
))}
);
}
// ─── FAQ Page ─────────────────────────────────────────────────────────────────
function FAQPage({ dark }) {
const [open, setOpen] = useState(null);
const cardCls = dark ? “bg-gray-800 border-gray-700” : “bg-white border-gray-200”;
const textPrimary = dark ? “text-gray-100” : “text-gray-900”;
const textSecondary = dark ? “text-gray-300” : “text-gray-600”;
const textMuted = dark ? “text-gray-400” : “text-gray-500”;
const divider = dark ? “border-gray-700” : “border-gray-100”;
return (
Frequently Asked Questions
{FAQ_DATA.length} questions answered
{open === i && (
{item.a}
)}
))}
Still have questions?
Can’t find what you’re looking for? Check the User Guide for step-by-step walkthroughs.
);
}
// ─── Saved Invoices Page ──────────────────────────────────────────────────────
function SavedPage({ savedInvoices, onSelect, onDelete, activeId, newInvoice, dark }) {
const textPrimary = dark ? “text-gray-100” : “text-gray-900”;
const textMuted = dark ? “text-gray-400” : “text-gray-500”;
return (
Saved Invoices
{savedInvoices.length} invoice{savedInvoices.length !== 1 ? “s” : “”} stored in session
{savedInvoices.length > 0 && (
{ label: “Total”, value: savedInvoices.length, color: dark ? “text-gray-100” : “text-gray-800” },
{ label: “Paid”, value: savedInvoices.filter((i) => i.status === “paid”).length, color: “text-green-500” },
{ label: “Pending”, value: savedInvoices.filter((i) => [“sent”, “draft”].includes(i.status)).length, color: “text-amber-500” },
{ label: “Overdue”, value: savedInvoices.filter((i) => i.status === “overdue”).length, color: “text-red-500” },
].map((s) => (
))}
)}
{savedInvoices.length === 0 ? (
No saved invoices yet
) : (
const { total } = calcTotals(inv);
return (
);
})}
)}