CRM Dashboard
A comprehensive customer relationship management interface with contacts, deals pipeline, activity tracking, and powerful analytics.
Manage Relationships at Scale
Keep track of every customer interaction, manage your sales pipeline, and make data-driven decisions with comprehensive analytics. Built for teams that take customer relationships seriously.
Contact Management
Organize contacts with tags, notes, and interaction history
Deals Pipeline
Visual kanban board for tracking deals through stages
Analytics & Reports
Real-time insights into sales performance and trends
Activity Tracking
Never miss a follow-up with smart reminders
Live Demo
pipeline
Leads
$25kPilot Program
Innovation Inc
Qualified
$23kStartup Package
TechStart
Team License
Design Studio
Proposal
$170kEnterprise Deal
Acme Corp
Enterprise Plus
Global Tech
Negotiation
$75kAnnual Contract
BigCo
Implementation Guide
Set Up Data Models
Define TypeScript interfaces for contacts, deals, and activities.
123456789101112131415161718192021222324252627282930313233343536// types/crm.ts export interface Contact { id: string; name: string; email: string; phone?: string; company: string; position?: string; status: "lead" | "prospect" | "customer" | "churned"; tags: string[]; lastContact?: Date; createdAt: Date; } export interface Deal { id: string; title: string; value: number; stage: "lead" | "qualified" | "proposal" | "negotiation" | "won" | "lost"; contactId: string; probability: number; expectedCloseDate: Date; assignedTo: string; activities: Activity[]; } export interface Activity { id: string; type: "call" | "email" | "meeting" | "note" | "task"; title: string; description?: string; date: Date; contactId?: string; dealId?: string; completed: boolean; }
Create the Deals Pipeline
Build a drag-and-drop kanban board for visualizing deal stages.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657const stages = [ { id: "lead", title: "Leads", color: "bg-slate-500" }, { id: "qualified", title: "Qualified", color: "bg-purple-500" }, { id: "proposal", title: "Proposal", color: "bg-cyan-500" }, { id: "negotiation", title: "Negotiation", color: "bg-amber-500" }, { id: "won", title: "Won", color: "bg-emerald-500" }, ]; function DealsPipeline({ deals }: { deals: Deal[] }) { const [draggedDeal, setDraggedDeal] = useState<Deal | null>(null); const handleDragStart = (deal: Deal) => setDraggedDeal(deal); const handleDrop = (stageId: string) => { if (draggedDeal) { // Update deal stage via API updateDealStage(draggedDeal.id, stageId); setDraggedDeal(null); } }; return ( <div className="flex gap-4 overflow-x-auto pb-4"> {stages.map(stage => { const stageDeals = deals.filter(d => d.stage === stage.id); const totalValue = stageDeals.reduce((sum, d) => sum + d.value, 0); return ( <div key={stage.id} onDragOver={e => e.preventDefault()} onDrop={() => handleDrop(stage.id)} className="flex-shrink-0 w-72" > <div className="flex items-center gap-2 mb-4"> <div className={`w-3 h-3 rounded-full ${stage.color}`} /> <h3 className="font-semibold">{stage.title}</h3> <span className="text-sm text-slate-500"> ${(totalValue / 1000).toFixed(0)}k </span> </div> <div className="space-y-3"> {stageDeals.map(deal => ( <DealCard key={deal.id} deal={deal} onDragStart={() => handleDragStart(deal)} /> ))} </div> </div> ); })} </div> ); }
Build Contact Management
Create a searchable, filterable contact list with detail views.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778function ContactsTable({ contacts }: { contacts: Contact[] }) { const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState<string>("all"); const [selectedContact, setSelectedContact] = useState<Contact | null>(null); const filteredContacts = contacts.filter(contact => { const matchesSearch = contact.name.toLowerCase().includes(search.toLowerCase()) || contact.email.toLowerCase().includes(search.toLowerCase()); const matchesStatus = statusFilter === "all" || contact.status === statusFilter; return matchesSearch && matchesStatus; }); return ( <div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800"> {/* Search & Filters */} <div className="p-4 border-b border-slate-200 dark:border-slate-800 flex gap-4"> <div className="flex-1 relative"> <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" /> <input type="text" placeholder="Search contacts..." value={search} onChange={e => setSearch(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-xl" /> </div> <select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="px-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-xl" > <option value="all">All Status</option> <option value="lead">Leads</option> <option value="prospect">Prospects</option> <option value="customer">Customers</option> </select> </div> {/* Table */} <table className="w-full"> <thead className="bg-slate-50 dark:bg-slate-800/50"> <tr> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Name</th> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Company</th> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Status</th> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Last Contact</th> </tr> </thead> <tbody> {filteredContacts.map(contact => ( <tr key={contact.id} onClick={() => setSelectedContact(contact)} className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30 cursor-pointer" > <td className="px-6 py-4"> <div className="flex items-center gap-3"> <Avatar name={contact.name} /> <div> <p className="font-medium">{contact.name}</p> <p className="text-sm text-slate-500">{contact.email}</p> </div> </div> </td> <td className="px-6 py-4">{contact.company}</td> <td className="px-6 py-4"> <StatusBadge status={contact.status} /> </td> <td className="px-6 py-4 text-sm text-slate-500"> {formatDate(contact.lastContact)} </td> </tr> ))} </tbody> </table> </div> ); }
Add Activity Timeline
Track all interactions with a comprehensive activity feed.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647function ActivityTimeline({ activities }: { activities: Activity[] }) { const getActivityIcon = (type: Activity["type"]) => { const icons = { call: { icon: PhoneIcon, color: "bg-blue-100 text-blue-600" }, email: { icon: MailIcon, color: "bg-purple-100 text-purple-600" }, meeting: { icon: CalendarIcon, color: "bg-emerald-100 text-emerald-600" }, note: { icon: NoteIcon, color: "bg-amber-100 text-amber-600" }, task: { icon: TaskIcon, color: "bg-cyan-100 text-cyan-600" }, }; return icons[type]; }; return ( <div className="space-y-4"> {activities.map((activity, i) => { const { icon: Icon, color } = getActivityIcon(activity.type); return ( <div key={activity.id} className="flex gap-4"> <div className="relative"> <div className={`w-10 h-10 rounded-full flex items-center justify-center ${color}`}> <Icon className="w-5 h-5" /> </div> {i < activities.length - 1 && ( <div className="absolute top-10 left-1/2 -translate-x-1/2 w-0.5 h-full bg-slate-200 dark:bg-slate-700" /> )} </div> <div className="flex-1 pb-8"> <div className="flex items-center justify-between mb-1"> <h4 className="font-medium text-slate-900 dark:text-white"> {activity.title} </h4> <span className="text-sm text-slate-500"> {formatRelativeTime(activity.date)} </span> </div> {activity.description && ( <p className="text-sm text-slate-600 dark:text-slate-400"> {activity.description} </p> )} </div> </div> ); })} </div> ); }
Full Source Code
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293"use client"; import { useState } from "react"; // Types interface Contact { id: string; name: string; email: string; company: string; status: "lead" | "prospect" | "customer"; lastContact: string; } interface Deal { id: string; title: string; company: string; value: number; stage: string; contact: string; } interface Activity { id: string; type: "call" | "email" | "meeting" | "note"; title: string; description: string; time: string; } // Main CRM Dashboard export default function CRMDashboard() { const [activeView, setActiveView] = useState<"pipeline" | "contacts" | "activities">("pipeline"); return ( <div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex"> <Sidebar activeView={activeView} onViewChange={setActiveView} /> <main className="flex-1"> <Header title={activeView} /> <div className="p-6"> {activeView === "pipeline" && <DealsPipeline />} {activeView === "contacts" && <ContactsTable />} {activeView === "activities" && <ActivityFeed />} </div> </main> </div> ); } // Sidebar Component function Sidebar({ activeView, onViewChange }: { activeView: string; onViewChange: (view: "pipeline" | "contacts" | "activities") => void; }) { const navItems = [ { id: "pipeline", label: "Pipeline", icon: "📊" }, { id: "contacts", label: "Contacts", icon: "👥" }, { id: "activities", label: "Activities", icon: "📋" }, ]; return ( <aside className="w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800"> <div className="p-4 border-b border-slate-200 dark:border-slate-800"> <div className="flex items-center gap-3"> <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white font-bold text-sm"> C </div> <span className="font-semibold text-slate-900 dark:text-white">CRM Pro</span> </div> </div> <nav className="p-3 space-y-1"> {navItems.map((item) => ( <button key={item.id} onClick={() => onViewChange(item.id as "pipeline" | "contacts" | "activities")} className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-colors ${ activeView === item.id ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300" : "text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800" }`} > <span>{item.icon}</span> {item.label} </button> ))} </nav> </aside> ); } // Deals Pipeline with Kanban function DealsPipeline() { const stages = [ { id: "lead", title: "Leads", color: "bg-slate-500" }, { id: "qualified", title: "Qualified", color: "bg-purple-500" }, { id: "proposal", title: "Proposal", color: "bg-cyan-500" }, { id: "negotiation", title: "Negotiation", color: "bg-amber-500" }, ]; const [deals, setDeals] = useState<Deal[]>([ { id: "1", title: "Enterprise Deal", company: "Acme Corp", value: 50000, stage: "proposal", contact: "John Smith" }, { id: "2", title: "Startup Package", company: "TechStart", value: 15000, stage: "qualified", contact: "Sarah Lee" }, // ... more deals ]); const [draggedDeal, setDraggedDeal] = useState<Deal | null>(null); const handleDragStart = (deal: Deal) => setDraggedDeal(deal); const handleDrop = (stageId: string) => { if (draggedDeal) { setDeals(prev => prev.map(d => d.id === draggedDeal.id ? { ...d, stage: stageId } : d) ); setDraggedDeal(null); } }; return ( <div className="flex gap-4 overflow-x-auto pb-4"> {stages.map((stage) => { const stageDeals = deals.filter(d => d.stage === stage.id); const totalValue = stageDeals.reduce((sum, d) => sum + d.value, 0); return ( <div key={stage.id} onDragOver={e => e.preventDefault()} onDrop={() => handleDrop(stage.id)} className="flex-shrink-0 w-72" > <div className="flex items-center gap-2 mb-4"> <div className={`w-3 h-3 rounded-full ${stage.color}`} /> <h3 className="font-semibold">{stage.title}</h3> <span className="text-sm text-slate-500">${(totalValue / 1000).toFixed(0)}k</span> </div> <div className="space-y-3"> {stageDeals.map((deal) => ( <div key={deal.id} draggable onDragStart={() => handleDragStart(deal)} className="p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 cursor-grab active:cursor-grabbing" > <h4 className="font-medium text-slate-900 dark:text-white mb-1">{deal.title}</h4> <p className="text-sm text-slate-500 mb-3">{deal.company}</p> <div className="flex items-center justify-between"> <span className="text-lg font-semibold text-emerald-600">${(deal.value / 1000).toFixed(0)}k</span> <Avatar name={deal.contact} /> </div> </div> ))} </div> </div> ); })} </div> ); } // Contacts Table function ContactsTable() { const [contacts] = useState<Contact[]>([ { id: "1", name: "John Smith", email: "john@acme.com", company: "Acme Corp", status: "customer", lastContact: "2 days ago" }, { id: "2", name: "Sarah Lee", email: "sarah@techstart.io", company: "TechStart", status: "prospect", lastContact: "1 week ago" }, // ... more contacts ]); const [search, setSearch] = useState(""); const filteredContacts = contacts.filter(c => c.name.toLowerCase().includes(search.toLowerCase()) || c.email.toLowerCase().includes(search.toLowerCase()) ); return ( <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800"> <div className="p-4 border-b border-slate-200 dark:border-slate-800"> <input type="text" placeholder="Search contacts..." value={search} onChange={e => setSearch(e.target.value)} className="w-full px-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-xl" /> </div> <table className="w-full"> <thead className="bg-slate-50 dark:bg-slate-800/50"> <tr> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Contact</th> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Company</th> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Status</th> <th className="px-6 py-3 text-left text-xs font-semibold uppercase">Last Contact</th> </tr> </thead> <tbody> {filteredContacts.map((contact) => ( <tr key={contact.id} className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30"> <td className="px-6 py-4"> <div className="flex items-center gap-3"> <Avatar name={contact.name} /> <div> <p className="font-medium">{contact.name}</p> <p className="text-sm text-slate-500">{contact.email}</p> </div> </div> </td> <td className="px-6 py-4">{contact.company}</td> <td className="px-6 py-4"> <StatusBadge status={contact.status} /> </td> <td className="px-6 py-4 text-sm text-slate-500">{contact.lastContact}</td> </tr> ))} </tbody> </table> </div> ); } // Activity Feed function ActivityFeed() { const activities: Activity[] = [ { id: "1", type: "call", title: "Call with John Smith", description: "Discussed enterprise requirements", time: "2 hours ago" }, { id: "2", type: "email", title: "Proposal sent to BigCo", description: "Sent pricing proposal", time: "4 hours ago" }, // ... more activities ]; const getActivityColor = (type: Activity["type"]) => ({ call: "bg-blue-100 text-blue-600", email: "bg-purple-100 text-purple-600", meeting: "bg-emerald-100 text-emerald-600", note: "bg-amber-100 text-amber-600", }[type]); return ( <div className="space-y-4"> {activities.map((activity) => ( <div key={activity.id} className="flex gap-4 p-4 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800"> <div className={`w-10 h-10 rounded-full flex items-center justify-center ${getActivityColor(activity.type)}`}> {activity.type === "call" && "📞"} {activity.type === "email" && "📧"} {activity.type === "meeting" && "📅"} {activity.type === "note" && "📝"} </div> <div className="flex-1"> <div className="flex items-center justify-between mb-1"> <h4 className="font-medium">{activity.title}</h4> <span className="text-sm text-slate-500">{activity.time}</span> </div> <p className="text-sm text-slate-600 dark:text-slate-400">{activity.description}</p> </div> </div> ))} </div> ); } // Helper Components function Avatar({ name }: { name: string }) { const initials = name.split(" ").map(n => n[0]).join(""); return ( <div className="w-10 h-10 rounded-full bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center text-white font-medium text-sm"> {initials} </div> ); } function StatusBadge({ status }: { status: string }) { const colors = { customer: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300", prospect: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300", lead: "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400", }; return ( <span className={`px-2.5 py-1 text-xs font-medium rounded-full ${colors[status as keyof typeof colors] || colors.lead}`}> {status} </span> ); } function Header({ title }: { title: string }) { return ( <header className="h-16 bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between px-6"> <h1 className="text-lg font-semibold capitalize">{title}</h1> <button className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-xl"> + Add New </button> </header> ); }
Customization Guide
Custom Deal Stages
Configure deal stages to match your sales process.
// Define your own pipeline stages
const customStages = [
{ id: "inquiry", title: "Inquiry", color: "bg-slate-500" },
{ id: "demo", title: "Demo Scheduled", color: "bg-blue-500" },
{ id: "trial", title: "Trial", color: "bg-purple-500" },
{ id: "contract", title: "Contract Sent", color: "bg-amber-500" },
{ id: "closed-won", title: "Closed Won", color: "bg-emerald-500" },
{ id: "closed-lost", title: "Closed Lost", color: "bg-red-500" },
];Contact Statuses
Customize contact lifecycle stages for your business.
const contactStatuses = {
cold: { label: "Cold Lead", color: "bg-slate-100 text-slate-700" },
warm: { label: "Warm Lead", color: "bg-amber-100 text-amber-700" },
hot: { label: "Hot Lead", color: "bg-red-100 text-red-700" },
customer: { label: "Customer", color: "bg-emerald-100 text-emerald-700" },
vip: { label: "VIP Customer", color: "bg-purple-100 text-purple-700" },
churned: { label: "Churned", color: "bg-slate-100 text-slate-500" },
};Activity Types
Add custom activity types for tracking interactions.
const activityTypes = [
{ id: "call", label: "Phone Call", icon: PhoneIcon },
{ id: "email", label: "Email", icon: MailIcon },
{ id: "meeting", label: "Meeting", icon: CalendarIcon },
{ id: "demo", label: "Demo", icon: ScreenIcon },
{ id: "proposal", label: "Proposal Sent", icon: DocumentIcon },
{ id: "contract", label: "Contract", icon: ContractIcon },
{ id: "note", label: "Note", icon: NoteIcon },
];Integration Setup
Connect with your existing tools and services.
// Example: Sync with external CRM
async function syncContacts() {
const externalContacts = await fetchFromSalesforce();
await Promise.all(
externalContacts.map(contact =>
upsertContact({
externalId: contact.id,
name: contact.Name,
email: contact.Email,
company: contact.Account?.Name,
// Map other fields...
})
)
);
}
// Webhook for real-time updates
export async function POST(req: Request) {
const event = await req.json();
switch (event.type) {
case "contact.created":
await handleNewContact(event.data);
break;
case "deal.stage_changed":
await handleDealUpdate(event.data);
break;
}
}Ready to Manage Your Relationships?
Copy this template and start building your CRM. Customize it to fit your sales process.