Skip to content

Repository files navigation

LeadDesk Mini

A full-stack B2B lead-capture and pipeline management tool built as a timed take-home.

Public visitors submit bulk-order enquiries via a polished form. Admin users log in to view, search, filter, and update lead statuses.

Built for the Digital Heroes Full Stack Development internship qualification task β€” digitalheroesco.com


Live Demo

πŸ”’ Visiting /admin without an active session automatically redirects to /admin/login β€” the dashboard is never reachable unauthenticated.


Tech Stack

Layer Choice
Framework Next.js 16 (App Router) + TypeScript
Styling Tailwind CSS v4
Forms React Hook Form + Zod
Database PostgreSQL on Neon (serverless)
ORM Prisma 7 (with @prisma/adapter-neon)
Auth Auth.js v5 (next-auth@beta) β€” credentials + JWT-in-cookie
Tests Vitest 4
Deploy Vercel

Local Setup

1. Prerequisites

  • Node.js >= 20
  • A Neon project with two connection strings:
    • DATABASE_URL β€” pooled (used by the app at runtime via the Neon adapter)
    • DIRECT_URL β€” direct / non-pooled (used by Prisma migrations to bypass pgBouncer)

2. Clone and Install

cd leaddesk-mini
npm install

3. Environment Variables

Copy the example file and fill in your values:

cp .env.example .env.local

Edit .env.local:

# Neon pooled connection (used by the app at runtime via the Neon adapter)
DATABASE_URL="postgresql://user:pass@ep-xxx-pooler.neon.tech/neondb?sslmode=require&pgbouncer=true&connect_timeout=15"

# Neon direct connection (used by prisma migrate, bypasses pgBouncer)
DIRECT_URL="postgresql://user:pass@ep-xxx.neon.tech/neondb?sslmode=require"

# Auth.js β€” generate with: openssl rand -base64 32
NEXTAUTH_SECRET="your-secret-here"
NEXTAUTH_URL="http://localhost:3000"

4. Run Migrations

npx prisma migrate dev

5. Seed the Admin User

npx tsx prisma/seed.ts

This creates:

Field Value
Email admin@leaddesk.com
Password Demo@LeadDesk1

⚠️ Change this password before treating any deployment as production.

6. Start the Dev Server

npm run dev

Running Tests

# Run all 41 tests once
npm test

# Watch mode
npm run test:watch

# Coverage report
npm run test:coverage

Test Suite Coverage:

  • 22 Zod schema unit tests: all validation rules, email lowercasing, whitespace trim
  • 9 POST /api/leads handler tests: happy path, validation failures, duplicate 24h logic, DB error, malformed JSON
  • 10 GET /api/leads + PATCH /api/leads/[id] tests: auth guards, search, status filter, 404, 503

API Reference

Method Path Auth Description
POST /api/leads Public Submit a lead enquiry
GET /api/leads Admin List leads with search + filter + pagination
GET /api/leads/stats Admin Dashboard counts (total, new, contacted, closed)
GET /api/leads/activity Admin Recent activity feed
PATCH /api/leads/[id] Admin Update lead status
GET/POST /api/auth/* β€” Auth.js handlers

POST /api/leads Request Body

{
  "name": "Jane Smith",
  "email": "jane@company.com",
  "budgetRange": "RANGE_1K_5K",
  "message": "Need 500 units for our warehouse project."
}

Field budgetRange is labelled "Order Value" in the UI. Accepted values:
UNDER_1K | RANGE_1K_5K | RANGE_5K_10K | OVER_10K

Duplicate handling: If the same email submits within 24 hours, the lead is stored with isDuplicate: true and the response includes a warning string. No submission is rejected.

PATCH /api/leads/[id] Request Body

{ "status": "CONTACTED" }

Accepted status values: NEW | CONTACTED | CLOSED


Project Structure

leaddesk-mini/
β”œβ”€β”€ prisma/
β”‚   β”œβ”€β”€ schema.prisma          # Lead + AdminUser models
β”‚   β”œβ”€β”€ seed.ts                # Admin user seeder
β”‚   └── migrations/            # SQL migration history
β”œβ”€β”€ prisma.config.ts           # Prisma 7 datasource config
β”œβ”€β”€ middleware.ts              # Edge-safe route protection
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ auth.config.ts         # Edge-safe Auth.js config (used by middleware)
β”‚   β”œβ”€β”€ auth.ts                # Full Auth.js config (Prisma + bcrypt, Node runtime only)
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”œβ”€β”€ page.tsx           # Public lead form (/)
β”‚   β”‚   β”œβ”€β”€ admin/
β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx       # Admin dashboard (/admin)
β”‚   β”‚   β”‚   └── login/
β”‚   β”‚   β”‚       └── page.tsx   # Login page (/admin/login)
β”‚   β”‚   └── api/
β”‚   β”‚       β”œβ”€β”€ leads/
β”‚   β”‚       β”‚   β”œβ”€β”€ route.ts            # GET + POST
β”‚   β”‚       β”‚   β”œβ”€β”€ stats/route.ts      # GET stats
β”‚   β”‚       β”‚   β”œβ”€β”€ activity/route.ts   # GET activity
β”‚   β”‚       β”‚   └── [id]/route.ts       # PATCH
β”‚   β”‚       └── auth/[...nextauth]/
β”‚   β”‚           └── route.ts            # Auth.js handlers
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ LeadForm.tsx       # Public form (client)
β”‚   β”‚   β”œβ”€β”€ Footer.tsx         # Digital Heroes credit footer
β”‚   β”‚   └── admin/
β”‚   β”‚       β”œβ”€β”€ AdminDashboard.tsx  # Dashboard (client)
β”‚   β”‚       └── SignOutButton.tsx   # Sign-out (client)
β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”œβ”€β”€ prisma.ts          # PrismaClient singleton (Neon adapter)
β”‚   β”‚   └── validations/
β”‚   β”‚       └── lead.ts        # Shared Zod schemas
β”‚   β”œβ”€β”€ types/
β”‚   β”‚   └── lead.ts            # TS types + label maps
β”‚   └── __tests__/
β”‚       β”œβ”€β”€ setup.ts           # Vitest global mocks
β”‚       β”œβ”€β”€ api/
β”‚       β”‚   β”œβ”€β”€ leads.post.test.ts
β”‚       β”‚   └── leads.get.test.ts
β”‚       └── lib/
β”‚           └── validations.test.ts
β”œβ”€β”€ vitest.config.ts
β”œβ”€β”€ CRITIQUE.md                # Staff-engineer self-critique
└── README.md                  # Project documentation

Note on auth.config.ts / auth.ts split

Vercel Edge Middleware runs on the Edge Runtime, which cannot execute Prisma Client or bcryptjs (both Node.js-only). auth.config.ts holds the route-authorization logic with zero database dependencies and is the only auth file middleware.ts imports. auth.ts extends it with the full Credentials provider (Prisma + bcrypt) for use in Route Handlers and Server Components. This split is what keeps the Edge Function bundle under Vercel's size limit.


Deploying to Vercel

  1. Push the repository to GitHub
  2. Import into Vercel (framework auto-detected as Next.js)
  3. Add environment variables in the Vercel dashboard
  4. Vercel runs next build automatically on every push
  5. Run migrations against production once: npx prisma migrate deploy

Design Decisions

  • Why JWT-in-cookie, not bearer tokens?
    This is a single-admin internal tool. The httpOnly, Secure, SameSite=Lax cookie attributes mean the session token is inaccessible to JavaScript (XSS-resistant) and sent automatically with every browser request (no client-side token management). A multi-service bearer-JWT setup would add a token-refresh flow and revocation mechanism for zero benefit in a single-admin dashboard.

  • Why allow duplicate submissions?
    Rejecting duplicate emails silently would confuse genuine re-submissions (e.g. a buyer who forgot they already submitted). Storing the duplicate with isDuplicate: true means the sales team sees the full picture, and the buyer still gets a friendly confirmation regardless.

  • Why Zod on both client and server?
    A single shared schema (src/lib/validations/lead.ts) powers React Hook Form on the client and safeParse on the server. This eliminates validation drift β€” if a server-side rule changes, the client immediately reflects it with zero code duplication.

  • Why split auth.config.ts from auth.ts?
    Initially, middleware.ts imported the full Auth.js config directly, which pulled Prisma Client and bcryptjs into the Edge Function bundle β€” this both failed to resolve (Module not found: .prisma/client/default, since Prisma's generated client isn't Edge-compatible) and pushed the bundle past Vercel's 1 MB Edge Function limit. Splitting the config so middleware only imports the lightweight, database-free authConfig fixed both issues and is the standard pattern recommended for Auth.js v5 + Prisma on Vercel.

See CRITIQUE.md for the full self-critique and production-readiness gap analysis.


Use of AI in this Build

I used AI throughout this build for: scaffolding the Prisma schema and initial Route Handlers, generating the Vitest test suite, and diagnosing two production deploy errors (a Prisma-in-Edge-Runtime module resolution failure and an Edge Function bundle-size limit breach).

What I changed afterward: I renamed and reframed the generic "lead capture" brief into a B2B wholesale-order niche (relabeling "Budget" to "Order Value," rewriting form copy), decided the duplicate-lead handling behavior (flag rather than reject) based on what made sense for a real sales team's workflow rather than the model's first suggestion, and made the explicit auth-architecture call (JWT-in-cookie over bearer tokens) after weighing it against this being a single-admin tool rather than a multi-service system.

About

Full-stack lead-capture and management tool for B2B wholesalers - built for the Digital Heroes internship qualification task.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages