Skip to content
Framework: Vite + React + TypeScriptDifficulty: intermediate

CRM pipeline

Contacts table plus a five-stage deal pipeline board with inline stage changes, all persisted locally.

A small CRM with two views over the same data: a searchable contacts table with stage selects and delete, and a kanban-style pipeline (Lead → Qualified → Proposal → Won/Lost) where deals move between columns with keyboard-accessible buttons. State lives in localStorage via a typed hook so the app works offline; swap src/store.ts for API calls when you add a backend.

  • react
  • crm
  • pipeline
  • kanban
  • tailwind

This preview is the template's actual file tree and source code — exactly what is copied into a new project. Codexa does not publish screenshots or mock-ups of templates; run the project to see it rendered.

Files14 files

src/ContactForm.tsx68 lines
import { useState, type FormEvent } from "react";
import { STAGES, type Contact, type Stage } from "./store";

export function ContactForm({ onSave, onCancel }: { onSave: (c: Omit<Contact, "id">) => void; onCancel: () => void }) {
  const [form, setForm] = useState<Omit<Contact, "id">>({ name: "", company: "", email: "", stage: "Lead", value: 0, note: "" });
  const [error, setError] = useState<string | null>(null);
  const set = <K extends keyof typeof form>(k: K, v: (typeof form)[K]) => setForm((f) => ({ ...f, [k]: v }));

  function submit(e: FormEvent) {
    e.preventDefault();
    if (!form.name.trim() || !form.company.trim()) return setError("Name and company are required.");
    if (!form.email.includes("@")) return setError("Enter a valid email.");
    onSave({ ...form, name: form.name.trim(), company: form.company.trim() });
  }

  return (
    <div className="fixed inset-0 z-20 flex items-end justify-center bg-black/40 p-4 sm:items-center" role="dialog" aria-modal="true" aria-labelledby="contact-form-title">
      <form onSubmit={submit} className="w-full max-w-md space-y-3 rounded-xl bg-white p-5 shadow-xl">
        <h2 id="contact-form-title" className="text-lg font-semibold">
          New contact
        </h2>
        <label className="block text-sm">
          <span>Name</span>
          <input value={form.name} onChange={(e) => set("name", e.target.value)} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" autoFocus />
        </label>
        <label className="block text-sm">
          <span>Company</span>
          <input value={form.company} onChange={(e) => set("company", e.target.value)} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
        </label>
        <label className="block text-sm">
          <span>Email</span>
          <input type="email" value={form.email} onChange={(e) => set("email", e.target.value)} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
        </label>
        <div className="grid grid-cols-2 gap-3">
          <label className="block text-sm">
            <span>Stage</span>
            <select value={form.stage} onChange={(e) => set("stage", e.target.value as Stage)} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2">
              {STAGES.map((s) => (
                <option key={s}>{s}</option>
              ))}
            </select>
          </label>
          <label className="block text-sm">
            <span>Value ($)</span>
            <input type="number" min={0} value={form.value} onChange={(e) => set("value", Number(e.target.value))} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
          </label>
        </div>
        <label className="block text-sm">
          <span>Note</span>
          <textarea value={form.note} onChange={(e) => set("note", e.target.value)} rows={2} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" />
        </label>
        {error ? (
          <p role="alert" className="text-sm text-red-600">
            {error}
          </p>
        ) : null}
        <div className="flex justify-end gap-2 pt-2">
          <button type="button" onClick={onCancel} className="rounded-md border border-slate-300 px-4 py-2 text-sm">
            Cancel
          </button>
          <button type="submit" className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white">
            Save
          </button>
        </div>
      </form>
    </div>
  );
}

Dependencies

Documented from the template's package.json. The runtime has a prebuilt dependency set for Vite + React + TypeScript, so installs are fast.

PackageVersion
react^19.1.0
react-dom^19.1.0
vite^6.3.0
@vitejs/plugin-react^4.4.0
tailwindcss^4.1.0
@tailwindcss/vite^4.1.0
typescript^5.8.0

How it runs

Framework
Vite + React + TypeScript
Dev command
npm run dev
Build command
npm run build
Output directory
dist
Port
5173
Opens at
/
Difficulty
intermediate

Projects run in an isolated sandbox with a live preview. Read the projects documentation for details on settings, snapshots and deployment.

Need the raw data? GET /api/v1/templates/vite-react-crm returns this template with its files.

Build on CRM pipeline

Create a free account, start from this template, and ask the AI agent to turn it into your app.