Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

2.5 kB Ruw bestand
import type { ReactNode } from 'react';
import { PLANS } from '@wellformed/shared';
import { usePublicConfig } from '../api/hooks';
import { useI18n, type TranslateFn } from '../i18n';
import { useSession } from '../state/session';
import { daysLeft } from './BillingDialog';

/**
 * A slim line under the top bar, shown only when the plan is about to change
 * what someone can do.
 *
 * Deliberately silent the rest of the time. A permanent "you are on Free"
 * ribbon is an advert, and an advert that never goes away is the first thing
 * people learn to stop reading — including on the day it says something that
 * matters.
 *
 * Silent for good on an unmetered install. Nothing there starts a trial, so
 * in practice there would be nothing to say anyway — but an install converted
 * with one running would otherwise keep counting down to a limit that has
 * been switched off, which is worse than a wrong advert.
 */
export function PlanBanner({ onOpen }: { onOpen(): void }): ReactNode {
  const session = useSession();
  const install = usePublicConfig();
  const { t } = useI18n();
  const workspace = session.workspace;
  if (!workspace || session.visiting || !session.can('admin')) return null;
  if (install.data && !install.data.metered) return null;

  const notice = describe(workspace.billingStatus, workspace.trialEndsAt, t);
  if (!notice) return null;

  return (
    <div className={`planbar ${notice.tone}`} role="status">
      <span>{notice.message}</span>
      <button className="linkbtn" onClick={onOpen}>
        {notice.action}
      </button>
    </div>
  );
}

interface Notice {
  tone: 'warn' | 'bad';
  message: string;
  action: string;
}

/** Trials only speak up in their last week; a payment problem speaks up at once. */
function describe(status: string, trialEndsAt: string | null, t: TranslateFn): Notice | null {
  if (status === 'past_due') {
    return {
      tone: 'bad',
      message: t('billing.pastDueBanner'),
      action: t('billing.fixPayment'),
    };
  }

  if (status === 'trialing' && trialEndsAt) {
    const remaining = new Date(trialEndsAt).getTime() - Date.now();
    if (remaining <= 0) {
      return {
        tone: 'warn',
        message: t('billing.trialEndedBanner'),
        action: t('billing.seePlans'),
      };
    }
    if (remaining <= 7 * 86_400_000) {
      return {
        tone: 'warn',
        message: t('billing.trialBanner', PLANS.team.name, daysLeft(trialEndsAt, t)),
        action: t('billing.seePlans'),
      };
    }
  }

  return null;
}