DEV Community

Cover image for How I Built a Complete Business Management Dashboard in React (No Dependencies)
Aura Base
Aura Base

Posted on

How I Built a Complete Business Management Dashboard in React (No Dependencies)

How I Built a Complete Business Management Dashboard in React (No Dependencies)


Every SaaS project I’ve worked on starts the same way. Before you can build your actual product — the thing that makes you money — you have to build the scaffolding. The dashboard. The sales tracker. The customer list. The invoice system. The landing page. The paywall.

It takes weeks. And you build the same thing every single time.

I got tired of it. So I built it once, properly, and packaged it as a reusable template.

This is the story of how I built Aura Base — a complete business management dashboard in React with zero external dependencies — and every architectural decision I made along the way.


What I Was Building

Before writing a single line of code, I mapped out every screen a typical business management SaaS needs:

  • Landing page — hero, features, pricing, contact form
  • Purchase page — Card + PayPal payment buttons
  • Dashboard — KPI cards, 7-day revenue chart, top products, top customers
  • Sales tracking — log sales with customer autocomplete
  • Customer CRM — contacts with purchase history
  • Invoice management — line items, paid/unpaid toggle
  • PDF export — for sales, customers, and invoices
  • Settings — business name, currency, danger zone
  • Paywall system — free tier with upgrade flow

That’s 8 pages, 7 reusable components, and a complete payment flow. Most developers would reach for Next.js, MUI, Recharts, jsPDF, React Query, and a dozen other packages. I didn’t.


Decision 1: Zero External Dependencies

This was the hardest constraint I set for myself, and the most valuable.

Most dashboard templates have 50+ dependencies. They’re bloated, they break when packages update, and they force you to learn 5 different APIs just to customize the thing you bought.

I wanted buyers to open the project and understand it immediately. If everything is built from scratch, there’s nothing to look up.

The chart: Instead of Recharts or Chart.js, I built a bar chart in 15 lines:

function MiniBarChart({ data, height = 120, formatValue }) {
  const maxValue = Math.max(...data.map(d => d.value), 1);

  return (
    <div style={{ display: "flex", alignItems: "flex-end", gap: 6, height }}>
      {data.map((point, index) => (
        <div key={index} style={{ display: "flex", flexDirection: "column", alignItems: "center", flex: 1 }}>
          <div style={{
            width: "100%",
            height: Math.max(3, (point.value / maxValue) * (height - 28)),
            background: `linear-gradient(180deg, #52B788, #2D6A4F)`,
            borderRadius: "3px 3px 0 0",
            transition: "height 0.4s",
          }} />
          <span style={{ fontSize: 8, color: "#5A7D68", marginTop: 3 }}>
            {point.label}
          </span>
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It takes data, finds the max value, and renders proportional bars. That’s all a 7-day revenue chart needs to do. No library required.

The PDF export: Instead of jsPDF or html2canvas, I open a new browser tab with print-optimized HTML:

function openPrintableReport(title, headers, tableRows, businessName) {
  const printWindow = window.open("", "_blank");

  printWindow.document.write(`<!DOCTYPE html>
    <html>
      <head>
        <style>
          table { width: 100%; border-collapse: collapse; }
          th { background: #1B4332; color: #FEFAE0; padding: 9px 12px; }
          td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; }
          .save-button {
            position: fixed; top: 16px; right: 16px;
            background: #1B4332; color: white;
            border: none; padding: 10px 20px;
            border-radius: 8px; cursor: pointer;
          }
          @media print { .save-button { display: none; } }
        </style>
      </head>
      <body>
        <button class="save-button" onclick="window.print()">
          Save as PDF
        </button>
        <h1>${title}${businessName}</h1>
        <table>
          <thead>
            <tr>${headers.map(h => `<th>${h}</th>`).join("")}</tr>
          </thead>
          <tbody>${tableRows}</tbody>
        </table>
      </body>
    </html>
  `);
}
Enter fullscreen mode Exit fullscreen mode

The user clicks “Save as PDF” → browser print dialog opens → they save as PDF. Clean, dependency-free, and works everywhere.


Decision 2: Inline Styles Over CSS Framework

This is the decision that surprises developers the most. No Tailwind, no CSS Modules, no styled-components.

My reasoning was simple: buyers shouldn’t need to learn your styling system to customize your template.

With Tailwind, buyers need to know Tailwind. With CSS Modules, they have to hunt through multiple files. With inline styles, the style is right there, next to the element, in plain JavaScript objects.

Want to change the card background color? Find background: theme.cardBackground and change it in theme.js. Done.

// theme.js — every color in one place
const theme = {
  background: "#091A10",
  cardBackground: "#0F2518",
  hoverBackground: "#153120",
  cream: "#FEFAE0",
  gold: "#D4A853",
  greenLight: "#52B788",
  red: "#E07A5F",
};
Enter fullscreen mode Exit fullscreen mode

Every component imports from this one file. Change gold from #D4A853 to #F59E0B and the entire app updates instantly. No grep, no find-and-replace, no Tailwind config.

For buyers who want Tailwind, migrating is straightforward — the inline styles are self-documenting. background: theme.cardBackground tells you exactly what CSS property to replace.


Decision 3: The Config File

Everything a buyer needs to customize lives in one file:

// src/config/config.js
const CONFIG = {
  // Branding
  appName: "",
  appLogo: "",
  tagline: "",

  // Pricing
  price: "",
  productName: "",

  // Contact
  contactEmail: "",

  // Free tier
  freeLimit: 5,

  // Payment links
  cardPaymentUrl: "",
  paypalUrl: "",

  // Defaults
  defaultCurrency: "USD",
  defaultBusinessName: "My Business",
};
Enter fullscreen mode Exit fullscreen mode

Every field is blank by default. The buyer fills in their values, and those values propagate everywhere — the navbar, sidebar, landing page, purchase page, paywall popup, footer, contact form, and PDF reports.

The app name appears in 11 different places. With this config, the buyer changes it once.

Decision 4: The Swappable Data Layer

This is the decision I’m most proud of.

Every template I’ve ever bought couples the UI directly to a specific backend. Firebase templates assume Firebase. Supabase templates assume Supabase. You can’t swap it without touching 20 files.

I put every data operation in a single file:

// src/services/db.js

export async function loadData() {
  // Currently: localStorage
  // Replace this entire function with your API call
  try {
    const raw = localStorage.getItem(CONFIG.storageKey);
    if (raw) return JSON.parse(raw);
  } catch (error) {
    console.error("Failed to load data:", error);
  }
  return { ...defaultData };
}

export async function saveData(data) {
  // Replace this with your database write
  localStorage.setItem(CONFIG.storageKey, JSON.stringify(data));
}

export async function activatePro() {
  // Replace this with your payment verification
  const data = await loadData();
  data.isPaid = true;
  await saveData(data);
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Want to use Firebase? Replace loadData with getDoc. Replace saveData with setDoc. The rest of the app — all 8 pages, 7 components — doesn’t change at all. They only import from db.js.

This also means the template ships with zero backend requirements. It works out of the box with localStorage. No Firebase project setup, no Supabase credentials, no API keys required just to see it run.


Decision 5: The Paywall System

Most templates either have no paywall or have a fake one. I built a real freemium system.

Free users get 5 sales and 5 customers. The check happens before every add operation:

function handleOpenNewSale() {
  // Check limit before opening the form
  if (!data.isPaid && data.sales.length >= CONFIG.freeLimit) {
    onPaywall(); // Show upgrade popup
    return;
  }
  setShowModal(true);
}
Enter fullscreen mode Exit fullscreen mode

The counter updates in real time in the page header:

<p>
  {data.sales.length} total
  {!data.isPaid && (
    <span style={{ color: theme.gold }}>
      · {Math.max(0, CONFIG.freeLimit - data.sales.length)} free left
    </span>
  )}
</p>
Enter fullscreen mode Exit fullscreen mode

When the limit is hit, an animated popup appears:

function PaywallPopup({ open, onClose, onUpgrade }) {
  if (!open) return null;
  return (
    <div style={{ position: "fixed", inset: 0, backdropFilter: "blur(8px)" }}>
      <div style={{ background: theme.cardBackground, borderRadius: 20, padding: 40 }}>
        <h3>Free Limit Reached</h3>
        <p>
          You've used all {CONFIG.freeLimit} free entries.
          Upgrade to {CONFIG.productName} for unlimited access.
        </p>
        <button onClick={onUpgrade}>
          Upgrade — {CONFIG.price}
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

After payment, isPaid: true is saved to localStorage permanently. The checks are bypassed, the upgrade button disappears from the sidebar, and a “Pro Activated” badge appears. The user never sees the paywall again.


The File Structure

One of the things buyers care most about is being able to navigate the code. I organized everything so the purpose of each file is obvious:

src/
├── config/
│   ├── config.js      ← All settings (name, price, payment links)
│   └── theme.js       ← All colors (change once, updates everywhere)
│
├── components/
│   ├── index.js       ← Clean single import point
│   ├── Button.jsx     ← 7 variants (primary, ghost, danger, pdf...)
│   ├── Inputs.jsx     ← TextInput + SelectInput
│   ├── Modal.jsx      ← Modal + ConfirmDialog
│   ├── DataDisplay.jsx ← DataTable + StatCard + MiniBarChart
│   ├── Overlays.jsx   ← PaywallPopup + CustomerAutocomplete
│   └── Icon.jsx       ← SVG icon library (24 icons)
│
├── pages/
│   ├── LandingPage.jsx
│   ├── PurchasePage.jsx
│   ├── AppShell.jsx   ← Sidebar layout + routing
│   ├── DashboardPage.jsx
│   ├── SalesPage.jsx
│   ├── CustomersPage.jsx
│   ├── InvoicesPage.jsx
│   └── SettingsPage.jsx
│
├── utils/
│   ├── formatters.js  ← formatCurrency, formatDate, generateId
│   └── pdfExport.js   ← exportSalesAsPDF, exportCustomersAsPDF...
│
├── services/
│   └── db.js          ← The only file you touch to change backends
│
├── App.jsx            ← Root router (landing → purchase → app)
└── styles.css         ← Animations + responsive breakpoints
Enter fullscreen mode Exit fullscreen mode

Pages import from components using a single line:

import { Button, Modal, DataTable, StatCard, Icon } from "../components";
Enter fullscreen mode Exit fullscreen mode

No hunting through folders. The index.js handles all the exports.


The Result

  • 24 files in a clean, navigable structure
  • 2,000+ lines of readable, commented code
  • 8 complete pages covering the full SaaS product flow
  • Zero external dependencies — just React 18
  • Full README with setup, customization, and architecture docs
  • One config file for all branding and payment customization
  • One service file to swap the entire backend

The dark green and cream color palette was a deliberate choice — I wanted something that stood out from the sea of blue/purple Material dashboards. The unique theme is part of the product.


What I Learned

Build for the person after you, not for yourself. Every decision I made was filtered through “will a developer who didn’t build this understand it immediately?” That meant longer variable names, more comments, a rigid folder structure, and no clever tricks.

Constraints force better decisions. Zero dependencies felt impossible at first. But it forced me to think about what each library was actually doing, then write simpler versions of just the parts I needed.

Documentation is half the product. The README took as long to write as some of the pages. But a template with clear documentation gets 5-star reviews. A template without it gets support tickets.


The Template

If you want to skip the 40+ hours of building these screens from scratch, the full template is available with complete source code, documentation, and a commercial license.

[Aura Base on Gumroad — $29]

Includes: 8 pages · 7 components · PDF export · Paywall system · One config file · Swappable backend · Full README


If this was useful, drop a ❤️ or leave a comment with questions about the architecture. Happy to go deeper on any of these decisions.

Top comments (0)