Apollo
Next.js Guru & Modern Web Advisor. Expert guidance on React, TypeScript, Tailwind CSS, shadcn/ui, and modern frontend architecture.
A Quick Note On Working with Apollo
Apollo is your Next.js Guru & Modern Web Advisor — expert guidance on React, TypeScript, Tailwind CSS, shadcn/ui, and modern frontend architecture. In practice, the work he handles is centered on data-driven admin interfaces and web portals — the same domain as Hermes, but with a modern React stack. Think server-rendered admin panels with data tables, filters, charts, document viewers, and payment flows — Next.js 15 with App Router and Server Components, TypeScript 5, Tailwind CSS v4 with shadcn/ui (Radix primitives + lucide-react icons), Drizzle ORM for data persistence, and Stripe/PayPal SDKs for payment processing.
The chat session below is a real-world example of how a conversation with Apollo might look — from discussing requirements, through creating a structured task file, to pair-programming the implementation top to bottom. Feel free to read through it, pick up patterns, get a sense of the rhythm.
But you don't have to follow any of it. You don't need to build a billing portal. You don't need to use pair programming. You don't need to create a task file first. Just open the chat with Apollo and tell him what you need — an admin dashboard, a self-service portal, a payment flow, whatever modern web app you have in mind. Build your own way of working together. He'll adapt.
Go chat with Apollo. Go build stuff.
Billing Portal — Admin Dashboard & Customer Portal UI
Context. The user previously worked with Athena to create a Billing Portal PRD (see Athena → New Billing Portal), then worked with Hephaestus to build the backend payment reminders. ReportBurster handles bill generation and initial email delivery. The missing piece — the admin dashboard and customer-facing portal UI — is Apollo's domain.
The approach: first, the user and Apollo discuss requirements and create a structured
tasks file (.org). Then they implement each task one by one in a fast-paced
pair-programming session — Apollo guides, provides code snippets, and pushes toward
completion while the user stays in full control, understanding where each piece of code
comes from and where it goes.
Why pair programming and not a single-shot agent? A capable coding agent like Claude Code could take the tasks file as input and produce the complete working app in one shot — and that is a perfectly legitimate way to work (we fully support this too). But for the purpose of this guide, we deliberately chose the pair-programming approach: it's still fast, and it gives the user a "stay in control and understand what's happening" mode of working.
Here is the PRD document that Athena created — the starting point for this session:
And here are the UI mockups Athena designed — the visual targets for the portal:
Framing the Problem
Good evening! Apollo here — your Next.js Guru & Modern Web Advisor. Happy to jump in on the UI side.
I can see Athena's PRD — billing-portal-prd.org in her artifacts folder — and her two mockups (bills list + bill detail). Great news: the next-playground already has a complete Invoice management system with Drizzle schema, API routes, admin pages (shadcn Table, forms, detail), and a full payment integration (StripeCheckoutForm + PayPalCheckout + InvoicePayment dialog). We follow these exact patterns for Bills.
The mockups are in Bootstrap 5, but we'll translate to Tailwind CSS + shadcn/ui — the semantics are the same. Bootstrap cards become shadcn Card, badges become Badge, tables become Table.
Here's the architecture:
Schema & API (data layer):
- bills table in Drizzle schema (follow invoices pattern)
- /api/bills routes (GET list, POST create, GET/PUT/DELETE by id)
Admin Pages (under app/(admin)/admin/bills/):
- Bills list — shadcn Table with search, status Badge, actions
- Bill detail — status banner, amounts, "Pay Now" BillPayment dialog
- Create/edit forms — shadcn Input, Select, Label
Portal Pages (under app/(main)/bills/):
- Bills homepage — Tailwind card grid with status badges
- Bill detail — Server Component with payment button
Payment Component:
- BillPayment.tsx — follows InvoicePayment.tsx (Dialog + Stripe/PayPal toggle)
Server Components for data fetching (list, detail), Client Components for interactive elements (forms, payment dialog). The existing InvoicePayment.tsx is a perfect blueprint — same Dialog, same provider wrappers, just different API endpoints.
Reviewing Athena's Mockups
Here's the translation for both mockups:
Bills list mockup → Tailwind translation:
- Bootstrap
card→ shadcnCard+CardHeader+CardContent+CardFooter - Bootstrap
badge bg-success/danger/warning→ shadcnBadgewithvariant="default"+ Tailwind color classes (bg-emerald-100 text-emerald-800,bg-red-100 text-red-800,bg-amber-100 text-amber-800) - Bootstrap grid
col-md-6 col-lg-4→ Tailwindgrid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 - Bootstrap
btn btn-primary→ shadcnButton
Bill detail mockup → Tailwind translation:
- Bootstrap layout → Tailwind flex/grid with
gap-6,p-6,rounded-lg - Amount display →
text-2xl font-boldwith proper formatting - Status badge → same Badge component as list
- "Pay Now" button → shadcn Button that opens BillPayment Dialog
The semantic structure is identical — we're just swapping class names. Our shadcn components (Card, Badge, Button, Table, Dialog) already exist in the project's components/ui/ folder.
Technical Approach
Follow the existing Invoice implementation exactly. Everything we need already exists:
lib/db/schema.ts→ add bills table (copy invoices table, adjust columns)app/api/invoices/→ copy toapp/api/bills/(change schema references)app/(admin)/admin/invoices/→ copy toadmin/bills/(swap components)components/payments/InvoicePayment.tsx→ copy toBillPayment.tsx- Payment API routes → add bill-specific variants
Architecture:
- 1. Schema (Drizzle) — single source of truth
- 2. API routes (Next.js Route Handlers) — data access
- 3. Admin pages (Server + Client Components) — management
- 4. Portal pages (Server Components) — customer-facing
- 5. Payment (existing Stripe/PayPal providers) — reuse
Shall we create the task file?
Creating the Task File
Here's the task file — I'll save it as billing-portal-ui-tasks.org in my artifacts folder.
Pair-Programming Session
How this works. The user writes all the code — Apollo provides the snippets, exact file paths, and placement instructions. They work through the 8 tasks top to bottom. Watch how fast it goes once the groundwork (PRD + task file) is in place.
That's exactly how I operate — mentored pair-development. You're in the driver's seat, I navigate.
Here's the rhythm:
- 1. I give you the task + code snippet + exact file path
- 2. You paste it in
- 3. You tell me "done" or "I have a question"
- 4. We move to the next task
No detours, no over-engineering. We ship a working billing portal UI today.
One thing before we touch any code. Let's make a backup:
| 1 | Copy _apps/flowkraft/next-playground |
| 2 | to _apps/flowkraft/next-playground-Copy |
That gives us a clean reference of the original code in case we break something. We keep working inside the original — it's already wired into Docker Compose, the UI can build and start/stop it, everything's connected.
Also — my approach while we develop: we only add new code. Don't touch or delete existing classes, even if they look like stuff we don't need. The Invoice and Payslip code is useful as working examples while we build — you might need to reference how they handle schemas, routes, or components.
Once everything works and we've tested it, we spend 5 minutes at the end to clean up inherited code we don't need. Sound good?
Task 1 — Add bills table to Drizzle schema.
Open lib/db/schema.ts — you'll see the invoices table definition. We add bills right below it, same pattern:
| 1 | // Add to lib/db/schema.ts after invoices table |
| 2 | |
| 3 | export const bills = sqliteTable('bills', { |
| 4 | id: integer('id').primaryKey({ autoIncrement: true }), |
| 5 | billNumber: text('bill_number').notNull().unique(), |
| 6 | customerName: text('customer_name').notNull(), |
| 7 | customerEmail: text('customer_email').notNull(), |
| 8 | customerAddress: text('customer_address'), |
| 9 | |
| 10 | subtotal: real('subtotal').default(0), |
| 11 | taxRate: real('tax_rate').default(0), |
| 12 | taxAmount: real('tax_amount').default(0), |
| 13 | discount: real('discount').default(0), |
| 14 | totalAmount: real('total_amount').default(0), |
| 15 | |
| 16 | issueDate: text('issue_date').notNull(), |
| 17 | dueDate: text('due_date'), |
| 18 | |
| 19 | status: text('status').default('draft'), // draft, sent, paid, overdue, cancelled |
| 20 | notes: text('notes'), |
| 21 | |
| 22 | // Payment tracking |
| 23 | paidAt: text('paid_at'), |
| 24 | paymentMethod: text('payment_method'), // stripe, paypal |
| 25 | paymentReference: text('payment_reference'), |
| 26 | |
| 27 | // Reminder tracking (Hephaestus's backend) |
| 28 | reminderCount: integer('reminder_count').default(0), |
| 29 | lastReminderDate: text('last_reminder_date'), |
| 30 | |
| 31 | createdAt: text('created_at').default(sql`(datetime('now'))`), |
| 32 | updatedAt: text('updated_at').default(sql`(datetime('now'))`), |
| 33 | }); |
Now create the API route — app/api/bills/route.ts. Copy from app/api/invoices/route.ts and change the table reference:
| 1 | import { db } from '@/lib/db'; |
| 2 | import { bills } from '@/lib/db/schema'; |
| 3 | import { desc, like, or } from 'drizzle-orm'; |
| 4 | import { NextResponse } from 'next/server'; |
| 5 | |
| 6 | export async function GET(request: Request) { |
| 7 | const { searchParams } = new URL(request.url); |
| 8 | const q = searchParams.get('q'); |
| 9 | |
| 10 | let result; |
| 11 | if (q) { |
| 12 | result = await db.select().from(bills).where( |
| 13 | or( |
| 14 | like(bills.billNumber, `%${q}%`), |
| 15 | like(bills.customerName, `%${q}%`) |
| 16 | ) |
| 17 | ).orderBy(desc(bills.createdAt)); |
| 18 | } else { |
| 19 | result = await db.select().from(bills) |
| 20 | .orderBy(desc(bills.createdAt)); |
| 21 | } |
| 22 | return NextResponse.json(result); |
| 23 | } |
| 24 | |
| 25 | export async function POST(request: Request) { |
| 26 | const body = await request.json(); |
| 27 | const result = await db.insert(bills).values(body) |
| 28 | .returning(); |
| 29 | return NextResponse.json(result[0], { status: 201 }); |
| 30 | } |
Tasks 2 and 3 — Admin bills pages.
Create app/(admin)/admin/bills/page.tsx — follow admin/invoices/page.tsx:
| 1 | import Link from 'next/link'; |
| 2 | import { db } from '@/lib/db'; |
| 3 | import { bills } from '@/lib/db/schema'; |
| 4 | import { desc } from 'drizzle-orm'; |
| 5 | import { Badge } from '@/components/ui/badge'; |
| 6 | import { Button } from '@/components/ui/button'; |
| 7 | import { |
| 8 | Table, TableBody, TableCell, |
| 9 | TableHead, TableHeader, TableRow, |
| 10 | } from '@/components/ui/table'; |
| 11 | import { Plus, Eye, Pencil } from 'lucide-react'; |
| 12 | |
| 13 | const statusColors: Record<string, string> = { |
| 14 | paid: 'bg-emerald-100 text-emerald-800', |
| 15 | overdue: 'bg-red-100 text-red-800', |
| 16 | sent: 'bg-blue-100 text-blue-800', |
| 17 | draft: 'bg-slate-100 text-slate-800', |
| 18 | cancelled: 'bg-gray-100 text-gray-800', |
| 19 | }; |
| 20 | |
| 21 | export default async function BillsPage() { |
| 22 | const allBills = await db.select().from(bills) |
| 23 | .orderBy(desc(bills.createdAt)); |
| 24 | |
| 25 | return ( |
| 26 | <div className="space-y-4"> |
| 27 | <div className="flex items-center justify-between"> |
| 28 | <h2 className="text-2xl font-bold">Bills</h2> |
| 29 | <Link href="/admin/bills/new"> |
| 30 | <Button size="sm"> |
| 31 | <Plus className="h-4 w-4 mr-1" /> New Bill |
| 32 | </Button> |
| 33 | </Link> |
| 34 | </div> |
| 35 | <Table> |
| 36 | <TableHeader> |
| 37 | <TableRow> |
| 38 | <TableHead>Bill #</TableHead> |
| 39 | <TableHead>Customer</TableHead> |
| 40 | <TableHead>Due Date</TableHead> |
| 41 | <TableHead className="text-right">Amount</TableHead> |
| 42 | <TableHead>Status</TableHead> |
| 43 | <TableHead>Actions</TableHead> |
| 44 | </TableRow> |
| 45 | </TableHeader> |
| 46 | <TableBody> |
| 47 | {allBills.map((bill) => ( |
| 48 | <TableRow key={bill.id}> |
| 49 | <TableCell>{bill.billNumber}</TableCell> |
| 50 | <TableCell> |
| 51 | <div>{bill.customerName}</div> |
| 52 | <div className="text-sm text-muted-foreground"> |
| 53 | {bill.customerEmail}</div> |
| 54 | </TableCell> |
| 55 | <TableCell>{bill.dueDate}</TableCell> |
| 56 | <TableCell className="text-right font-medium"> |
| 57 | ${bill.totalAmount?.toFixed(2)}</TableCell> |
| 58 | <TableCell> |
| 59 | <Badge className={statusColors[bill.status!]}> |
| 60 | {bill.status}</Badge> |
| 61 | </TableCell> |
| 62 | <TableCell className="flex gap-1"> |
| 63 | <Link href={`/admin/bills/${bill.id}`}> |
| 64 | <Button variant="outline" size="icon"> |
| 65 | <Eye className="h-4 w-4" /> |
| 66 | </Button> |
| 67 | </Link> |
| 68 | <Link href={`/admin/bills/${bill.id}/edit`}> |
| 69 | <Button variant="outline" size="icon"> |
| 70 | <Pencil className="h-4 w-4" /> |
| 71 | </Button> |
| 72 | </Link> |
| 73 | </TableCell> |
| 74 | </TableRow> |
| 75 | ))} |
| 76 | </TableBody> |
| 77 | </Table> |
| 78 | </div> |
| 79 | ); |
| 80 | } |
Tasks 4 and 5 — Portal bills pages.
Create app/(main)/bills/page.tsx — the customer-facing bills list with Tailwind cards:
| 1 | import Link from 'next/link'; |
| 2 | import { db } from '@/lib/db'; |
| 3 | import { bills } from '@/lib/db/schema'; |
| 4 | import { desc } from 'drizzle-orm'; |
| 5 | import { Badge } from '@/components/ui/badge'; |
| 6 | import { Button } from '@/components/ui/button'; |
| 7 | import { |
| 8 | Card, CardContent, CardFooter, CardHeader, |
| 9 | } from '@/components/ui/card'; |
| 10 | |
| 11 | const statusColors: Record<string, string> = { |
| 12 | paid: 'bg-emerald-100 text-emerald-800', |
| 13 | overdue: 'bg-red-100 text-red-800', |
| 14 | sent: 'bg-blue-100 text-blue-800', |
| 15 | draft: 'bg-slate-100 text-slate-800', |
| 16 | }; |
| 17 | |
| 18 | export default async function BillsPortalPage() { |
| 19 | const allBills = await db.select().from(bills) |
| 20 | .orderBy(desc(bills.createdAt)); |
| 21 | |
| 22 | return ( |
| 23 | <div className="container mx-auto py-8"> |
| 24 | <h2 className="text-2xl font-bold mb-6">My Bills</h2> |
| 25 | <div className="grid grid-cols-1 md:grid-cols-2 |
| 26 | lg:grid-cols-3 gap-4"> |
| 27 | {allBills.map((bill) => ( |
| 28 | <Card key={bill.id}> |
| 29 | <CardHeader className="pb-2"> |
| 30 | <div className="flex justify-between items-start"> |
| 31 | <span className="font-semibold"> |
| 32 | {bill.billNumber}</span> |
| 33 | <Badge className={statusColors[bill.status!]}> |
| 34 | {bill.status}</Badge> |
| 35 | </div> |
| 36 | <p className="text-sm text-muted-foreground"> |
| 37 | {bill.customerName}</p> |
| 38 | </CardHeader> |
| 39 | <CardContent> |
| 40 | <p className="text-2xl font-bold"> |
| 41 | ${bill.totalAmount?.toFixed(2)}</p> |
| 42 | <p className="text-sm text-muted-foreground"> |
| 43 | Due: {bill.dueDate}</p> |
| 44 | </CardContent> |
| 45 | <CardFooter className="gap-2"> |
| 46 | <Link href={`/bills/${bill.id}`}> |
| 47 | <Button variant="outline" size="sm"> |
| 48 | View</Button> |
| 49 | </Link> |
| 50 | {['sent', 'overdue'].includes(bill.status!) && ( |
| 51 | <Link href={`/bills/${bill.id}`}> |
| 52 | <Button size="sm">Pay Now</Button> |
| 53 | </Link> |
| 54 | )} |
| 55 | </CardFooter> |
| 56 | </Card> |
| 57 | ))} |
| 58 | </div> |
| 59 | </div> |
| 60 | ); |
| 61 | } |
Notice how the grid layout maps exactly to Athena's mockup — 3 columns on large screens, 2 on medium, 1 on mobile. The cards use shadcn Card components instead of Bootstrap cards, but the structure is identical.
Task 5 and 6 — BillPayment component + payment API.
Create components/payments/BillPayment.tsx — this is a copy of InvoicePayment.tsx with bill references:
| 1 | 'use client'; |
| 2 | |
| 3 | import { useState, useEffect } from 'react'; |
| 4 | import { Button } from '@/components/ui/button'; |
| 5 | import { |
| 6 | Dialog, DialogContent, DialogHeader, |
| 7 | DialogTitle, DialogTrigger, |
| 8 | } from '@/components/ui/dialog'; |
| 9 | import { StripeProvider } from './StripeProvider'; |
| 10 | import { StripeCheckoutForm } from './StripeCheckoutForm'; |
| 11 | import { PayPalProvider } from './PayPalProvider'; |
| 12 | import { PayPalCheckout } from './PayPalCheckout'; |
| 13 | |
| 14 | interface BillPaymentProps { |
| 15 | billId: number; |
| 16 | billNumber: string; |
| 17 | amount: number; |
| 18 | } |
| 19 | |
| 20 | export function BillPayment({ |
| 21 | billId, billNumber, amount |
| 22 | }: BillPaymentProps) { |
| 23 | const [method, setMethod] = useState<'stripe'|'paypal'>('stripe'); |
| 24 | const [clientSecret, setClientSecret] = useState(''); |
| 25 | const [open, setOpen] = useState(false); |
| 26 | |
| 27 | useEffect(() => { |
| 28 | if (open && method === 'stripe') { |
| 29 | fetch('/api/payments/stripe/create-bill-intent', { |
| 30 | method: 'POST', |
| 31 | headers: { 'Content-Type': 'application/json' }, |
| 32 | body: JSON.stringify({ billId }), |
| 33 | }) |
| 34 | .then(r => r.json()) |
| 35 | .then(data => setClientSecret(data.clientSecret)); |
| 36 | } |
| 37 | }, [open, method, billId]); |
| 38 | |
| 39 | return ( |
| 40 | <Dialog open={open} onOpenChange={setOpen}> |
| 41 | <DialogTrigger asChild> |
| 42 | <Button>Pay Now</Button> |
| 43 | </DialogTrigger> |
| 44 | <DialogContent> |
| 45 | <DialogHeader> |
| 46 | <DialogTitle> |
| 47 | Pay Bill {billNumber} — ${amount.toFixed(2)} |
| 48 | </DialogTitle> |
| 49 | </DialogHeader> |
| 50 | <div className="flex gap-2 mb-4"> |
| 51 | <Button variant={method === 'stripe' ? 'default' : 'outline'} |
| 52 | onClick={() => setMethod('stripe')}>Card</Button> |
| 53 | <Button variant={method === 'paypal' ? 'default' : 'outline'} |
| 54 | onClick={() => setMethod('paypal')}>PayPal</Button> |
| 55 | </div> |
| 56 | {method === 'stripe' && clientSecret && ( |
| 57 | <StripeProvider clientSecret={clientSecret}> |
| 58 | <StripeCheckoutForm |
| 59 | amount={amount} |
| 60 | onSuccess={() => setOpen(false)} /> |
| 61 | </StripeProvider> |
| 62 | )} |
| 63 | {method === 'paypal' && ( |
| 64 | <PayPalProvider> |
| 65 | <PayPalCheckout |
| 66 | billId={billId} |
| 67 | onSuccess={() => setOpen(false)} /> |
| 68 | </PayPalProvider> |
| 69 | )} |
| 70 | </DialogContent> |
| 71 | </Dialog> |
| 72 | ); |
| 73 | } |
Exactly — same providers, same checkout forms, different API endpoint. The payment routes follow the same pattern too.
Task 7 — Navigation.
Add Bills to AdminSidebar.tsx:
| 1 | // Add to the navigation items array in AdminSidebar.tsx |
| 2 | { |
| 3 | label: 'Bills', |
| 4 | href: '/admin/bills', |
| 5 | icon: Receipt, // from lucide-react |
| 6 | }, |
And add "My Bills" to Navbar.tsx:
| 1 | // Add to the nav links in Navbar.tsx |
| 2 | <Link href="/bills" |
| 3 | className="text-sm font-medium hover:text-primary"> |
| 4 | My Bills |
| 5 | </Link> |
Task 8 — Smoke test.
Add bills to lib/db/seed.ts — follow the invoices seeding pattern:
| 1 | // Add to seed.ts after invoice seeding |
| 2 | const sampleBills = Array.from({ length: 10 }, (_, i) => ({ |
| 3 | billNumber: `BILL-${String(i + 1).padStart(3, '0')}`, |
| 4 | customerName: faker.company.name(), |
| 5 | customerEmail: faker.internet.email(), |
| 6 | subtotal: parseFloat(faker.commerce.price({ min: 100, max: 5000 })), |
| 7 | taxRate: 10, |
| 8 | taxAmount: 0, // calculated below |
| 9 | totalAmount: 0, |
| 10 | issueDate: faker.date.recent({ days: 30 }).toISOString(), |
| 11 | dueDate: faker.date.soon({ days: 30 }).toISOString(), |
| 12 | status: faker.helpers.arrayElement( |
| 13 | ['draft', 'sent', 'paid', 'overdue']), |
| 14 | })); |
| 15 | |
| 16 | for (const bill of sampleBills) { |
| 17 | bill.taxAmount = bill.subtotal * (bill.taxRate / 100); |
| 18 | bill.totalAmount = bill.subtotal + bill.taxAmount; |
| 19 | await db.insert(bills).values(bill); |
| 20 | } |
Run the app — npm run dev — and check:
- 1. /admin/bills — shadcn table with 10 seeded bills
- 2. /admin/bills/1 — detail page with status banner
- 3. /bills — portal card grid
- 4. Click "Pay Now" on an overdue bill — payment dialog opens
All passing. The billing portal is working end to end — admin CRUD, portal views, and payment flow. We still have the Invoice and Payslip code sitting around as reference. We'll do a proper cleanup next.
All tasks complete. Here's what you built today:
Admin Dashboard: Full CRUD for bills using shadcn/ui — Table with search and status badges, detail page with payment info, create/edit forms.
Customer Portal: Bills homepage with responsive card grid (Tailwind CSS), bill detail with payment button.
Payment Integration: BillPayment dialog with Stripe + PayPal toggle — reused existing providers and checkout forms.
The stack: Next.js 15 App Router + TypeScript + Tailwind CSS v4 + shadcn/ui + Drizzle ORM. ~250 lines of new code across schema, routes, pages, and components.
Easy — just add a "Reminders" card to the bill detail page. Something like {bill.reminderCount} reminders sent, last on {bill.lastReminderDate}. Drizzle already has the columns in the schema. Have a good evening!
Cleaning Session
What happens here. The pair-programming session kept existing Invoice/Payslip code around as working reference. Now that the billing portal is verified, the user and Apollo do a thorough project audit — removing all non-billing feature code (Charts, Reports, Pivot Tables, Data Warehouse, Tabulator, Canvas, Settings), updating shared files (sidebar, navbar, schema, seed data), and verifying the app compiles and runs as a slim billing-only portal.
Right. Now that Bills works end to end, remove the old feature code:
- Remove invoices + payslips table definitions from schema.ts (keep bills)
- Remove app/api/invoices/ and app/api/payslips/ routes
- Remove app/(admin)/admin/invoices/ and payslips/ pages
- Remove InvoicePayment.tsx (we have BillPayment.tsx now)
Keep: StripeProvider, PayPalProvider, StripeCheckoutForm, PayPalCheckout, BillPayment, components/ui/ (shared), AdminSidebar, Navbar, layouts.
Let's do a full audit. Here's every remaining file that needs attention:
DELETE — these pages and routes are not part of the billing portal:
Pages to delete (8 directories):
- app/(main)/charts/
- app/(main)/data-warehouse/
- app/(main)/pivot-tables/
- app/(main)/report-parameters/
- app/(main)/reports/
- app/(main)/tabulator/
- app/(main)/your-canvas/
- app/(admin)/admin/settings/
API routes to delete (2 directories):
- app/api/settings/
- app/api/stats/
Components to delete (4 files):
- components/Placeholder.tsx — generic placeholder, not used by billing
- components/ComponentDemo.tsx — demo component, not needed
- components/CodeBlock.tsx — code display, not part of billing portal
- components/RbWebComponentsLoader.tsx — not needed for billing
Other files to delete:
- types/rb-components.d.ts — type declarations for RbWebComponentsLoader
- hooks/use-settings.ts — settings hook, goes with Settings page
UPDATE — these shared files reference the old features:
- components/admin/AdminSidebar.tsx — remove nav links to Dashboard, Invoices, Payslips, Settings. Keep only: Bills
- components/layout/Navbar.tsx — remove nav links to Charts, Reports, Pivot Tables, Data Warehouse, Tabulator, Canvas. Keep: My Bills
- app/(admin)/admin/page.tsx — update the admin dashboard to show billing stats instead of generic dashboard content
- app/(main)/page.tsx — update the home page for billing portal (remove feature grid, add billing welcome content)
- lib/db/seed.ts — remove any remaining invoice/payslip seeding code. Keep the bills seeding
- components/payments/index.ts — update barrel export: remove InvoicePayment, keep BillPayment
KEEP — billing portal code + infrastructure:
- app/(admin)/admin/bills/ — admin bill pages
- app/(main)/bills/ — portal bill pages
- app/api/bills/ — bill API routes
- app/api/payments/ — Stripe + PayPal endpoints
- app/api/health/ — health check
- app/api/auth/ — authentication
- components/payments/ (BillPayment, StripeProvider, StripeCheckoutForm, PayPalProvider, PayPalCheckout)
- components/ui/ — all shadcn components (shared)
- components/admin/ (AdminHeader, AdminSidebar — after update)
- components/layout/ (Footer, Navbar — after update)
- components/ThemeProvider.tsx, ThemeSelector.tsx — theme system
- hooks/use-toast.ts, useThemeSettings.ts
- lib/ (db, utils, themes, rb-config)
- middleware.ts — auth/routing
- All config (next.config.ts, tsconfig.json, tailwind, postcss, etc.)
- All Docker files, package.json
That's 8 page directories + 2 API directories + 4 components + 2 other files to delete, and 6 files to update. Ready?
Delete in this order — pages, API routes, components, other files:
| 1 | # Step 1: Delete page directories (8) |
| 2 | app/(main)/charts/ |
| 3 | app/(main)/data-warehouse/ |
| 4 | app/(main)/pivot-tables/ |
| 5 | app/(main)/report-parameters/ |
| 6 | app/(main)/reports/ |
| 7 | app/(main)/tabulator/ |
| 8 | app/(main)/your-canvas/ |
| 9 | app/(admin)/admin/settings/ |
| 10 | |
| 11 | # Step 2: Delete API route directories (2) |
| 12 | app/api/settings/ |
| 13 | app/api/stats/ |
| 14 | |
| 15 | # Step 3: Delete components (4) |
| 16 | components/Placeholder.tsx |
| 17 | components/ComponentDemo.tsx |
| 18 | components/CodeBlock.tsx |
| 19 | components/RbWebComponentsLoader.tsx |
| 20 | |
| 21 | # Step 4: Delete other files (2) |
| 22 | types/rb-components.d.ts |
| 23 | hooks/use-settings.ts |
Start with AdminSidebar.tsx — strip the nav items down to billing:
| 1 | // AdminSidebar.tsx — navigation items |
| 2 | const navItems = [ |
| 3 | { |
| 4 | label: 'Bills', |
| 5 | href: '/admin/bills', |
| 6 | icon: Receipt, |
| 7 | }, |
| 8 | ]; |
Update Navbar.tsx — remove all the old feature links, keep only My Bills:
| 1 | // Navbar.tsx — portal navigation links |
| 2 | <Link href="/bills" |
| 3 | className="text-sm font-medium hover:text-primary"> |
| 4 | My Bills |
| 5 | </Link> |
Update app/(main)/page.tsx — replace the feature grid with a billing portal landing:
| 1 | export default function HomePage() { |
| 2 | return ( |
| 3 | <div className="container mx-auto py-12 text-center"> |
| 4 | <h1 className="text-4xl font-bold mb-4"> |
| 5 | Billing Portal |
| 6 | </h1> |
| 7 | <p className="text-lg text-muted-foreground mb-8"> |
| 8 | Manage bills, track payments, send reminders. |
| 9 | </p> |
| 10 | <div className="flex gap-4 justify-center"> |
| 11 | <Link href="/bills"> |
| 12 | <Button size="lg">View My Bills</Button> |
| 13 | </Link> |
| 14 | <Link href="/admin/bills"> |
| 15 | <Button variant="outline" size="lg"> |
| 16 | Admin Panel |
| 17 | </Button> |
| 18 | </Link> |
| 19 | </div> |
| 20 | </div> |
| 21 | ); |
| 22 | } |
Update app/(admin)/admin/page.tsx — simplify the admin dashboard:
| 1 | export default async function AdminPage() { |
| 2 | const allBills = await db.select().from(bills); |
| 3 | const overdue = allBills.filter(b => b.status === 'overdue'); |
| 4 | const paid = allBills.filter(b => b.status === 'paid'); |
| 5 | |
| 6 | return ( |
| 7 | <div className="space-y-4"> |
| 8 | <h2 className="text-2xl font-bold">Dashboard</h2> |
| 9 | <div className="grid grid-cols-3 gap-4"> |
| 10 | <Card><CardContent className="pt-6"> |
| 11 | <p className="text-sm text-muted-foreground"> |
| 12 | Total Bills</p> |
| 13 | <p className="text-2xl font-bold"> |
| 14 | {allBills.length}</p> |
| 15 | </CardContent></Card> |
| 16 | <Card><CardContent className="pt-6"> |
| 17 | <p className="text-sm text-muted-foreground"> |
| 18 | Overdue</p> |
| 19 | <p className="text-2xl font-bold text-red-500"> |
| 20 | {overdue.length}</p> |
| 21 | </CardContent></Card> |
| 22 | <Card><CardContent className="pt-6"> |
| 23 | <p className="text-sm text-muted-foreground"> |
| 24 | Paid</p> |
| 25 | <p className="text-2xl font-bold text-emerald-500"> |
| 26 | {paid.length}</p> |
| 27 | </CardContent></Card> |
| 28 | </div> |
| 29 | </div> |
| 30 | ); |
| 31 | } |
Finally, clean up lib/db/seed.ts — remove invoice/payslip seeding, keep bills. And update components/payments/index.ts — export BillPayment instead of InvoicePayment.
npm run dev to verify...Here's your clean billing portal:
| 1 | next-playground/ |
| 2 | ├── app/ |
| 3 | │ ├── (admin)/ |
| 4 | │ │ ├── admin/ |
| 5 | │ │ │ ├── bills/ (page, new, [id], [id]/edit) |
| 6 | │ │ │ └── page.tsx # Dashboard with stats |
| 7 | │ │ └── layout.tsx |
| 8 | │ ├── (main)/ |
| 9 | │ │ ├── bills/ (page, [id]) |
| 10 | │ │ ├── page.tsx # Billing portal landing |
| 11 | │ │ └── layout.tsx |
| 12 | │ ├── api/ |
| 13 | │ │ ├── auth/ # Authentication |
| 14 | │ │ ├── bills/ (route, [id]) # Bill CRUD API |
| 15 | │ │ ├── health/ # Health check |
| 16 | │ │ └── payments/ (stripe, paypal) |
| 17 | │ ├── globals.css |
| 18 | │ └── layout.tsx |
| 19 | ├── components/ |
| 20 | │ ├── admin/ (AdminHeader, AdminSidebar) |
| 21 | │ ├── layout/ (Footer, Navbar) |
| 22 | │ ├── payments/ (BillPayment, Stripe*, PayPal*) |
| 23 | │ ├── ui/ (badge, button, card, dialog, ...) |
| 24 | │ ├── ThemeProvider.tsx |
| 25 | │ └── ThemeSelector.tsx |
| 26 | ├── hooks/ (use-toast, useThemeSettings) |
| 27 | ├── lib/ |
| 28 | │ ├── db/ (index, schema, seed) |
| 29 | │ ├── utils.ts, themes.ts, rb-config.ts |
| 30 | ├── middleware.ts |
| 31 | ├── package.json, next.config.ts, tsconfig.json |
| 32 | ├── Dockerfile, docker-compose.yml |
| 33 | └── drizzle.config.ts |
From ~50 source files down to ~30. Every file serves the billing portal — schema, API routes, admin pages, portal pages, payment components, and infrastructure. Clean, focused, production-ready.