Mikkel Manniche
Dansk English Deutsch

Skabelon · React · HTML

Wallet dashboard

Et tegnebogsdashboard i Tiles: saldo, en vifte af tre betalingskort, søjler for pengestrøm, en forbrugsdonut og abonnementer.

Forhåndsvisning

React

Installér skabelonen med shadcn. Den lægges i dit projekt som almindelige komponenter, som du selv retter i.

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

Hele siden som én fil med al CSS indbygget og intet JavaScript. Knapper, faner og filtre virker med HTML og CSS alene.

Åbn i ny fane

Farven i din egen app

Har du allerede shadcn sat op, giver disse variabler samme farve. Lys i :root, mørk i .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);
}

Installér

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

Afhængigheder

    Filer

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

    Manniche Lab · open source

    Gratis værktøjer til AI-agenter og webudvikling.