React single-page application for the InvoicePro invoicing platform: onboarding, company and customer management, an item catalogue, GST-aware invoice creation (CGST / SGST / IGST), browser-side PDF export, dashboards, and reports.
Companion repository — the Express + Prisma API this client consumes lives at rupeshv2121/invoice_generator_server.
- System Architecture
- Stack
- Project Structure
- Getting Started
- Environment Variables
- Scripts
- Application Architecture
- Routing
- State Management
- API Layer
- Feature Modules
- Core Pipelines
- UI System
- Deployment
- Known Gaps
Three tiers, plus Supabase as an external identity provider. This client never talks to the database — every read and write goes through the API, and the browser holds only a short-lived bearer token and the public Supabase anon key.
flowchart TB
subgraph Browser["Web-Client — Vite SPA"]
UI["Pages and UI components"]
CTX["AuthContext / SubscriptionContext"]
APIL["API layer (axios + useApi hook)"]
PDF["pdfService (jsPDF + autotable)"]
UI --> CTX --> APIL
UI --> PDF
end
subgraph Server["API (separate repo)"]
EXP["Express 5 — auth, company, customer,<br/>item, invoice, settings, subscription"]
PRISMA["Prisma Client"]
EXP --> PRISMA
end
subgraph Supabase["Supabase (managed)"]
SBAUTH["Auth — users, JWT issuance"]
SBPG[("PostgreSQL")]
end
APIL -- "HTTPS + Bearer JWT" --> EXP
EXP -- "token verify" --> SBAUTH
PRISMA --> SBPG
Design notes
- Supabase Auth is the identity source of truth. The client stores the issued
access_tokenand sends it as a bearer header; the API verifies it on every request. - Context over Redux. Session and subscription state live in two React contexts; page-level data is fetched per page rather than held in a global store.
- Client-side limit checks are affordances, not enforcement.
SubscriptionContextdisables actions before the user hits a rejection, but the API re-checks every limit. - Invoice totals shown during entry are a preview. The server recomputes all amounts on write, so the two must agree by using the same formulas — they do.
| Concern | Choice |
|---|---|
| Framework | React 18 (StrictMode, function components + hooks) |
| Build | Vite 5 with @vitejs/plugin-react |
| Routing | React Router 6 (BrowserRouter) |
| Styling | Tailwind CSS 3.4 + tailwind-merge, class-variance-authority, clsx |
| Forms | react-hook-form |
| Charts | Recharts, D3 |
| Icons | lucide-react |
| Animation | framer-motion |
| HTTP | axios |
| Auth | Supabase JS client + API-issued bearer tokens |
jspdf + jspdf-autotable |
|
| Notifications | react-toastify (mounted globally), react-hot-toast |
| Dates | date-fns |
invoice_generator/
├── index.html
├── vite.config.js
├── tailwind.config.js / postcss.config.js
└── src/
├── main.jsx # React root + global toast CSS
├── App.jsx # Routes + ToastContainer
├── Routes.jsx # providers, route table, error boundary
├── supabaseClient.js # browser Supabase client (anon key)
├── api/ # one module per backend resource
│ ├── api.js # axios instance + useApi() authed client
│ ├── auth.js customers.js dashboard.js
│ ├── invoice.js items.js myCompany.js subscription.js
├── context/
│ ├── AuthContext.jsx # session, token, login/register/logout
│ └── SubscriptionContext.jsx
├── components/
│ ├── ProtectedRoute.jsx ErrorBoundary.jsx ScrollToTop.jsx
│ ├── OauthCallback.jsx SubscriptionBanner.jsx SubscriptionStatusCard.jsx
│ ├── AppIcon.jsx AppImage.jsx
│ └── ui/ # Button, Input, Select, Checkbox, Modal,
│ # Header, Breadcrumb, SkeletonLoader, QuickActionButton
├── pages/ # one folder per route, each with components/
│ ├── landing-page/ login/ register/ pricing/ setup/
│ ├── dashboard/ invoice-creation/ invoice-list/ pdf-preview/
│ ├── customer-management/ items-management/ reports/
│ ├── company-profile/ company-management/ settings/
│ └── NotFound.jsx
├── services/
│ ├── pdfService.js # jsPDF invoice renderer
│ └── settingsService.js # terms & conditions lookup
├── styles/ # tailwind.css, index.css
└── utils/ # cn.js (class merge), numberUtils.js
Prerequisites: Node.js 18+, npm, and a running instance of the API server.
git clone https://github.com/rupeshv2121/invoice_generator.git
cd invoice_generator
npm install
# create .env — see below
npm start # http://localhost:5173The backend must be running (default http://localhost:3001) for anything past the landing
page to work. Clone and start it from
invoice_generator_server, and set
its FRONTEND_URL to http://localhost:5173 so CORS passes in the other direction.
Create .env in the repository root. Only VITE_-prefixed variables are exposed to the
bundle.
VITE_API_URL="http://localhost:3001"
VITE_SUPABASE_URL="https://<project-ref>.supabase.co"
VITE_SUPABASE_ANON_KEY="<anon-key>"| Variable | Required | Purpose |
|---|---|---|
VITE_API_URL |
yes | API base URL. Falls back to http://localhost:3001. |
VITE_SUPABASE_URL |
yes | Supabase project URL. |
VITE_SUPABASE_ANON_KEY |
yes | Public anon key — safe in the browser. Never put the service-role key here. |
VITE_API_URL must have no trailing slash, and must match the server's FRONTEND_URL
CORS setting in the other direction. .env is git-ignored — never commit real keys.
| Script | Action |
|---|---|
npm start |
Vite dev server with HMR |
npm run build |
vite build --sourcemap → dist/ |
npm run serve |
Preview the production build locally |
flowchart TB
M["main.jsx — createRoot + StrictMode"] --> A["App.jsx"]
A --> T["ToastContainer (global)"]
A --> R["Routes.jsx"]
R --> BR["BrowserRouter"]
BR --> AP["AuthProvider"]
AP --> SP["SubscriptionProvider"]
SP --> EB["ErrorBoundary"]
EB --> STT["ScrollToTop"]
STT --> RT["Route table"]
RT --> PUB["Public: /, /login, /register, /pricing, /auth/callback"]
RT --> PR["ProtectedRoute"]
PR --> APP["App pages: dashboard, invoices, customers,<br/>items, reports, settings, company-profile, setup"]
Providers nest deliberately: SubscriptionProvider sits inside AuthProvider because it
reads user from auth context and refetches whenever the session changes.
sequenceDiagram
participant P as Page (index.jsx)
participant H as useXService() hook
participant AX as useApi() axios client
participant AC as AuthContext
participant API as Express API
P->>H: getCustomers()
H->>AX: api.get("/api/customer")
AX->>AC: read token
AC-->>AX: access_token
AX->>API: GET + Authorization: Bearer
API-->>AX: JSON
AX-->>H: response.data
H-->>P: setState → render table
Defined in src/Routes.jsx.
| Path | Page | Access |
|---|---|---|
/ |
Landing page | Public |
/login |
Login | Public |
/register |
Registration | Public |
/pricing |
Plan comparison | Public |
/auth/callback |
OAuth callback handler | Public |
/setup |
Company-profile wizard | Semi-protected — session required, profile not |
/dashboard |
Metrics, charts, reminders | Protected |
/invoice-creation |
Invoice builder | Protected |
/invoice-list |
Invoice table + filters | Protected |
/pdf-preview |
Full-page invoice preview | Protected |
/customer-management |
Customer CRUD | Protected |
/items-management |
Item catalogue CRUD | Protected |
/company-profile |
Business details, GST, banking, logo | Protected |
/settings |
Invoice prefix, tax defaults, terms | Protected |
/reports |
Revenue, customer, GST reports | Protected |
* |
NotFound |
— |
AuthContext (src/context/AuthContext.jsx)
Owns the session and exposes
{ session, token, user, loading, setSession, login, register, loginWithProvider, logout }.
- Restore on load — reads
access_tokenfromlocalStorage, validates it withGET /api/auth/me, and clears it on failure. - Persist — an effect mirrors
session.access_tokenintolocalStorageon every change. login/register— post to/api/auth/*and apply the returned session.logout— clears storage and redirects to/login.loginWithProvider— currently throws; OAuth is not configured.
SubscriptionContext (src/context/SubscriptionContext.jsx)
Fetches GET /api/subscription/current whenever user changes and derives UI state:
hasActiveSubscription(), canCreateInvoice(), getRemainingInvoices(),
getDaysRemaining(), getSubscriptionPlan(), getSubscriptionStatus(),
isTrialActive(), isTrialExpiring() (≤ 2 days left), hasTrialExpired(),
isSubscriptionExpired().
These drive SubscriptionBanner and SubscriptionStatusCard and disable actions before the
user hits a server-side rejection. They are UI affordances only — the server re-checks
every limit, so bypassing them client-side gains nothing.
Plans the API reports (-1 = unlimited):
| Plan | ₹/month | Invoices | Customers | Items |
|---|---|---|---|---|
FREE (7-day trial) |
0 | 10 | 50 | 100 |
BASIC |
499 | 100 | 200 | 500 |
PROFESSIONAL |
999 | ∞ | ∞ | ∞ |
ENTERPRISE |
2499 | ∞ | ∞ | ∞ |
src/api/api.js exports two things:
apiClient— a plain axios instance for unauthenticated calls (login, register, session restore).useApi()— a hook returning an axios instance with a request interceptor that attaches the currentAuthContexttoken asAuthorization: Bearer <token>on every call.
Each resource module wraps useApi() in its own hook, so pages call domain methods rather
than URLs:
| Module | Hook | Endpoints |
|---|---|---|
auth.js |
(plain functions) | POST /api/auth/login, POST /api/auth/register, logout() |
myCompany.js |
useMyCompanyService |
GET/POST /api/company, PATCH /api/company/:id |
customers.js |
useCustomersService |
GET/POST /api/customer, GET /api/customer/:id, PATCH, DELETE, GET /api/customer/stats |
items.js |
useItemService |
GET/POST /api/item, GET /api/item/:id, PATCH, DELETE, GET /api/item/stats |
invoice.js |
useInvoiceService |
GET/POST /api/invoice, GET /api/invoice/:id, PATCH, DELETE |
subscription.js |
useSubscriptionService |
/api/subscription/current, /status, /plans, POST /trial, POST /cancel |
dashboard.js |
useDashboardService |
/api/dashboard/stats, /api/dashboard/overdue, recent invoices — |
landing-page/ composes HeroSection, FeaturesSection, BenefitsSection,
DemoSection, SocialProofSection, PricingSection, CTASection, Navigation, Footer.
/pricing renders plan cards from GET /api/subscription/plans.
register/ → setup/ collects business identity, GST registration, address, and banking
details, creates the company profile, then starts the 7-day trial.
MetricsCard, InvoiceChart, RevenueChart (Recharts), GSTSummary,
RecentInvoicesTable, PaymentReminders, QuickActions.
The most involved page. CompanyCustomerSelector picks the parties;
CompanyDetailsSection / CustomerDetailsSection / BankDetailsSection render the
document header; InvoiceDetailsSection captures number, dates, and shipping metadata
(marka, date of supply, state code, transportation); InvoiceItemsTable + ItemSelector
manage lines with catalogue autocomplete or ad-hoc entry; InvoiceTotalsSection shows live
subtotal, CGST, SGST, IGST, and grand total; InvoicePreviewModal and
ExportInvoiceTemplate handle preview and export.
InvoiceFilters (search, status, customer, date range), InvoiceTable,
InvoiceStatusBadge, InvoiceActionMenu, BulkActions, InvoicePagination. Status
lifecycle is DRAFT → SENT → PAID / OVERDUE / CANCELLED.
Matching CRUD modules: *Filters, *Table, *Stats, and a create/edit modal.
CustomerHistoryModal shows a customer's invoice history.
CompanyDetailsForm, GSTComplianceSection (GSTIN, PAN, IEC, ARN), BankDetailsSection,
LogoUploadSection, InvoiceTemplateSettings.
ReportTypeSelector, DateRangeFilter, RevenueChart, CustomerAnalysis,
GSTComplianceSection, ExportControls, backed by /api/settings/reports/*.
flowchart LR
R["/register"] --> SB["Supabase user created via API"]
SB --> SETUP["/setup — company profile wizard"]
SETUP --> CP["POST /api/company"]
CP --> TRIAL["POST /api/subscription/trial<br/>7-day FREE trial"]
TRIAL --> DASH["/dashboard"]
The client composes the document; the server re-computes every number before persisting.
flowchart TD
A["Invoice creation page"] --> B["Select company + customer"]
B --> C["Add line items (ItemSelector or manual)"]
C --> D["InvoiceTotalsSection — live preview"]
D --> E["POST /api/invoice"]
E --> F{"API response"}
F -- "403 SUBSCRIPTION_REQUIRED" --> F1["Prompt to subscribe"]
F -- "403 LIMIT_REACHED" --> F2["Show remaining + upgrade CTA"]
F -- "400" --> F3["Surface field errors"]
F -- "201" --> G["Invoice saved with server number<br/>INV-0001 and final totals"]
G --> H["Preview / PDF export"]
Tax maths used for the on-screen preview, matching the server exactly:
amount = quantity × rate
cgst = amount × cgstRate / 100
sgst = amount × sgstRate / 100
igst = amount × igstRate / 100
lineTotal = amount + cgst + sgst + igst
totalAmount = Σ amount + Σ cgst + Σ sgst + Σ igst
Defaults are 9% CGST + 9% SGST intra-state and 18% IGST inter-state, overridable per company
in Settings and per line at entry time. Invoice numbers are assigned by the server
(prefix + zero-padded counter), not the client.
Rendering happens entirely in the browser via jsPDF and jspdf-autotable — no server
round-trip, no headless Chrome.
flowchart LR
I["Invoice data"] --> S["settingsService<br/>terms and conditions"]
I --> N["numberToWordsIndian()<br/>Crore / Lakh / Thousand"]
I --> F["formatIndianNumber()<br/>1,23,456.00 grouping"]
S --> P["pdfService"]
N --> P
F --> P
P --> O["Download / print-ready invoice"]
src/services/pdfService.js provides:
formatIndianNumber(num)— two decimals with Indian digit grouping (1,23,456.00).numberToWordsIndian(num)— amount in words on the Crore / Lakh / Thousand scale, as required on a GST invoice.- Terms and conditions pulled through
settingsService.getTermsAndConditions().
/pdf-preview renders the same layout as HTML so the document can be checked on screen
before export.
- Tailwind CSS with a minimal config; composition helper
cn()insrc/utils/cn.jsmergesclsxoutput throughtailwind-mergeso later classes win predictably. components/ui/holds the shared primitives —Button(variant-driven viaclass-variance-authority),Input,Select,Checkbox,Modal,Header,Breadcrumb,QuickActionButton,SkeletonLoader.AppIconwrapslucide-reactfor consistent icon sizing;AppImagehandles image fallbacks.- Feedback —
ToastContaineris mounted once inApp.jsx(top-right, 3s autoclose);SkeletonLoadercovers loading states. - Motion —
framer-motionfor page and modal transitions. - Layouts are responsive down to mobile widths throughout.
Static build, deployed to Vercel.
npm run build # → dist/ (with sourcemaps)
npm run serve # verify the production bundle locallyChecklist:
- Set
VITE_API_URL,VITE_SUPABASE_URL, andVITE_SUPABASE_ANON_KEYin the hosting project — Vite inlines them at build time, so a change requires a rebuild. - Point
VITE_API_URLat the deployed API and set the server'sFRONTEND_URLto this app's origin (no trailing slash) so CORS passes. - This is an SPA using
BrowserRouter— all paths must rewrite toindex.html, or a hard refresh on/dashboard404s. dist/is build output and is git-ignored; keep it that way.
Documented deliberately so they are not rediscovered as bugs.
| Gap | Detail |
|---|---|
| Dashboard endpoints do not exist | src/api/dashboard.js calls /api/dashboard/stats and /api/dashboard/overdue; the server mounts no /api/dashboard router. The equivalent data is at /api/settings/reports/dashboard. |
company-management page unrouted |
The folder exists under src/pages/company-management/ but no route references it. |
| OAuth scaffolded, not wired | /auth/callback and OauthCallback.jsx exist, but loginWithProvider() throws "not configured". |
| Redux installed, unused | |
| Two toast libraries | Both react-toastify (mounted globally) and react-hot-toast are installed. |
Token in localStorage |
Convenient for session restore, but readable by any script on the origin; consider httpOnly cookies if XSS risk matters for your deployment. |
| No tests | Testing Library packages are installed but there is no test script or test file. |
Pinned react-router-dom 6.0.2 |
An early v6 release; upgrading unlocks newer router APIs. |
No license file is present. Add one before distributing.