Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.next/
out/
dist/
*.tsbuildinfo
.env*
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
# dead-simple-crm
A minimal, opinionated CRM that does contacts, companies, deals, and notes — nothing more. Clean UI, fast search, CSV import/export. Built for people who want a CRM without the bloat of Salesforce or HubSpot. Every feature is a discrete bounty: filters, email integration, pipeline views, reporting.

A minimal, opinionated CRM that does contacts, companies, deals, and notes:
nothing more. Clean UI, fast search, CSV import/export. Built for people who
want a CRM without the bloat of Salesforce or HubSpot.

## Getting Started

```bash
npm install
npm run dev
```

Open http://localhost:3000 to view the CRM shell.

## Scripts

```bash
npm run dev
npm run build
npm run lint
npm run typecheck
```

## Included Pages

- `/contacts`
- `/companies`
- `/deals`
- `/notes`
35 changes: 35 additions & 0 deletions app/companies/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { companies } from "@/lib/sample-data";

export default function CompaniesPage() {
return (
<div className="space-y-6">
<header className="border-b border-line pb-5">
<p className="text-sm font-black uppercase tracking-[0.16em] text-brick">
Companies
</p>
<h1 className="mt-2 text-4xl font-black text-graphite">Accounts</h1>
</header>

<section className="grid gap-4 md:grid-cols-2">
{companies.map((company) => (
<article key={company.id} className="border border-line bg-white p-5">
<p className="text-sm font-bold uppercase tracking-[0.12em] text-pine">
{company.industry}
</p>
<h2 className="mt-2 text-2xl font-black">{company.name}</h2>
<dl className="mt-4 grid gap-3 text-sm">
<div>
<dt className="font-black text-zinc">Domain</dt>
<dd>{company.domain}</dd>
</div>
<div>
<dt className="font-black text-zinc">Owner</dt>
<dd>{company.owner}</dd>
</div>
</dl>
</article>
))}
</section>
</div>
);
}
37 changes: 37 additions & 0 deletions app/contacts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { contacts } from "@/lib/sample-data";

export default function ContactsPage() {
return (
<div className="space-y-6">
<header className="border-b border-line pb-5">
<p className="text-sm font-black uppercase tracking-[0.16em] text-brick">
Contacts
</p>
<h1 className="mt-2 text-4xl font-black text-graphite">People</h1>
</header>

<div className="overflow-hidden border border-line bg-white">
<table className="w-full min-w-[680px] border-collapse text-left">
<thead className="bg-linen">
<tr className="text-sm uppercase tracking-[0.12em] text-zinc">
<th className="px-4 py-3">Name</th>
<th className="px-4 py-3">Email</th>
<th className="px-4 py-3">Company</th>
<th className="px-4 py-3">Updated</th>
</tr>
</thead>
<tbody className="divide-y divide-line">
{contacts.map((contact) => (
<tr key={contact.id}>
<td className="px-4 py-4 font-black">{contact.name}</td>
<td className="px-4 py-4 text-zinc">{contact.email}</td>
<td className="px-4 py-4">{contact.company}</td>
<td className="px-4 py-4 text-zinc">{contact.lastUpdated}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions app/deals/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { deals } from "@/lib/sample-data";

const stages = ["Lead", "Contacted", "Proposal", "Won"] as const;

export default function DealsPage() {
return (
<div className="space-y-6">
<header className="border-b border-line pb-5">
<p className="text-sm font-black uppercase tracking-[0.16em] text-brick">
Deals
</p>
<h1 className="mt-2 text-4xl font-black text-graphite">Pipeline</h1>
</header>

<section className="grid gap-4 xl:grid-cols-4">
{stages.map((stage) => (
<div key={stage} className="border border-line bg-white">
<div className="border-b border-line bg-linen px-4 py-3">
<h2 className="font-black">{stage}</h2>
</div>
<div className="grid gap-3 p-3">
{deals
.filter((deal) => deal.stage === stage)
.map((deal) => (
<article key={deal.id} className="border border-line p-4">
<p className="font-black">{deal.title}</p>
<p className="mt-1 text-sm text-zinc">{deal.contact}</p>
<p className="mt-4 text-lg font-black text-brass">{deal.value}</p>
</article>
))}
</div>
</div>
))}
</section>
</div>
);
}
29 changes: 29 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
color-scheme: light;
background: #f6f1e8;
color: #222426;
}

* {
box-sizing: border-box;
}

html {
min-width: 320px;
}

body {
min-height: 100vh;
margin: 0;
background: #f6f1e8;
font-family: "Aptos", "Segoe UI", sans-serif;
}

a {
color: inherit;
text-decoration: none;
}
53 changes: 53 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = {
title: "Dead Simple CRM",
description: "A minimal CRM shell for contacts, companies, deals, and notes."
};

const navItems = [
{ href: "/", label: "Dashboard" },
{ href: "/contacts", label: "Contacts" },
{ href: "/companies", label: "Companies" },
{ href: "/deals", label: "Deals" },
{ href: "/notes", label: "Notes" }
];

export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<div className="min-h-screen lg:grid lg:grid-cols-[260px_1fr]">
<aside className="border-b border-line bg-graphite text-linen lg:min-h-screen lg:border-b-0 lg:border-r">
<div className="flex h-full flex-col gap-6 p-5">
<Link href="/" className="block border-b border-linen/15 pb-5">
<span className="block text-2xl font-black">Dead Simple</span>
<span className="block text-sm font-bold uppercase tracking-[0.14em] text-linen/55">
CRM
</span>
</Link>
<nav className="flex flex-wrap gap-2 lg:grid" aria-label="Primary">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="border border-linen/15 px-3 py-2 text-sm font-bold text-linen/75 transition hover:border-linen hover:text-linen"
>
{item.label}
</Link>
))}
</nav>
</div>
</aside>
<main className="px-5 py-6 lg:px-8 lg:py-8">{children}</main>
</div>
</body>
</html>
);
}
28 changes: 28 additions & 0 deletions app/notes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { notes } from "@/lib/sample-data";

export default function NotesPage() {
return (
<div className="space-y-6">
<header className="border-b border-line pb-5">
<p className="text-sm font-black uppercase tracking-[0.16em] text-brick">
Notes
</p>
<h1 className="mt-2 text-4xl font-black text-graphite">Conversation log</h1>
</header>

<section className="grid gap-4">
{notes.map((note) => (
<article key={note.id} className="border border-line bg-white p-5">
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
<h2 className="text-xl font-black">{note.subject}</h2>
<span className="text-sm font-bold uppercase tracking-[0.12em] text-zinc">
{note.date}
</span>
</div>
<p className="mt-3 leading-7 text-zinc">{note.body}</p>
</article>
))}
</section>
</div>
);
}
73 changes: 73 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Link from "next/link";
import { companies, contacts, deals, notes } from "@/lib/sample-data";

const metrics = [
{ label: "Contacts", value: contacts.length, href: "/contacts" },
{ label: "Companies", value: companies.length, href: "/companies" },
{ label: "Deals", value: deals.length, href: "/deals" },
{ label: "Notes", value: notes.length, href: "/notes" }
];

export default function DashboardPage() {
return (
<div className="space-y-8">
<header className="border-b border-line pb-6">
<p className="text-sm font-black uppercase tracking-[0.16em] text-brick">
Workspace
</p>
<h1 className="mt-2 text-4xl font-black text-graphite">Sales desk</h1>
</header>

<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{metrics.map((metric) => (
<Link
key={metric.label}
href={metric.href}
className="border border-line bg-white p-5 transition hover:-translate-y-1 hover:border-graphite"
>
<p className="text-4xl font-black text-pine">{metric.value}</p>
<p className="mt-2 text-sm font-bold uppercase tracking-[0.12em] text-zinc">
{metric.label}
</p>
</Link>
))}
</section>

<section className="grid gap-5 xl:grid-cols-[1fr_0.8fr]">
<div className="border border-line bg-white">
<div className="border-b border-line px-4 py-3">
<h2 className="font-black">Active deals</h2>
</div>
<div className="divide-y divide-line">
{deals.map((deal) => (
<div key={deal.id} className="grid gap-2 px-4 py-4 md:grid-cols-[1fr_auto]">
<div>
<p className="font-black">{deal.title}</p>
<p className="text-sm text-zinc">{deal.contact}</p>
</div>
<div className="text-left md:text-right">
<p className="font-black text-brass">{deal.value}</p>
<p className="text-sm text-zinc">{deal.stage}</p>
</div>
</div>
))}
</div>
</div>

<div className="border border-line bg-white">
<div className="border-b border-line px-4 py-3">
<h2 className="font-black">Recent notes</h2>
</div>
<div className="divide-y divide-line">
{notes.map((note) => (
<div key={note.id} className="px-4 py-4">
<p className="font-black">{note.subject}</p>
<p className="mt-1 text-sm leading-6 text-zinc">{note.body}</p>
</div>
))}
</div>
</div>
</section>
</div>
);
}
Loading