Skip to content
UI

Search docs

Search components and documentation

New
← Back to application blocks

admin-customers-01

A customers back-office view: a KPI row (total customers, new this month, repeat rate) over a customers table with avatars, order count, lifetime spend, last order, active/new/churned segment badges, per-row actions and live search. Use as the customers tab of a store admin.

Code

"use client"

import * as React from "react"
import { MoreHorizontalIcon, SearchIcon, UserPlusIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

type Segment = "active" | "new" | "churned"
type Customer = {
  name: string
  email: string
  orders: number
  spent: string
  lastOrder: string
  segment: Segment
}

const KPIS = [
  { label: "Total customers", value: "8,942" },
  { label: "New this month", value: "312" },
  { label: "Repeat rate", value: "48%" },
]

const CUSTOMERS: Customer[] = [
  { name: "Olivia Martin", email: "olivia@example.com", orders: 24, spent: "$3,120", lastOrder: "2026-07-14", segment: "active" },
  { name: "Jackson Lee", email: "jackson@example.com", orders: 2, spent: "$168", lastOrder: "2026-07-10", segment: "new" },
  { name: "Isabella Nguyen", email: "isabella@example.com", orders: 41, spent: "$6,540", lastOrder: "2026-07-13", segment: "active" },
  { name: "William Kim", email: "william@example.com", orders: 8, spent: "$742", lastOrder: "2026-02-18", segment: "churned" },
  { name: "Sofia Davis", email: "sofia@example.com", orders: 1, spent: "$18", lastOrder: "2026-07-12", segment: "new" },
  { name: "Liam Johnson", email: "liam@example.com", orders: 17, spent: "$2,410", lastOrder: "2026-07-09", segment: "active" },
]

const SEGMENT_VARIANT: Record<Segment, "default" | "secondary" | "outline"> = {
  active: "default",
  new: "secondary",
  churned: "outline",
}

const initials = (name: string) =>
  name
    .split(" ")
    .map((p) => p[0])
    .slice(0, 2)
    .join("")

export function AdminCustomers({ className, ...props }: React.ComponentProps<"div">) {
  const [query, setQuery] = React.useState("")

  const filtered = CUSTOMERS.filter((c) => {
    const q = query.toLowerCase()
    return c.name.toLowerCase().includes(q) || c.email.toLowerCase().includes(q)
  })

  return (
    <div className={cn("mx-auto w-full max-w-5xl px-6 py-10", className)} {...props}>
      <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex flex-col gap-1">
          <h1 className="font-heading text-2xl font-bold tracking-tight">Customers</h1>
          <p className="text-muted-foreground text-sm">
            Your customer base, segments and lifetime value.
          </p>
        </div>
        <Button>
          <UserPlusIcon className="size-4" />
          Add customer
        </Button>
      </div>

      <div className="mb-6 grid gap-4 sm:grid-cols-3">
        {KPIS.map((kpi) => (
          <Card key={kpi.label}>
            <CardHeader className="pb-2">
              <CardDescription>{kpi.label}</CardDescription>
              <CardTitle className="text-2xl tabular-nums">{kpi.value}</CardTitle>
            </CardHeader>
          </Card>
        ))}
      </div>

      <Card>
        <CardHeader className="flex flex-row items-center justify-between">
          <div className="grid gap-1.5">
            <CardTitle>All customers</CardTitle>
            <CardDescription>Showing {filtered.length} of {CUSTOMERS.length}</CardDescription>
          </div>
          <div className="relative">
            <SearchIcon className="text-muted-foreground absolute top-2.5 left-2.5 size-4" />
            <Input
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="Search customers..."
              className="w-full pl-8 sm:w-[240px]"
              aria-label="Search customers"
            />
          </div>
        </CardHeader>
        <CardContent>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>Customer</TableHead>
                <TableHead className="hidden sm:table-cell">Orders</TableHead>
                <TableHead className="text-right">Spent</TableHead>
                <TableHead className="hidden md:table-cell">Last order</TableHead>
                <TableHead>Segment</TableHead>
                <TableHead>
                  <span className="sr-only">Actions</span>
                </TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {filtered.map((c) => (
                <TableRow key={c.email}>
                  <TableCell>
                    <div className="flex items-center gap-2">
                      <Avatar className="size-7">
                        <AvatarFallback className="text-xs">{initials(c.name)}</AvatarFallback>
                      </Avatar>
                      <div className="min-w-0">
                        <p className="truncate text-sm font-medium">{c.name}</p>
                        <p className="text-muted-foreground truncate text-xs">{c.email}</p>
                      </div>
                    </div>
                  </TableCell>
                  <TableCell className="hidden sm:table-cell tabular-nums">{c.orders}</TableCell>
                  <TableCell className="text-right font-medium tabular-nums">{c.spent}</TableCell>
                  <TableCell className="hidden md:table-cell tabular-nums">{c.lastOrder}</TableCell>
                  <TableCell>
                    <Badge variant={SEGMENT_VARIANT[c.segment]}>{c.segment}</Badge>
                  </TableCell>
                  <TableCell>
                    <DropdownMenu>
                      <DropdownMenuTrigger asChild>
                        <Button variant="ghost" size="icon" aria-label={`Actions for ${c.name}`}>
                          <MoreHorizontalIcon className="size-4" />
                        </Button>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent align="end">
                        <DropdownMenuLabel>Actions</DropdownMenuLabel>
                        <DropdownMenuItem>View profile</DropdownMenuItem>
                        <DropdownMenuItem>Email customer</DropdownMenuItem>
                        <DropdownMenuSeparator />
                        <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </TableCell>
                </TableRow>
              ))}
              {filtered.length === 0 && (
                <TableRow>
                  <TableCell colSpan={6} className="text-muted-foreground h-24 text-center">
                    No customers found.
                  </TableCell>
                </TableRow>
              )}
            </TableBody>
          </Table>
        </CardContent>
      </Card>
    </div>
  )
}