Приложение и дашбордappdashboardsaasbillingplanusagemeter
Plan Usage Card
Сводка по оплате: тариф, продление, счётчики использования с порогами, способ оплаты и повышение тарифа.
Живое превью
Current plan
Pro €149 / month
Renews on Dec 1, 2026
Visa •••• 4242
- Rooms42 of 50
84% used — consider upgrading soon.
- Team seats4 of 10
- Channel connections3 of 5
- Guest emails8,200 of 10,000 / month
82% used — consider upgrading soon.
Использование
Скопируйте и вставьте этот код в ваш React-компонент:
tsx
import { PlanUsageCard } from 'react-component-kit-ui/app';
<PlanUsageCard plan="Pro" usage={[{ label: 'Rooms', used: 42, limit: 50 }]} />Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/PlanUsageCard.tsx + PlanUsageCard.css:
PlanUsageCard.tsx
'use client';
import type { CSSProperties } from 'react';
import './PlanUsageCard.css';
/** class:list → className */
function cx(...args: unknown[]): string {
const out: string[] = [];
const walk = (v: unknown) => {
if (!v) return;
if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
else if (Array.isArray(v)) v.forEach(walk);
else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
};
args.forEach(walk);
return out.join(' ');
}
/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
const o: Record<string, string> = {};
for (const decl of String(css ?? '').split(';')) {
const i = decl.indexOf(':');
if (i < 0) continue;
const k = decl.slice(0, i).trim();
if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
}
return o as CSSProperties;
}
/**
* PlanUsageCard — Billing summary: current plan and price, renewal date, usage meters with
* warning thresholds, payment method and upgrade / manage actions.
*
* @param {string} plan - Plan name. Default 'Pro'.
* @param {string} price - Price label. Default '€149 / month'.
* @param {string} renews - Renewal text. Default 'Renews on Dec 1, 2026'.
* @param {Array<{label: string, used: number, limit: number, unit?: string}>} usage - Meters.
* @param {string} card - Payment method label (optional). Default 'Visa •••• 4242'.
* @param {{label: string, href: string}} upgrade - Primary action. Default upgrade.
* @param {{label: string, href: string}} manage - Secondary action (optional).
* @param {number} warnAt - Percentage at which a meter turns amber. Default 80.
* @param {string} variant - Style (card|accent). Default 'card'.
* @param {string} className - Extra class.
*
* Accessibility: each meter is a role="meter" with aria-valuenow/min/max and a text value,
* so usage is never conveyed by colour alone.
*/
export interface PlanUsageCardProps {
plan?: string;
price?: string;
renews?: string;
usage?: Array<{ label: string; used: number; limit: number; unit?: string }>;
card?: string;
upgrade?: { label: string; href: string };
manage?: { label: string; href: string };
warnAt?: number;
variant?: 'card' | 'accent';
className?: string;
}
export function PlanUsageCard({
plan = 'Pro',
price = '€149 / month',
renews = 'Renews on Dec 1, 2026',
usage = [
{ label: 'Rooms', used: 42, limit: 50 },
{ label: 'Team seats', used: 4, limit: 10 },
{ label: 'Channel connections', used: 3, limit: 5 },
{ label: 'Guest emails', used: 8200, limit: 10000, unit: '/ month' },
],
card = 'Visa •••• 4242',
upgrade = { label: 'Upgrade to Business', href: '#' },
manage = { label: 'Manage billing', href: '#' },
warnAt = 80,
variant = 'card',
className = '',
}: PlanUsageCardProps) {
const fmt = (n: number) => new Intl.NumberFormat('en-GB').format(n);
return (
<>
<section className={cx(['plan', `plan--${variant}`, className])} aria-labelledby="plan-title" data-rk-aac02d="">
<header className="plan__head" data-rk-aac02d="">
<div data-rk-aac02d="">
<p className="plan__eyebrow" data-rk-aac02d="">Current plan</p>
<h2 className="plan__title" id="plan-title" data-rk-aac02d="">{plan} <span data-rk-aac02d="">{price}</span></h2>
<p className="plan__renew" data-rk-aac02d="">{renews}</p>
</div>
{card && <p className="plan__card" data-rk-aac02d=""><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-aac02d=""><rect x="2" y="5" width="20" height="14" rx="2" data-rk-aac02d="" /><path d="M2 10h20" data-rk-aac02d="" /></svg>{card}</p>}
</header>
<ul className="plan__meters" data-rk-aac02d="">
{usage.map((u) => {
const pct = Math.min(100, Math.round((u.used / u.limit) * 100));
const tone = pct >= 100 ? 'danger' : pct >= warnAt ? 'warning' : 'ok';
return (
<li data-rk-aac02d="">
<div className="plan__meter-head" data-rk-aac02d="">
<span data-rk-aac02d="">{u.label}</span>
<strong data-rk-aac02d="">{fmt(u.used)} <small data-rk-aac02d="">of {fmt(u.limit)}{u.unit ? ` ${u.unit}` : ''}</small></strong>
</div>
<div className={cx(['plan__bar', `plan__bar--${tone}`])} role="meter" aria-label={u.label} aria-valuemin={0} aria-valuemax={u.limit} aria-valuenow={u.used} aria-valuetext={`${fmt(u.used)} of ${fmt(u.limit)}, ${pct}%`} data-rk-aac02d="">
<span style={st(`width: ${pct}%`)} data-rk-aac02d=""></span>
</div>
{tone === 'warning' && <p className="plan__warn" data-rk-aac02d="">{pct}% used — consider upgrading soon.</p>}
</li>
);
})}
</ul>
<div className="plan__actions" data-rk-aac02d="">
{upgrade && <a className="plan__btn plan__btn--primary" href={upgrade.href} data-rk-aac02d="">{upgrade.label}</a>}
{manage && <a className="plan__btn" href={manage.href} data-rk-aac02d="">{manage.label}</a>}
</div>
</section>
</>
);
}
export default PlanUsageCard;PlanUsageCard.css
/* PlanUsageCard — ported from astro-component-kit (app/PlanUsageCard.astro) */
.plan:where([data-rk-aac02d]) { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5, 1.25rem); padding: var(--sp-6, 1.5rem); border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); }
.plan--accent:where([data-rk-aac02d]) { background: radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--app-accent, var(--c-primary-light, #d4a017)) 22%, transparent), transparent 55%), var(--c-bg-1, #0a0a0a); }
.plan__head:where([data-rk-aac02d]) { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4, 1rem); }
.plan__eyebrow:where([data-rk-aac02d]) { margin: 0; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--app-accent, var(--c-primary-light, #d4a017)); }
.plan__title:where([data-rk-aac02d]) { margin: 4px 0 0; font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
.plan__title:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { font-size: 0.95rem; font-weight: 500; color: var(--c-text-2, #a3a3a3); }
.plan__renew:where([data-rk-aac02d]) { margin: 2px 0 0; font-size: 0.8rem; color: var(--c-text-3, #737373); }
.plan__card:where([data-rk-aac02d]) { display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 6px 10px; border-radius: 8px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); font-size: 0.8rem; color: var(--c-text-1, #fff); white-space: nowrap; }
.plan__meters:where([data-rk-aac02d]) { display: grid; gap: var(--sp-4, 1rem); margin: 0; padding: 0; list-style: none; }
.plan__meter-head:where([data-rk-aac02d]) { display: flex; justify-content: space-between; gap: var(--sp-2, 0.5rem); margin-bottom: 6px; font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
.plan__meter-head:where([data-rk-aac02d]) strong:where([data-rk-aac02d]) { color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
.plan__meter-head:where([data-rk-aac02d]) small:where([data-rk-aac02d]) { font-weight: 500; color: var(--c-text-3, #737373); }
.plan__bar:where([data-rk-aac02d]) { height: 8px; border-radius: 999px; background: rgb(var(--ink-rgb, 255 255 255) / 0.08); overflow: hidden; }
.plan__bar:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { display: block; height: 100%; border-radius: inherit; background: var(--app-accent, var(--c-primary-light, #d4a017)); }
.plan__bar--warning:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { background: var(--c-warning, #fbbf24); }
.plan__bar--danger:where([data-rk-aac02d]) span:where([data-rk-aac02d]) { background: var(--c-danger, #f87171); }
.plan__warn:where([data-rk-aac02d]) { margin: 4px 0 0; font-size: 0.75rem; color: var(--c-warning, #fbbf24); }
.plan__actions:where([data-rk-aac02d]) { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem); }
.plan__btn:where([data-rk-aac02d]) { display: inline-flex; align-items: center; min-height: 40px; padding: 0 1rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); color: var(--c-text-1, #fff); font-size: 0.85rem; font-weight: 600; text-decoration: none; }
.plan__btn:hover:where([data-rk-aac02d]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
.plan__btn--primary:where([data-rk-aac02d]) { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
.plan__btn--primary:hover:where([data-rk-aac02d]) { background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); filter: brightness(1.08); }
.plan__btn:focus-visible:where([data-rk-aac02d]) { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
@container (max-width: 420px) { .plan__head:where([data-rk-aac02d]) { flex-direction: column; } }Краткая информация
- Категория
- Приложение и дашборд
- Имя файла
PlanUsageCard.tsx- Зависимости
- Нет — чистый React + CSS
- Теги
- appdashboardsaasbillingplanusagemeter