Subscription Card

Carte de statut et de gestion d'abonnement pour les details du forfait, du renouvellement et de l'utilisation. Installez Subscription Card depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Carte de statut et de gestion d'abonnement pour les details du forfait, du renouvellement et de l'utilisation. Fait partie de la famille content dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez Subscription Card a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/subscription-card.json

Source

import * as React from "react"; import { cn } from "../../lib/utils"; import { Button } from "../button/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "../card/card"; import { PlanBadge, type PlanBadgeTier } from "../plan-badge/plan-badge"; export type SubscriptionCardStatus = | "active" | "canceled" | "past-due" | "trialing"; export type SubscriptionCardProps = React.ComponentPropsWithoutRef< typeof Card > & { note?: string; plan: PlanBadgeTier; priceLabel: string; primaryActionLabel?: string; renewalLabel: string; seatsLabel?: string; secondaryActionLabel?: string; status: SubscriptionCardStatus; usageLabel?: string; }; type SubscriptionActionsProps = { primaryActionLabel?: string; secondaryActionLabel?: string; }; type SubscriptionDetailsProps = { note?: string; priceLabel: string; renewalLabel: string; seatsLabel?: string; usageLabel?: string; }; function getStatusLabel(status: SubscriptionCardStatus): string { switch (status) { case "active": return "Active"; case "canceled": return "Canceled"; case "past-due": return "Past due"; case "trialing": return "Trialing"; } } function getStatusClasses(status: SubscriptionCardStatus): string { switch (status) { case "active": return "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"; case "canceled": return "bg-muted text-muted-foreground"; case "past-due": return "bg-amber-500/10 text-amber-700 dark:text-amber-300"; case "trialing": return "bg-sky-500/10 text-sky-700 dark:text-sky-300"; } } function getPlanState( status: SubscriptionCardStatus, ): "current" | "legacy" | "trial" { switch (status) { case "active": case "past-due": return "current"; case "canceled": return "legacy"; case "trialing": return "trial"; } } function DetailRow({ label, value }: { label: string; value: string }) { return ( <div className="flex items-center justify-between gap-4 text-sm"> <span className="text-muted-foreground">{label}</span> <span className="text-right font-medium">{value}</span> </div> ); } function SubscriptionDetails({ note, priceLabel, renewalLabel, seatsLabel, usageLabel, }: SubscriptionDetailsProps) { return ( <CardContent className="space-y-4"> <div className="rounded-lg border border-border/70 bg-background px-4 py-3"> <p className="text-xs uppercase tracking-[0.18em] text-muted-foreground"> Monthly total </p> <p className="mt-2 text-3xl font-semibold tracking-tight"> {priceLabel} </p> </div> <div className="space-y-3 rounded-lg border border-border/70 bg-muted/20 p-4"> <DetailRow label="Renewal" value={renewalLabel} /> {seatsLabel ? <DetailRow label="Seats" value={seatsLabel} /> : null} {usageLabel ? <DetailRow label="Usage" value={usageLabel} /> : null} </div> {note ? ( <p className="rounded-lg bg-muted px-4 py-3 text-sm text-muted-foreground"> {note} </p> ) : null} </CardContent> ); } function SubscriptionActions({ primaryActionLabel, secondaryActionLabel, }: SubscriptionActionsProps) { if (!primaryActionLabel && !secondaryActionLabel) { return null; } return ( <CardFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end"> {secondaryActionLabel ? ( <Button className="w-full sm:w-auto" variant="outline"> {secondaryActionLabel} </Button> ) : null} {primaryActionLabel ? ( <Button className="w-full sm:w-auto">{primaryActionLabel}</Button> ) : null} </CardFooter> ); } export const SubscriptionCard = ({ className, note, plan, priceLabel, primaryActionLabel, ref: reference, renewalLabel, seatsLabel, secondaryActionLabel, status, usageLabel, ...props }: SubscriptionCardProps & { ref?: React.Ref<React.ComponentRef<typeof Card>>; }) => { return ( <Card className={cn( "w-full max-w-md border-border/70 bg-card shadow-sm", className, )} ref={reference} {...props} > <CardHeader className="space-y-4 pb-4"> <div className="flex items-start justify-between gap-3"> <div className="space-y-1"> <CardTitle className="text-lg">Subscription</CardTitle> <CardDescription> Billing overview for the current workspace plan. </CardDescription> </div> <span className={cn( "inline-flex rounded-full px-2.5 py-1 text-xs font-medium", getStatusClasses(status), )} > {getStatusLabel(status)} </span> </div> <div className="flex items-center justify-between gap-3 rounded-lg border border-border/70 bg-muted/30 px-4 py-3"> <div> <p className="text-sm font-medium">Current plan</p> <p className="text-xs text-muted-foreground">{renewalLabel}</p> </div> <PlanBadge state={getPlanState(status)} tier={plan} /> </div> </CardHeader> <SubscriptionDetails note={note} priceLabel={priceLabel} renewalLabel={renewalLabel} seatsLabel={seatsLabel} usageLabel={usageLabel} /> <SubscriptionActions primaryActionLabel={primaryActionLabel} secondaryActionLabel={secondaryActionLabel} /> </Card> ); }; SubscriptionCard.displayName = "SubscriptionCard";

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0