Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Wallet dashboard

Ein Wallet-Dashboard in Tiles: Kontostand, ein Fächer aus drei Zahlungskarten, Cashflow-Balken, ein Ausgaben-Donut und Abos.

Vorschau

React

Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.json
npx shadcn@latest add @manniche/wallet-dashboard
wallet-dashboard.tsx
// A wallet screen in Tiles: balance, a fanned stack of payment cards, cash flow, a spending donut and subscriptions.
// Compose-only: every chart is a Manniche primitive; the screen owns layout and copy.
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { BigNumber, DeltaPill, Pills } from '@/registry/manniche/chart-kit/chart-kit'
import { formatValue, type ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import { CardStack, type StackCard } from '@/registry/manniche/card-stack/card-stack'
import { Donut, type DonutDatum } from '@/registry/manniche/donut/donut'

export type WalletSubscription = { id: string; name: string; amount: number; renews: string }
export type WalletPeriod = { id: string; label: string; long: string; flow: BarPoint[]; spending: DonutDatum[] }

export type WalletDashboardProps = {
  data: {
    balance: number
    change: number
    cards: StackCard[]
    periods: WalletPeriod[]
    subscriptions: WalletSubscription[]
  }
  format?: ValueFormat
  title?: string
  note?: string
  labels?: {
    balance?: string
    cards?: string
    cashFlow?: string
    spending?: string
    subscriptions?: string
    period?: string
    perMonth?: string
    renews?: (date: string) => string
    monthlyTotal?: string
  }
  /** Reserved for date-aware content; the screen itself is static. */
  now?: Date
  className?: string
}

const DEFAULT_FORMAT: ValueFormat = { currency: 'EUR', decimals: 2 }

/** Wallet overview: balance, card stack, cash flow, spending by category and recurring payments. */
export function WalletDashboard({ data, format = DEFAULT_FORMAT, title = 'Wallet', note = 'Example data.', labels, className }: WalletDashboardProps) {
  const l = {
    balance: 'Total balance',
    cards: 'Cards',
    cashFlow: 'Cash flow',
    spending: 'Spending',
    subscriptions: 'Subscriptions',
    period: 'Period',
    perMonth: 'a month',
    renews: (d: string) => `Renews ${d}`,
    monthlyTotal: 'Monthly total',
    ...labels,
  }
  const [periodId, setPeriodId] = useState(data.periods[0]?.id ?? '')
  const period = data.periods.find((p) => p.id === periodId) ?? data.periods[0]
  const monthly = data.subscriptions.reduce((s, x) => s + x.amount, 0)
  return (
    <div className={cn('@container w-full font-sans text-foreground', className)}>
      <div className="grid gap-4 @3xl:grid-cols-6 @6xl:grid-cols-12">
        <div className="@3xl:col-span-6 @6xl:col-span-12">
          <h1 className="text-[22px] leading-tight font-semibold tracking-tight">{title}</h1>
        </div>
        <DataTile title={l.balance} className="@3xl:col-span-3 @6xl:col-span-4" footer={<span>{note}</span>}>
          <BigNumber value={data.balance} format={format} size="lg" />
          <div className="mt-3">
            <DeltaPill value={data.change} format={{ decimals: 1, suffix: '%', sign: true }} />
          </div>
        </DataTile>
        <DataTile title={l.cards} className="@3xl:col-span-3 @6xl:col-span-4">
          <CardStack data={data.cards} label={l.cards} format={format} />
        </DataTile>
        <DataTile
          title={l.cashFlow}
          className="@3xl:col-span-6 @6xl:col-span-4"
          action={<Pills label={l.period} options={data.periods.map(({ id, label }) => ({ id, label }))} value={periodId} onChange={setPeriodId} />}
        >
          {period && <BarChart data={period.flow} label={`${l.cashFlow}, ${period.long}`} format={{ ...format, decimals: 0 }} />}
        </DataTile>
        <DataTile title={l.spending} className="@3xl:col-span-3 @6xl:col-span-6">
          {period && <Donut data={period.spending} label={`${l.spending}, ${period.long}`} format={format} />}
        </DataTile>
        <DataTile
          title={l.subscriptions}
          className="@3xl:col-span-3 @6xl:col-span-6"
          footer={
            <span>
              {l.monthlyTotal}: <strong className="tabular-nums">{formatValue(monthly, { ...format, decimals: 2 })}</strong>
            </span>
          }
        >
          {/* The tile's heading names the list; a hidden <li> as its label would be counted as an extra item. */}
          <ul aria-label={l.subscriptions} className="divide-y divide-border">
            {data.subscriptions.map((s) => (
              <li key={s.id} className="flex items-center gap-3 py-3 text-sm">
                <span aria-hidden className="grid size-9 shrink-0 place-items-center rounded-full bg-muted text-xs font-semibold text-muted-foreground">
                  {s.name.slice(0, 2).toUpperCase()}
                </span>
                <span className="min-w-0 flex-1">
                  <span className="block truncate font-medium">{s.name}</span>
                  <span className="block text-xs text-muted-foreground">{l.renews(s.renews)}</span>
                </span>
                <span className="font-medium tabular-nums">
                  {formatValue(s.amount, { ...format, decimals: 2 })}
                  <span className="sr-only"> {l.perMonth}</span>
                </span>
              </li>
            ))}
          </ul>
        </DataTile>
      </div>
    </div>
  )
}

export default WalletDashboard
wallet-dashboard-demo.tsx
import { WalletDashboard } from '@/components/wallet-dashboard'

// Example data for the demo, not real accounts.
const data = {
  balance: 8318.06,
  change: 3.2,
  cards: [
    { id: 'everyday', name: 'Everyday', kind: 'Debit', ending: '4821', amount: 2091.2 },
    { id: 'travel', name: 'Travel', kind: 'Multi-currency', ending: '0937', amount: 612.08 },
    { id: 'savings', name: 'Savings', kind: 'Pot', ending: '1162', amount: 5614.78 },
  ],
  periods: [
    {
      id: 'week',
      label: 'Week',
      long: 'last 7 days',
      flow: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((label, i) => ({ label, value: [120, 340, -80, 210, 560, -150, 90][i] })),
      spending: [
        { label: 'Groceries', value: 90.84 },
        { label: 'Transport', value: 47.1 },
        { label: 'Eating out', value: 74.02 },
        { label: 'Bills', value: 84.12 },
      ],
    },
    {
      id: 'month',
      label: 'Month',
      long: 'last 30 days',
      flow: ['Wk 1', 'Wk 2', 'Wk 3', 'Wk 4'].map((label, i) => ({ label, value: [820, 640, 1180, 910][i] })),
      spending: [
        { label: 'Groceries', value: 414.96 },
        { label: 'Transport', value: 179.82 },
        { label: 'Eating out', value: 235.14 },
        { label: 'Shopping', value: 152.15 },
        { label: 'Bills', value: 401.13 },
      ],
    },
  ],
  subscriptions: [
    { id: 'music', name: 'Northwind Music', amount: 10.99, renews: '12 Oct' },
    { id: 'cloud', name: 'Harbour Cloud', amount: 2.99, renews: '15 Oct' },
    { id: 'news', name: 'Green Leaf Daily', amount: 7.5, renews: '21 Oct' },
  ],
}

export default function WalletDashboardDemo() {
  return <WalletDashboard data={data} />
}

HTML

Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.

In neuem Tab öffnen

Die Farbe in deiner eigenen App

Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel in .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Installieren

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.json

Abhängigkeiten

    Dateien

    • wallet-dashboard.tsx
    • wallet-dashboard.html

    Manniche Lab · Open Source

    Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.