Invoice Frontend: A Type-Safe React Invoicing Application
What Is Invoice Frontend?
invoice-frontend is the client-side application for a full-stack invoice management system. It pairs with invoice-backend — a Laravel REST API — to provide a complete invoicing workflow: creating clients, generating invoices, tracking payments, and exporting records.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | React 19 + TypeScript |
| Build Tool | Vite |
| UI Components | shadcn/ui (components.json) |
| Styling | Tailwind CSS |
| Forms | React Hook Form + @hookform/resolvers |
| HTTP Client | Axios |
| Date Handling | date-fns |
| Icons | lucide-react |
| Linter | oxlint |
| Deployment | Vercel (vercel.json) |
Project Structure
src/
├── pages/ # Route-level page components
├── components/ # Reusable UI components (shadcn/ui + custom)
├── contexts/ # React Context for auth and app state
├── lib/ # Axios instance, utility functions
├── data/ # Static/seed data
└── App.tsx # Routes and layout
Why TypeScript?
This project uses TypeScript throughout — including strict tsconfig settings. The invoice domain has complex, interrelated data shapes: clients, line items, tax rates, payment statuses. TypeScript catches mismatches between the API response and the UI at compile time, rather than at runtime in production.
// Strict typing means the form knows exactly what shape invoice data takes
interface InvoiceLineItem {
description: string;
quantity: number;
unitPrice: number;
taxRate: number;
}
interface Invoice {
id: string;
clientId: string;
lineItems: InvoiceLineItem[];
status: 'draft' | 'sent' | 'paid' | 'overdue';
issuedAt: string; // ISO date string
dueAt: string;
}
Form Handling with React Hook Form
Invoice forms are complex — line items can be added/removed dynamically, totals recalculate on every change, and everything needs validation before submission. React Hook Form handles this without re-rendering the entire form on each keystroke.
@hookform/resolvers connects the form to a Zod schema for declarative validation:
const invoiceSchema = z.object({
clientId: z.string().min(1, 'Client is required'),
dueAt: z.string().min(1, 'Due date is required'),
lineItems: z.array(z.object({
description: z.string().min(1),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
})).min(1, 'At least one line item is required'),
});
shadcn/ui Components
Rather than a traditional component library, this project uses shadcn/ui — copy-paste components built on Radix UI primitives and styled with Tailwind. The components.json in the root configures the component registry. This approach means the components live in the codebase and can be freely modified, unlike a locked-in external library.
Date Handling
Invoice dates are handled with date-fns for formatting, parsing, and arithmetic. Due date calculation (e.g., Net 30, Net 60) is computed client-side and sent to the backend as ISO strings.
Deployment
Deployed on Vercel with SPA rewrite rules in vercel.json to handle client-side routing correctly.