Skip to content
BigBang

Popular

    ↑↓Navigate↵OpenEscClose
    TR

    Templates

    Dashboard

    A date range, four KPI cards, a trend chart and a recent records table.

    The dashboard block builds a summary page inside the AppShell template. When the date range changes, the figures swap; the chart and table do not enter again.

    Change the date range. The brand and mode switches in the page header restyle the preview too. The second preview is the first-load state.

    Live example, StorybookOpen in Storybook
    Live example, StorybookOpen in Storybook
    Live example, StorybookOpen in Storybook
    Terminal window
    pnpm add @bigbang/ui
    Data Type Note
    KPIs { active, open, done, cycle, delta } The app formats them with Intl; StatCard never formats numbers.
    Chart series ChartSeries[] At most 8 series; colours from the chart.categorical-* tokens.
    Recent records Row[] DataTable shows loading, empty and error states through status.
    loading boolean Shows every region in its first-load state.
    import { Button, Chart, createDataTableColumnHelper, DataTable, Grid, Icon, SegmentedControl, StatCard, StatusBadge } from '@bigbang/ui';
    import type { ChartSeries, StatusKey } from '@bigbang/ui';
    import { useMemo, useState } from 'react';
    import type { Lang } from '../../patterns/lang';
    import { AppShellTemplate } from '../../templates/app-shell/AppShellTemplate';
    const copy = {
    tr: {
    title: 'Genel bakış',
    description: 'Ekibinizin projeleri ve görevleri.',
    range: 'Tarih aralığı',
    ranges: { '7': '7 gün', '30': '30 gün', '90': '90 gün' },
    report: 'Rapor oluştur',
    active: 'Aktif proje',
    open: 'Açık görev',
    done: 'Tamamlanan görev',
    cycle: 'Ortalama süre',
    days: (value: string) => `${value} gün`,
    period: 'önceki döneme göre',
    chart: 'Açılan ve kapanan görevler',
    chartDescription: 'Haftalık görev sayısı',
    opened: 'Açılan',
    closed: 'Kapanan',
    week: (index: number) => `${index}. hafta`,
    recent: 'Son kayıtlar',
    name: 'Kayıt',
    status: 'Durum',
    owner: 'Ekip',
    updated: 'Güncellendi',
    record: (index: number) => `Kayıt ${index}`,
    team: (index: number) => `Ekip ${index}`,
    },
    en: {
    title: 'Overview',
    description: "Your team's projects and tasks.",
    range: 'Date range',
    ranges: { '7': '7 days', '30': '30 days', '90': '90 days' },
    report: 'Create report',
    active: 'Active projects',
    open: 'Open tasks',
    done: 'Completed tasks',
    cycle: 'Average cycle time',
    days: (value: string) => `${value} days`,
    period: 'against the previous period',
    chart: 'Opened and closed tasks',
    chartDescription: 'Tasks per week',
    opened: 'Opened',
    closed: 'Closed',
    week: (index: number) => `Week ${index}`,
    recent: 'Recent records',
    name: 'Record',
    status: 'Status',
    owner: 'Team',
    updated: 'Updated',
    record: (index: number) => `Record ${index}`,
    team: (index: number) => `Team ${index}`,
    },
    };
    type Range = '7' | '30' | '90';
    // Round demo figures per range; an app passes its own query results.
    const figures: Record<Range, { active: number; open: number; done: number; cycle: number; delta: number }> = {
    '7': { active: 24, open: 120, done: 300, cycle: 3.5, delta: 8 },
    '30': { active: 26, open: 140, done: 1200, cycle: 4, delta: 12 },
    '90': { active: 30, open: 180, done: 3600, cycle: 4.5, delta: 5 },
    };
    const opened = [40, 45, 50, 48, 60, 55, 70, 65, 80, 75, 90, 85];
    const closed = [30, 40, 45, 50, 50, 55, 60, 65, 70, 75, 80, 85];
    const statuses: StatusKey[] = ['completed', 'in-review', 'active', 'pending', 'completed'];
    type Row = { id: string; name: string; status: StatusKey; owner: string; updated: string };
    const helper = createDataTableColumnHelper<Row>();
    export type DashboardShellBlockProps = {
    lang?: Lang;
    /** Shows the loading state of every region at once, as on first load. */
    loading?: boolean;
    defaultRange?: Range;
    };
    /** A dashboard page: range control, four KPIs, a trend chart and the latest records. */
    export function DashboardShellBlock({ lang = 'tr', loading = false, defaultRange = '30' }: DashboardShellBlockProps) {
    const t = copy[lang];
    const [range, setRange] = useState<Range>(defaultRange);
    const number = new Intl.NumberFormat(lang === 'tr' ? 'tr-TR' : 'en-GB');
    const f = figures[range];
    // Turkish puts the percent sign first.
    const percent = (value: number) => (lang === 'tr' ? `%${value}` : `${value}%`);
    const series = useMemo<ChartSeries[]>(
    () => [
    { id: 'opened', label: t.opened, data: opened.map((y, index) => ({ x: t.week(index + 1), y })) },
    { id: 'closed', label: t.closed, data: closed.map((y, index) => ({ x: t.week(index + 1), y })) },
    ],
    [t],
    );
    const rows = useMemo<Row[]>(() => {
    const date = new Intl.DateTimeFormat(lang === 'tr' ? 'tr-TR' : 'en-GB', { day: 'numeric', month: 'short' });
    return statuses.map((status, index) => ({ id: `r${index + 1}`, name: t.record(index + 1), status, owner: t.team((index % 3) + 1), updated: date.format(new Date(2026, 9, 28 - index)) }));
    }, [t, lang]);
    const columns = useMemo(
    () => [
    helper.accessor('name', { header: t.name, meta: { rowHeader: true } }),
    helper.accessor('status', { header: t.status, enableSorting: false, cell: (context) => <StatusBadge status={context.getValue()} size="sm" /> }),
    helper.accessor('owner', { header: t.owner }),
    helper.accessor('updated', { header: t.updated }),
    ],
    [t],
    );
    return (
    <AppShellTemplate
    lang={lang}
    current="home"
    title={t.title}
    description={t.description}
    loading={loading}
    actions={
    <>
    <SegmentedControl label={t.range} size="sm" value={range} onValueChange={(value) => setRange(value as Range)} items={(['7', '30', '90'] as const).map((value) => ({ value, label: t.ranges[value] }))} />
    <Button size="sm" startIcon={<Icon name="file" />}>
    {t.report}
    </Button>
    </>
    }
    >
    {/* Figures change by remount (110 ms cross-fade); amounts never count up. */}
    <Grid columns={{ base: 2, md: 4 }} gap="lg">
    <StatCard key={`active-${range}`} label={t.active} value={number.format(f.active)} loading={loading} />
    <StatCard key={`open-${range}`} label={t.open} value={number.format(f.open)} loading={loading} />
    <StatCard key={`done-${range}`} label={t.done} value={number.format(f.done)} delta={{ value: percent(f.delta), direction: 'up' }} period={t.period} loading={loading} />
    <StatCard
    key={`cycle-${range}`}
    label={t.cycle}
    value={t.days(number.format(f.cycle))}
    delta={{ value: percent(5), direction: 'down', sentiment: 'positive' }}
    period={t.period}
    loading={loading}
    />
    </Grid>
    <Chart kind="area" title={t.chart} description={t.chartDescription} series={series} height="md" loading={loading} formatValue={(value) => number.format(value)} />
    <DataTable caption={t.recent} columns={columns} data={rows} getRowId={(row) => row.id} status={loading ? 'loading' : 'ready'} density="compact" />
    </AppShellTemplate>
    );
    }
    1. AppShell template: navigation and top bar.
    2. PageHeader: h1 “Overview”, a SegmentedControl date range and “Create report”.
    3. Four StatCards: two columns below 1024 px, four above.
    4. A Chart area chart: opened and closed tasks over 12 weeks.
    5. A DataTable “Recent records” with status as a StatusBadge.
    • The page has one h1; the chart and the table are named by their own titles and caption.
    • The date range reads as a radiogroup; one tab stop, arrow keys choose.
    • The chart reads as role="img" with a summary text, and offers a “Show data as a table” fallback.
    • While loading, regions carry aria-busy; the page header keeps its place.
    • A figure change is a 110 ms cross-fade; amounts never count up.

    StatCard, Chart, DataTable, SegmentedControl, StatusBadge, Grid, PageHeader. Patterns: Loading, Charts gallery.