TotalCustomers.tsx

A social proof component in Plainform that displays customer avatars, star ratings, and total customer count from paid Stripe checkout sessions

The TotalCustomers component fetches real customer data from Stripe and displays it as social proof with avatars, star ratings, and customer count. It's commonly used in hero sections to build trust.

Props

TotalCustomers PropsTypeDefault
text
string
-

Usage

Basic Implementation

@/app/(base)/page.tsx
import { TotalCustomers } from '@/components/TotalCustomers';

export default function HomePage() {
  return (
    <section>
      <h1>Welcome to Our Product</h1>
      <TotalCustomers text="customers already using this" />
    </section>
  );
}

In Hero Section

@/components/Hero.tsx
import { TotalCustomers } from '@/components/TotalCustomers';
import locale from '@/locales/en.json';

export function Hero() {
  const heroLocale = locale?.homePage?.heroSection;

  return (
    <section className="flex flex-col items-center gap-8">
      <h1>{heroLocale?.title}</h1>
      <p>{heroLocale?.description}</p>
      <TotalCustomers text={heroLocale?.customersText} />
    </section>
  );
}

Features

  • Real Data: Fetches actual customer count from paid Stripe checkout sessions
  • Customer Avatars: Displays profile images from your S3 mockupUsers/ folder
  • Star Rating: Shows 5-star rating visualization
  • Responsive Design: Adapts layout for mobile and desktop
  • Automatic Fallback: Returns null if no orders exist
  • Image Optimization: Uses Next.js Image with fallback support

Implementation Details

Data Fetching

The component uses the getCustomers() helper to fetch data from the Stripe customers API route:

@/lib/stripe/getCustomers.ts
export async function getCustomers() {
  const res = await fetch(`${env.SITE_URL}/api/stripe/customers`, {
    cache: 'force-cache',
    next: { tags: ['stripe/customers'] },
  });

  return res.json();
}

Returns:

  • totalCustomersCount - Number of unique paid Stripe customers
  • imageUrls - Array of customer avatar/mockup image URLs

Customers API Route

The API route at app/api/stripe/customers/route.ts counts unique paid customers from Stripe checkout sessions:

app/api/stripe/customers/route.ts
const paidCustomerIds = new Set<string>();

allSessions.forEach((session) => {
  if (
    session.payment_status === 'paid' &&
    typeof session.customer === 'string'
  ) {
    paidCustomerIds.add(session.customer);
  }
});

const totalCustomersCount = paidCustomerIds.size;

It also reads avatar images from S3 using the mockupUsers/ prefix. The API requests at most 5 images from S3, then limits the returned avatar list to the number of paid customers.

For example:

  • 2 paid customers and 5 images in S3 → 2 avatars displayed
  • 5 paid customers and 3 images in S3 → 3 avatars displayed
  • 12 paid customers and 10 images in S3 → at most 5 avatars displayed

The count is based on unique Stripe customer IDs from paid checkout sessions, not the number of local Clerk users or raw checkout sessions.

Avatar Display

Customer avatars are displayed with overlapping effect:

Avatar Styling
<div className="flex items-center -space-x-2">
  {imageUrls?.map((img, i) => (
    <ImageWithFallback
      src={img}
      alt={`User Mockup ${i}`}
      width={84}
      height={84}
      className="size-12 object-cover rounded-full ring-4 ring-surface"
      key={`user-${i}`}
    />
  ))}
</div>

The -space-x-2 class creates the overlapping effect.

Star Rating

The component renders 5 filled star icons:

Star Rating
<div className="flex items-center gap-1">
  <SvgFinder icon="Star" className="fill-yellow-400 stroke-yellow-400" />
  <SvgFinder icon="Star" className="fill-yellow-400 stroke-yellow-400" />
  <SvgFinder icon="Star" className="fill-yellow-400 stroke-yellow-400" />
  <SvgFinder icon="Star" className="fill-yellow-400 stroke-yellow-400" />
  <SvgFinder icon="Star" className="fill-yellow-400 stroke-yellow-400" />
</div>

Customization

Custom Text

Pass different text for various contexts:

Different Contexts
{/* Hero section */}
<TotalCustomers text="happy customers" />

{/* Pricing page */}
<TotalCustomers text="businesses trust us" />

{/* Testimonials section */}
<TotalCustomers text="5-star reviews" />

Styling Avatars

Customize avatar appearance:

Custom Avatar Styling
<ImageWithFallback
  src={img}
  alt={`User ${i}`}
  width={84}
  height={84}
  className="size-16 object-cover rounded-full ring-2 ring-primary"
/>

Changing Star Color

Modify star icon colors:

Custom Star Colors
<SvgFinder icon="Star" className="fill-blue-400 stroke-blue-400" />

Locale Configuration

Configure the text in locales/en.json:

@/locales/en.json
{
  "homePage": {
    "heroSection": {
      "customersText": "customers already using this"
    }
  }
}

Conditional Rendering

The component returns null if there are no paid customers:

Conditional Rendering
if (!data?.totalCustomersCount) {
  return null;
}

This prevents displaying "0 customers" on new installations.

Performance

  • Server Component: Fetches data on the server for optimal performance
  • Image Optimization: Uses Next.js Image with priority flag
  • Quality Setting: Images loaded at 80% quality for faster loading

Styling

The component uses Tailwind CSS with responsive design:

  • Desktop: Horizontal layout with avatars and text side-by-side
  • Mobile: Vertical layout with centered content (max-md:flex-col max-md:items-center)
  • Avatar rings: ring-4 ring-surface creates white border effect
  • Text color: text-neutral-foreground for secondary text

How is this guide ?

Last updated on

On this page