
A store admin overview with sales stats, an online versus in-store bar chart, and a filterable orders list.
@esuiss
bases/radix/components/blocks/dash-store.tsx
"use client"
import * as React from "react"
import {
Bar,
BarChart,
CartesianGrid,
Cell,
useActiveTooltipDataPoints,
XAxis,
} from "recharts"
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
} from "@/components/ui/chart"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
type NavItem = {
value: string
label: string
icon: React.ReactNode
badge?: React.ReactNode
group?: string
}
function NavSidebar({
items,
value,
onValueChange,
header,
footer,
}: {
items: NavItem[]
value: string
onValueChange: (value: string) => void
header?: React.ReactNode
footer?: React.ReactNode
}) {
const { isMobile, setOpenMobile } = useSidebar()
const groups = [...new Set(items.map((item) => item.group ?? ""))]
return (
<Sidebar collapsible="icon">
{header && (
<SidebarHeader className="group-data-[collapsible=icon]:hidden">
{header}
</SidebarHeader>
)}
<SidebarContent>
{groups.map((group) => (
<SidebarGroup key={group}>
{group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
<SidebarGroupContent>
<SidebarMenu>
{items
.filter((item) => (item.group ?? "") === group)
.map((item) => (
<SidebarMenuItem key={item.value}>
<SidebarMenuButton
isActive={item.value === value}
tooltip={item.label}
aria-current={item.value === value ? "page" : undefined}
onClick={() => {
onValueChange(item.value)
if (isMobile) setOpenMobile(false)
}}
>
{item.icon}
<span>{item.label}</span>
</SidebarMenuButton>
{item.badge !== undefined && (
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
{footer && (
<SidebarFooter className="group-data-[collapsible=icon]:hidden">
{footer}
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
)
}
const items: NavItem[] = [
{
value: "overview",
label: "Overview",
icon: (
<IconPlaceholder
lucide="HouseIcon"
tabler="IconHome"
hugeicons="Home01Icon"
phosphor="HouseIcon"
remixicon="RiHomeLine"
/>
),
},
{
value: "orders",
label: "Orders",
icon: (
<IconPlaceholder
lucide="HandbagIcon"
tabler="IconShoppingBag"
hugeicons="ShoppingBag01Icon"
phosphor="HandbagIcon"
remixicon="RiHandbagLine"
/>
),
badge: 12,
},
{
value: "customers",
label: "Customers",
icon: (
<IconPlaceholder
lucide="UsersIcon"
tabler="IconUsers"
hugeicons="UserGroupIcon"
phosphor="UsersIcon"
remixicon="RiGroupLine"
/>
),
},
{
value: "reports",
label: "Reports",
icon: (
<IconPlaceholder
lucide="ChartColumnIcon"
tabler="IconChartBar"
hugeicons="ChartColumnIcon"
phosphor="ChartBarIcon"
remixicon="RiBarChartLine"
/>
),
},
...["Union Square", "Fifth Avenue", "Michigan Avenue"].map((store) => ({
value: store,
label: store,
icon: (
<IconPlaceholder
lucide="StoreIcon"
tabler="IconBuildingStore"
hugeicons="Store01Icon"
phosphor="StorefrontIcon"
remixicon="RiStore2Line"
/>
),
group: "Stores",
})),
]
const ranges = [
{ value: "24h", label: "24 Hours", scale: 0.15 },
{ value: "7d", label: "7 Days", scale: 1 },
{ value: "30d", label: "30 Days", scale: 4.3 },
]
const week = [
{ day: "Mon", online: 42, store: 46 },
{ day: "Tue", online: 58, store: 44 },
{ day: "Wed", online: 50, store: 56 },
{ day: "Thu", online: 68, store: 54 },
{ day: "Fri", online: 64, store: 70 },
{ day: "Sat", online: 80, store: 62 },
{ day: "Sun", online: 72, store: 76 },
]
const chartConfig = {
online: { label: "Online", color: "var(--chart-1)" },
store: { label: "In store", color: "var(--chart-2)" },
} satisfies ChartConfig
const orders = [
{
id: "W1048",
name: "Jamie Rivera",
product: "Phone Pro",
status: "Shipped",
total: 999,
hue: 250,
},
{
id: "W1047",
name: "Morgan Lee",
product: "Book Air",
status: "Processing",
total: 1099,
hue: 30,
},
{
id: "W1046",
name: "Riley Chen",
product: "Watch Series 11",
status: "Delivered",
total: 399,
hue: 150,
},
{
id: "W1045",
name: "Avery Brooks",
product: "Pods Studio",
status: "Returned",
total: 549,
hue: 330,
},
{
id: "W1044",
name: "Taylor Kim",
product: "Pad Air",
status: "Delivered",
total: 599,
hue: 200,
},
]
const filters = ["All", "Shipped", "Delivered", "Returned"]
const money = (value: number) =>
value.toLocaleString("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
})
const initials = (name: string) =>
name
.split(" ")
.map((part) => part[0])
.join("")
function ActiveDay({ onChange }: { onChange: (index: number | null) => void }) {
const points = useActiveTooltipDataPoints<{ index?: number }>()
const index = points?.[0]?.index ?? null
React.useEffect(() => onChange(index), [index, onChange])
return null
}
export function DashStore() {
const [section, setSection] = React.useState("overview")
const [range, setRange] = React.useState("7d")
const [filter, setFilter] = React.useState("All")
const [active, setActive] = React.useState<number | null>(null)
const scale = ranges.find((item) => item.value === range)?.scale ?? 1
const data = week.map((day, index) => ({ ...day, index }))
const activeDay = active === null ? undefined : week[active]
const title = items.find((item) => item.value === section)?.label
const stats = [
{
label: "Revenue",
value: money(1284500 * scale),
change: "+12.4%",
up: true,
},
{
label: "Orders",
value: Math.round(842 * scale).toLocaleString("en-US"),
change: "+8.1%",
up: true,
},
{ label: "Average order", value: money(1525), change: "−2.3%", up: false },
{ label: "Conversion", value: "3.42%", change: "+0.4 pts", up: true },
]
return (
<SidebarProvider>
<NavSidebar
items={items}
value={section}
onValueChange={setSection}
header={
<p className="flex items-center gap-2 px-2 pt-1 text-base font-semibold">
<span className="font-bold tracking-tight">suiss</span>
Store Admin
</p>
}
footer={
<div className="flex items-center gap-2.5 px-2 pb-1">
<Avatar>
<AvatarFallback>JR</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-sm font-semibold">Jamie Rivera</span>
<span className="text-xs text-muted-foreground">Admin</span>
</div>
</div>
}
/>
<SidebarInset>
<div className="flex flex-col gap-5 px-5 py-5.5 md:px-7">
<div className="flex flex-wrap items-start gap-4">
<SidebarTrigger className="-ms-2 mt-1" />
<div className="flex flex-1 flex-col">
<h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
<p className="text-sm text-muted-foreground">All stores</p>
</div>
<ToggleGroup
spacing={0}
type="single"
value={range}
onValueChange={(value: string) => {
if (value) setRange(value)
}}
aria-label="Range"
>
{ranges.map((item) => (
<ToggleGroupItem key={item.value} value={item.value} size="sm">
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<dl className="grid grid-cols-2 overflow-hidden rounded-xl bg-muted">
{stats.map((stat, index) => (
<div
key={stat.label}
data-index={index}
className="flex flex-col gap-0.5 border-border px-4 py-3.5 data-[index='1']:border-s data-[index='2']:border-t data-[index='3']:border-s data-[index='3']:border-t"
>
<dt className="text-sm text-muted-foreground">{stat.label}</dt>
<dd className="text-2xl font-semibold tracking-tight tabular-nums max-sm:text-xl">
{stat.value}
</dd>
<dd
data-up={stat.up ? "" : undefined}
className="text-xs text-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_25%)] data-up:text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-red-500 dark:data-up:text-green-500"
>
{stat.change}
<span className="max-sm:sr-only"> vs. previous period</span>
</dd>
</div>
))}
</dl>
<div className="rounded-xl bg-muted px-4 pt-3.5 pb-2">
<div className="flex items-start gap-2">
<div className="flex flex-1 flex-col" aria-live="polite">
<span className="text-sm text-muted-foreground">
{activeDay ? activeDay.day : "Weekly sales"}
</span>
<span className="text-2xl font-semibold tabular-nums">
{activeDay
? money((activeDay.online + activeDay.store) * 1000)
: money(1284500 * scale)}
</span>
</div>
<div className="flex gap-3 text-xs text-muted-foreground">
{(["online", "store"] as const).map((key) => (
<span key={key} className="flex items-center gap-1.25">
<span
style={{ background: chartConfig[key].color }}
className="size-2 rounded-full"
/>
{chartConfig[key].label}
</span>
))}
</div>
</div>
<ChartContainer
config={chartConfig}
className="mt-3.5 aspect-auto h-40 w-full"
>
<BarChart
accessibilityLayer
data={data}
barGap={3}
margin={{ top: 4, right: 0, left: 0, bottom: 0 }}
>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
dataKey="day"
tickLine={false}
axisLine={false}
tickMargin={6}
/>
<ChartTooltip content={() => null} cursor={false} />
{(["online", "store"] as const).map((key) => (
<Bar
key={key}
dataKey={key}
fill={`var(--color-${key})`}
radius={3}
maxBarSize={12}
isAnimationActive={false}
>
{data.map((day) => (
<Cell
key={day.day}
fillOpacity={
active === null || active === day.index ? 1 : 0.35
}
className="transition-[fill-opacity] duration-200"
/>
))}
</Bar>
))}
<ActiveDay onChange={setActive} />
</BarChart>
</ChartContainer>
</div>
<div className="flex flex-wrap items-center gap-3">
<h2 className="flex-1 px-1 text-sm font-semibold">Recent orders</h2>
<ToggleGroup
spacing={0}
type="single"
value={filter}
onValueChange={(value: string) => {
if (value) setFilter(value)
}}
aria-label="Order status"
>
{filters.map((item) => (
<ToggleGroupItem key={item} value={item} size="sm">
{item}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<ItemGroup
variant="inset"
className="**:data-[slot=item-separator]:ms-15"
>
{orders
.filter((order) => filter === "All" || order.status === filter)
.map((order, index) => (
<React.Fragment key={`${filter}-${order.id}`}>
{index > 0 && <ItemSeparator />}
<Item
size="sm"
role="listitem"
className="transition-[opacity,translate] duration-700 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-1.5 starting:opacity-0 motion-reduce:transition-none"
>
<Avatar>
<AvatarFallback
style={{
background: `linear-gradient(160deg, oklch(0.78 0.1 ${order.hue}), oklch(0.6 0.14 ${order.hue + 30}))`,
}}
className="text-white"
>
{initials(order.name)}
</AvatarFallback>
</Avatar>
<ItemContent className="gap-0">
<ItemTitle className="font-semibold">
{order.name}
</ItemTitle>
<ItemDescription className="text-sm">
{order.product} · {order.id}
</ItemDescription>
</ItemContent>
<ItemActions className="gap-4 text-sm">
<span
data-returned={
order.status === "Returned" ? "" : undefined
}
className="hidden w-24 text-end text-muted-foreground data-returned:text-destructive sm:block"
>
{order.status}
</span>
<span className="w-20 text-end text-base tabular-nums">
{money(order.total)}
</span>
</ItemActions>
</Item>
</React.Fragment>
))}
</ItemGroup>
</div>
</SidebarInset>
</SidebarProvider>
)
}

























