// BalanceOnboardingCard — empty-state onboarding for /balance. // // Issue #178. Replaces the BalanceOverviewCard when the user has no accounts // or no snapshots yet. Two vertical steps: // 1. Create an account → /balance/accounts // 2. Enter a snapshot → /balance/snapshot // // Each step has 3 states: // - "active": primary CTA, currently the next thing to do // - "done": marked with a checkmark, no CTA // - "disabled": grayed out (e.g. step 2 when 0 accounts), CTA disabled // // The whole card is replaced by BalanceOverviewCard once at least one // snapshot exists, so step 2 in practice is rendered as "active" or // "disabled"; the "done" branch is supported for completeness/tests. import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import type { TFunction } from "i18next"; import { Wallet, FileText, Check, ArrowRight } from "lucide-react"; interface BalanceOnboardingCardProps { /** Number of active (non-archived) accounts. */ accountsCount: number; /** Number of snapshots saved (any date). */ snapshotsCount: number; } export type StepState = "active" | "done" | "disabled"; /** * Pure helper exposed for unit tests — derives the state of each onboarding * step from the (accountsCount, snapshotsCount) pair. * * - Step 1 is "done" once at least one account exists, "active" otherwise. * - Step 2 is "done" once any snapshot exists, "active" once at least one * account exists, "disabled" otherwise. In practice the parent guard on * /balance only renders this card when snapshotsCount === 0, so the * "done" branch for step 2 is mostly defensive. */ export function deriveOnboardingSteps( accountsCount: number, snapshotsCount: number ): { step1: StepState; step2: StepState } { const step1: StepState = accountsCount >= 1 ? "done" : "active"; const step2: StepState = snapshotsCount >= 1 ? "done" : accountsCount >= 1 ? "active" : "disabled"; return { step1, step2 }; } export default function BalanceOnboardingCard({ accountsCount, snapshotsCount, }: BalanceOnboardingCardProps) { const { t } = useTranslation(); const { step1: step1State, step2: step2State } = deriveOnboardingSteps( accountsCount, snapshotsCount ); return (

{t("balance.onboarding.title")}

{t("balance.onboarding.subtitle")}

    } title={t("balance.onboarding.step1.title")} description={t("balance.onboarding.step1.description")} ctaLabel={t("balance.onboarding.step1.cta")} ctaHref="/balance/accounts" t={t} /> } title={t("balance.onboarding.step2.title")} description={t("balance.onboarding.step2.description")} ctaLabel={t("balance.onboarding.step2.cta")} ctaHref="/balance/snapshot" disabledHint={t("balance.onboarding.step2.disabledHint")} t={t} />
); } // ----------------------------------------------------------------------------- // Internal — single step row // ----------------------------------------------------------------------------- interface StepProps { number: number; state: StepState; icon: React.ReactNode; title: string; description: string; ctaLabel: string; ctaHref: string; disabledHint?: string; t: TFunction; } function Step({ number, state, icon, title, description, ctaLabel, ctaHref, disabledHint, t, }: StepProps) { const isDone = state === "done"; const isActive = state === "active"; const isDisabled = state === "disabled"; // Number bubble: green check when done, primary bg when active, muted when disabled. const bubbleClass = isDone ? "bg-[var(--positive)] text-white" : isActive ? "bg-[var(--primary)] text-white" : "bg-[var(--muted)] text-[var(--muted-foreground)]"; const titleClass = isDisabled ? "text-[var(--muted-foreground)]" : "text-[var(--foreground)]"; return (
  • {title}

    {description}

    {isDisabled && disabledHint && (

    {disabledHint}

    )}
    {isDone ? ( {t("balance.onboarding.doneBadge")} ) : isActive ? ( {ctaLabel} ) : ( )}
  • ); }