← Files WixARCHIVED FILE

skills/wix-headless-templates/pricing-plans/project/src/components/pricing-plans/PlansView.tsx

3.98 KB · Oct 8, 2026 · 12:02 UTC

↓ Download file

See the change to this file →

// REFERENCE pricing surface: plan cards in a grid on the @theme tokens. Correct and
// complete; per the skill's model you design and build your own on usePlans, with the shipped
// SubscribeButton as each card's purchase control.
import type { ComponentType, ReactNode } from "react";
import { usePlans } from "../../hooks/pricing-plans/usePlans";
import SubscribeButton from "./SubscribeButton";
import type { PlanSummary } from "../../wix/pricing-plans/types";

export interface LinkLikeProps {
  href: string;
  className?: string;
  children?: ReactNode;
}

const PlainLink = ({ href, className, children }: LinkLikeProps) => (
  <a href={href} className={className}>
    {children}
  </a>
);

export interface PlanCardProps {
  plan: PlanSummary;
  planHref?: (slug: string) => string;
  LinkComponent?: ComponentType<LinkLikeProps>;
}

export function PlanCard({ plan, planHref = (slug) => `/plans/${slug}`, LinkComponent = PlainLink }: PlanCardProps) {
  return (
    <div className="flex h-full flex-col rounded-lg border border-border bg-background p-6">
      <p className="text-base font-semibold text-foreground">{plan.name}</p>
      {plan.description && <p className="mt-1 text-sm text-muted-foreground">{plan.description}</p>}
      <p className="mt-4">
        <span className="text-3xl font-bold tracking-tight text-foreground">{plan.price}</span>
        {plan.billing && <span className="ml-1.5 text-sm text-muted-foreground">{plan.billing}</span>}
        {plan.duration && <span className="text-sm text-muted-foreground"> · valid {plan.duration}</span>}
      </p>
      {/* Fees are part of the price — under it, before the CTA, never hidden. */}
      {plan.fees.map((fee) => (
        <p key={fee.name + fee.amount} className="mt-1 text-sm text-muted-foreground">
          + {fee.amount} {fee.name}
        </p>
      ))}
      {plan.freeTrialDays !== null && (
        <p className="mt-1 text-xs font-medium text-muted-foreground">{plan.freeTrialDays}-day free trial</p>
      )}
      {plan.perks.length > 0 && (
        <ul className="mt-4 space-y-2">
          {plan.perks.map((perk) => (
            <li key={perk} className="flex gap-2 text-sm text-foreground">
              <span className="text-muted-foreground" aria-hidden="true">
                ✓
              </span>
              <span>{perk}</span>
            </li>
          ))}
        </ul>
      )}
      <div className="mt-auto grid gap-2 pt-6">
        <SubscribeButton plan={plan} assignedText={null} />
        <LinkComponent
          href={planHref(plan.slug)}
          className="rounded-control border border-border px-6 py-2.5 text-center text-sm font-semibold text-foreground no-underline transition-colors hover:bg-secondary"
        >
          View details
        </LinkComponent>
      </div>
    </div>
  );
}

export interface PlansViewProps {
  initialPlans?: PlanSummary[];
  emptyMessage?: string;
  planHref?: PlanCardProps["planHref"];
  LinkComponent?: ComponentType<LinkLikeProps>;
  CardComponent?: ComponentType<PlanCardProps>;
}

export default function PlansView({
  initialPlans,
  emptyMessage = "No plans yet — check back soon.",
  planHref,
  LinkComponent,
  CardComponent = PlanCard,
}: PlansViewProps) {
  const { plans, error } = usePlans({ initialPlans });

  return (
    <div>
      {error && <p className="mb-4 text-sm text-destructive">{error}</p>}
      {plans === null ? (
        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
          {Array.from({ length: 3 }, (_, i) => (
            <div key={i} className="h-80 animate-pulse rounded-lg bg-secondary" />
          ))}
        </div>
      ) : plans.length === 0 ? (
        <p className="py-16 text-center text-muted-foreground">{emptyMessage}</p>
      ) : (
        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {plans.map((p) => (
            <CardComponent key={p.id} plan={p} planHref={planHref} LinkComponent={LinkComponent} />
          ))}
        </div>
      )}
    </div>
  );
}

SHA-256: 74373c18c3d7c14610fe202c6c96b27c4cd1e0109ef94365e5bf7d112eea0280