Skip to contentVibraUI

Product figures

Price, stock against the reorder point, and units and revenue over twelve weeks against the twelve before; reads productRecord(id).

Preview

Install

npx shadcn@latest add @vibra/widget-ecommerce-product-product-figures

Needs the @vibra registry in your components.json — set it up once.

Source

app/ecommerce/products/[id]/components/product-figures.tsx
"use client"

import { formatCurrency, formatNumber } from "@/lib/format"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"

import { useProduct } from "./product-context"

export type ProductFiguresProps = {
  reorderPoint: number
  weeks: number
  sales: { units: number; revenue: number }
  previous: { units: number; revenue: number }
}

/** A change as a ratio, or nothing when there is no earlier figure to divide by — a rise from nothing is no percentage. */
const change = (now: number, before: number) => (before === 0 ? (now === 0 ? 0 : undefined) : now / before - 1)

/**
 * The four figures under the title. Price and stock come from the provider,
 * so a save in the form below moves them; units and revenue are history, read
 * once on the server and measured against the weeks before. The stock tile
 * tints its frame once the shelf is at or below the reorder point.
 */
export function ProductFigures({ reorderPoint, weeks, sales, previous }: ProductFiguresProps) {
  const { figures } = useProduct()
  const low = figures.stock <= reorderPoint
  const span = `last ${weeks} weeks`

  return (
    <StatCardGroup data-widget="widget-ecommerce-product-product-figures" columns={4}>
      <StatCard label="Price" value={formatCurrency(figures.priceCents / 100)} description="list price" />
      <StatCard
        label="In stock"
        value={formatNumber(figures.stock)}
        description={`reorder at ${reorderPoint}`}
        tone={low ? "warning" : "neutral"}
      />
      <StatCard
        label="Units sold"
        value={formatNumber(sales.units)}
        delta={change(sales.units, previous.units)}
        description={span}
      />
      <StatCard
        label="Revenue"
        value={formatCurrency(sales.revenue, "USD", { maximumFractionDigits: 0 })}
        delta={change(sales.revenue, previous.revenue)}
        description={`${span}, at the prices charged`}
      />
    </StatCardGroup>
  )
}
app/ecommerce/products/[id]/components/product-context.tsx
"use client"

import * as React from "react"

/** The two numbers the edit form can change, held once for every card that prints them. */
export type ProductFigures = { priceCents: number; stock: number }

type ProductContextValue = {
  figures: ProductFigures
  setFigures: (next: ProductFigures) => void
}

const ProductContext = React.createContext<ProductContextValue | null>(null)

/**
 * Holds the price and the stock the server last confirmed. The page renders
 * them from the row it read; a save replaces them with what the action wrote
 * back, so the figures at the top and the form at the bottom never disagree.
 */
export function ProductProvider({ initial, children }: { initial: ProductFigures; children: React.ReactNode }) {
  const [figures, setFigures] = React.useState(initial)
  const value = React.useMemo(() => ({ figures, setFigures }), [figures])
  return <ProductContext.Provider value={value}>{children}</ProductContext.Provider>
}

export function useProduct(): ProductContextValue {
  const context = React.useContext(ProductContext)
  if (!context) throw new Error("useProduct must be used inside <ProductProvider>.")
  return context
}

Its page

On its page the card sits among the rest of the dashboard and shares its range and its data with them.

From the Product record page