All pages

Admin

The rows below are fixtures written into each demo file, not a live account. What the real surface passes comes from GET /admin/users/:id, and the api enforces the admin allowlist on that call.

StatsCard

Matches
3
Offers held
2
Posts published
1 of 2

One number with a label. These count what is on the page rather than what is in the database.

src/lib/docs/showcase/stats-card.tsx

import { StatsCard } from '@/components/Admin/StatsCard/StatsCard'

export function StatsCardDemo() {
  return (
    <div className="flex flex-wrap gap-10">
      <StatsCard title="Matches" value={3} />
      <StatsCard title="Offers held" value={2} />
      <StatsCard title="Posts published" value="1 of 2" />
    </div>
  )
}

UserSummaryCard

User

Name:
Ada Lovelace
Email:
ada@example.com
Clerk id:
user_2pQ7hK3mVxYt1aB9cD0eF4gH5i
User id:
3f6a2c18-9b41-4d0e-9a7f-2c85d1e4b730
Created:
Jul 14, 2026

Who a row is. There is no edit and no delete: nothing writes what someone is entitled to, and refunding in Stripe is the only revoke control.

src/lib/docs/showcase/user-summary-card.tsx

import type { AdminUserSummary } from '@ffd/shared'
import { UserSummaryCard } from '@/components/Admin/UserSummaryCard/UserSummaryCard'

const USER: AdminUserSummary = {
  id: '3f6a2c18-9b41-4d0e-9a7f-2c85d1e4b730',
  clerkUserId: 'user_2pQ7hK3mVxYt1aB9cD0eF4gH5i',
  email: 'ada@example.com',
  displayName: 'Ada Lovelace',
  createdAt: '2026-07-14T09:12:04.000Z',
}

export function UserSummaryCardDemo() {
  return <UserSummaryCard user={USER} />
}

EntitlementsCard

Entitlement

publishingboilerplatelifetime-updates
  • proOne-time payment · since Jul 14, 2026
  • creator-monthlySubscription · since Aug 2, 2026

What a user may do and what they paid for it to be true. Features and offers are two lists because two offers can grant the same feature.

src/lib/docs/showcase/entitlements-card.tsx

import type { EntitlementsResponse } from '@ffd/shared'
import { EntitlementsCard } from '@/components/Admin/EntitlementsCard/EntitlementsCard'

// Three features from two offers, because two offers can grant the same
// feature and the counts are not meant to match.
const ENTITLEMENTS: EntitlementsResponse = {
  features: ['publishing', 'boilerplate', 'lifetime-updates'],
  offers: [
    { slug: 'pro', source: 'purchase', since: '2026-07-14T09:31:22.000Z' },
    {
      slug: 'creator-monthly',
      source: 'subscription',
      since: '2026-08-02T11:04:51.000Z',
    },
  ],
  channelLimit: 15,
}

export function EntitlementsCardDemo() {
  return <EntitlementsCard entitlements={ENTITLEMENTS} />
}

BillingHistoryCard

Billing history

  • creator-monthlyActive
    Subscription · Aug 2, 2026$29.00
    price_1UTNrVATDpUETzMdR7nBnsUO
  • proPaid
    One-time payment · Jul 14, 2026$169.00
    price_1PED7uP4McnT55wKre6G3Rjd
  • Unknown offerCanceled
    Subscription · May 30, 2026$9.00
    price_1T9xQd1DJWXEccvOa2LmPq7R
  • proPaid
    One-time payment · Apr 11, 2026$169.00
    price_1PED7uP4McnT55wKre6G3Rjd
    $169.00 refunded on Apr 18, 2026

Every paid Stripe object recorded against a user, newest first, as one chronological list with a kind on each row. A refunded purchase says so, because it is the row that explains why access ended.

src/lib/docs/showcase/billing-history-card.tsx

import type { BillingEntry } from '@ffd/shared'
import { BillingHistoryCard } from '@/components/Admin/BillingHistoryCard/BillingHistoryCard'

// The third row is a price that has left the catalog, which is why its slug is
// null. The payment still happened, so the row still shows. The fourth is a
// fully refunded purchase, which is the one shape that no longer grants.
const ENTRIES: BillingEntry[] = [
  {
    id: 'sub_1UBAPn1DJWXEccvOw3Z1dOjV',
    kind: 'subscription',
    slug: 'creator-monthly',
    stripePriceId: 'price_1UTNrVATDpUETzMdR7nBnsUO',
    amount: 2_900,
    currency: 'usd',
    status: 'active',
    occurredAt: '2026-08-02T11:04:51.000Z',
    refundedAt: null,
    amountRefunded: 0,
  },
  {
    id: 'pi_3PEDaBP4McnT55wK1kQvJm2X',
    kind: 'purchase',
    slug: 'pro',
    stripePriceId: 'price_1PED7uP4McnT55wKre6G3Rjd',
    amount: 16_900,
    currency: 'usd',
    status: 'paid',
    occurredAt: '2026-07-14T09:31:22.000Z',
    refundedAt: null,
    amountRefunded: 0,
  },
  {
    id: 'sub_1T9xQd1DJWXEccvOa2LmPq7R',
    kind: 'subscription',
    slug: null,
    stripePriceId: 'price_1T9xQd1DJWXEccvOa2LmPq7R',
    amount: 900,
    currency: 'usd',
    status: 'canceled',
    occurredAt: '2026-05-30T16:47:09.000Z',
    refundedAt: null,
    amountRefunded: 0,
  },
  {
    id: 'pi_3NxKwQP4McnT55wK0aBcDe4F',
    kind: 'purchase',
    slug: 'pro',
    stripePriceId: 'price_1PED7uP4McnT55wKre6G3Rjd',
    amount: 16_900,
    currency: 'usd',
    status: 'paid',
    occurredAt: '2026-04-11T08:15:03.000Z',
    refundedAt: '2026-04-18T13:22:40.000Z',
    amountRefunded: 16_900,
  },
]

export function BillingHistoryCardDemo() {
  return <BillingHistoryCard entries={ENTRIES} />
}

Not shown here

UserLookup and AdminGate paint from a live query or from the Clerk session, so what you saw in a frame would depend on whether you happen to be signed in. Read them at src/components/Admin/.

Updated