Skabelon · React · HTML
Sales floor
Et dashboard til et salgsteam i Tiles: en målskive i segmenter, et mørkt kort med det igangværende opkald, søjler pr. sælger og en omsætningsdonut.
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/sales-floor.jsonnpx shadcn@latest add @manniche/sales-floorsales-floor.tsx
// A sales-floor screen: a segmented arc dial for the target, a live call card, performance bars and revenue by source.
import { Phone } from 'lucide-react'
import { cn } from '@/lib/utils'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import type { ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'
import { Dial, type DialZone } from '@/registry/manniche/dial/dial'
import { Donut, type DonutDatum } from '@/registry/manniche/donut/donut'
export type SalesFloorData = {
target: { title: string; value: number; max: number; zones: DialZone[]; caption: string; format?: ValueFormat }
call: { title: string; contact: string; company: string; status: string; duration: string; action: string }
performance: { title: string; points: BarPoint[]; format?: ValueFormat; /** The bar to mark as current, if the bars are periods. Default none. */ current?: number | null }
sources: { title: string; items: DonutDatum[]; format?: ValueFormat }
}
export type SalesFloorLabels = { note?: string }
export type SalesFloorProps = { data: SalesFloorData; labels?: SalesFloorLabels; onEndCall?: () => void; className?: string }
export function SalesFloor({ data, labels, onEndCall, className }: SalesFloorProps) {
const note = labels?.note ?? 'Example data.'
return (
<div className={cn('@container w-full rounded-2xl bg-background p-3 text-foreground', className)}>
<div className="grid grid-cols-1 gap-3 @2xl:grid-cols-6">
<div className="@2xl:col-span-3">
<DataTile className="h-full" title={data.call.title} inverted>
<div className="flex flex-wrap items-center gap-3">
<span aria-hidden className="grid size-11 place-items-center rounded-full bg-muted">
<Phone className="size-5" />
</span>
<div className="min-w-0">
<p className="truncate font-medium">{data.call.contact}</p>
<p className="truncate text-sm opacity-70">{data.call.company}</p>
</div>
<span role="status" className="ml-auto text-sm whitespace-nowrap tabular-nums">
{data.call.status} · {data.call.duration}
</span>
</div>
<button
type="button"
onClick={onEndCall}
className="mt-4 rounded-full bg-foreground px-4 py-2 text-sm font-medium text-background transition-transform active:scale-[0.97] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
{data.call.action}
</button>
</DataTile>
</div>
<div className="@2xl:col-span-3">
<DataTile className="h-full" title={data.target.title} footer={note}>
<Dial
value={data.target.value}
max={data.target.max}
zones={data.target.zones}
label={data.target.title}
caption={data.target.caption}
format={data.target.format}
segments={24}
/>
</DataTile>
</div>
<div className="@2xl:col-span-4">
<DataTile className="h-full" title={data.performance.title} footer={note}>
<BarChart data={data.performance.points} label={data.performance.title} format={data.performance.format} current={data.performance.current ?? null} />
</DataTile>
</div>
<div className="@2xl:col-span-2">
<DataTile className="h-full" title={data.sources.title} footer={note}>
<Donut data={data.sources.items} label={data.sources.title} format={data.sources.format} />
</DataTile>
</div>
</div>
</div>
)
}sales-floor-demo.tsx
import { SalesFloor, type SalesFloorData } from '@/components/sales-floor'
// Example data only. All names and figures are invented.
const money = { currency: 'EUR', decimals: 0 }
const data: SalesFloorData = {
call: { title: 'Live call', contact: 'Alex Example', company: 'Sample Co', status: 'Connected', duration: '04:12', action: 'End call' },
target: {
title: 'Monthly target',
value: 62000,
max: 100000,
format: money,
caption: 'of an example target',
zones: [
{ label: 'Behind', to: 40000 },
{ label: 'On track', to: 80000 },
{ label: 'Ahead', to: 100000 },
],
},
performance: {
title: 'Calls booked per rep',
points: ['Ana', 'Ben', 'Cy', 'Di', 'Eli'].map((label, i) => ({ label, title: `${label} (example)`, value: [34, 28, 41, 22, 30][i] })),
},
sources: {
title: 'Revenue by source',
format: money,
items: [
{ label: 'Outbound', value: 28000 },
{ label: 'Referral', value: 21000 },
{ label: 'Inbound', value: 13000 },
],
},
}
export default function SalesFloorDemo() {
return <SalesFloor 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.
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/sales-floor.json