قیمتگذاریِ مصرفی
پیشنمایش تمامصفحهقیمتگذاریِ پلکانی به اندازهٔ مصرف: اسلایدرِ حجمِ ماهانه، جدولِ پلهها با پلهٔ درگیرِ برجسته و سهمِ هر پله، و کارتِ تیرهٔ هزینهٔ برآوردی با میانگینِ هر واحد.
۱. نصب کامپوننتها
این بلاک از ۵ آیتم دیگ استفاده میکند: buttoncontrol-sizeiconspersianslider
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/slider.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/slider.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/slider.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/slider.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
pricing-usage.tsx
"use client"
import * as React from "react"
import { InfoIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Slider } from "@/components/ui/slider"
import { formatNumber } from "@/lib/persian"
import { cn } from "@/lib/utils"
/** پلههای مصرف: هر هزار پیامک در هر پله قیمتِ خودش را دارد. */
const TIERS = [
{ upTo: 10_000, price: 0, label: "۱۰ هزارِ اول" },
{ upTo: 100_000, price: 180, label: "۱۰ تا ۱۰۰ هزار" },
{ upTo: 1_000_000, price: 140, label: "۱۰۰ هزار تا ۱ میلیون" },
{ upTo: Infinity, price: 110, label: "بیش از ۱ میلیون" },
]
const STOPS = [5_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 2_000_000]
function costOf(volume: number) {
let rest = volume
let from = 0
return TIERS.map((tier) => {
const used = Math.max(0, Math.min(rest, tier.upTo - from))
rest -= used
from = tier.upTo
return { ...tier, used, cost: used * tier.price }
})
}
/**
* قیمتگذاریِ مصرفی و پلکانی: اسلایدرِ حجمِ ماهانه (پیامک یا درخواست) با پلههای معنادار،
* جدولِ پلهها که پلهٔ درگیر را برجسته و سهمِ هر پله را جدا نشان میدهد، و جمعِ برآوردشده با
* توضیحِ اینکه فقط مصرفِ واقعی حساب میشود.
*/
export default function PricingUsageBlock() {
const [stop, setStop] = React.useState(3)
const volume = STOPS[stop]
const rows = costOf(volume)
const total = rows.reduce((sum, row) => sum + row.cost, 0)
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-12 px-4 py-24 sm:px-6">
<header className="flex flex-col items-center gap-4 text-center">
<span className="text-sm font-semibold text-brand-secondary">پرداخت به اندازهٔ مصرف</span>
<h1 className="text-4xl leading-tight font-black text-balance sm:text-5xl">هر چه فرستادی، همان را بپرداز</h1>
<p className="max-w-lg leading-7 text-muted-foreground">بدونِ اشتراکِ ماهانه؛ ۱۰ هزار پیامکِ اول هر ماه رایگان است و هرچه بیشتر بفرستید، هر پیامک ارزانتر میشود.</p>
</header>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1.4fr_1fr]">
<section aria-labelledby="pu-calc" className="flex flex-col gap-8 rounded-xl border border-border bg-card p-6 sm:p-8">
<div className="flex flex-col gap-1">
<h2 id="pu-calc" className="text-lg font-bold">
حجمِ ماهانه
</h2>
<span className="text-3xl font-black tabular-nums">{formatNumber(volume)} پیامک</span>
</div>
<Slider
value={[stop]}
min={0}
max={STOPS.length - 1}
step={1}
onValueChange={(value) => setStop(value[0])}
aria-label="حجمِ ماهانهٔ پیامک"
aria-valuetext={`${formatNumber(volume)} پیامک`}
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{formatNumber(STOPS[0])}</span>
<span>{formatNumber(STOPS[STOPS.length - 1])}</span>
</div>
<ul className="flex flex-col divide-y divide-border rounded-lg border border-border">
{rows.map((row) => (
<li key={row.label} className={cn("flex items-center justify-between gap-4 px-4 py-3 text-sm", row.used > 0 ? "bg-subtle" : "text-muted-foreground")}>
<span className="flex flex-col">
<span className="font-semibold">{row.label}</span>
<span className="text-xs text-muted-foreground">{row.price === 0 ? "رایگان" : `${formatNumber(row.price)} تومان هر پیامک`}</span>
</span>
<span className="tabular-nums">{row.used > 0 ? `${formatNumber(row.used)} × ` : "—"}</span>
</li>
))}
</ul>
</section>
<aside aria-live="polite" className="flex flex-col justify-between gap-8 rounded-xl bg-inverse p-6 text-[var(--dig-white)] [--primary-foreground:var(--dig-graphite-950)] [--primary:var(--dig-white)] sm:p-8">
<div className="flex flex-col gap-2">
<span className="text-sm text-[var(--dig-neutral-400)]">هزینهٔ برآوردیِ این ماه</span>
<span className="text-4xl font-black tabular-nums">{total === 0 ? "رایگان" : `${formatNumber(Math.round(total / 1000))} هزار تومان`}</span>
<span className="text-sm text-[var(--dig-neutral-400)]">میانگینِ هر پیامک: {volume > 0 ? formatNumber(Math.round(total / volume)) : 0} تومان</span>
</div>
<p className="flex items-start gap-2 text-sm leading-6 text-[var(--dig-neutral-300)]">
<InfoIcon aria-hidden className="mt-1 size-4 shrink-0" />
آخرِ ماه فقط پیامکهای تحویلشده حساب میشود؛ ناموفقها رایگاناند.
</p>
<div className="flex flex-col gap-2">
<Button fullWidth>شروعِ رایگان</Button>
<Button fullWidth variant="link" className="text-[var(--dig-white)]">
بیش از ۲ میلیون؟ تماس با فروش
</Button>
</div>
</aside>
</div>
</main>
</ControlSizeProvider>
)
}