snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { MoveRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
Progress,
|
||||
ProgressLabel,
|
||||
ProgressValue,
|
||||
} from "@/styles/base-sera/ui/progress"
|
||||
|
||||
const DEMOGRAPHIC_DATA = [
|
||||
{ age: "18 - 24", percentage: 22 },
|
||||
{ age: "25 - 34", percentage: 64 },
|
||||
{ age: "35 - 44", percentage: 12 },
|
||||
{ age: "45+", percentage: 5 },
|
||||
]
|
||||
|
||||
export function Demographics({ ...props }: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Demographics</CardTitle>
|
||||
<CardDescription>Reader Profile</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-10">
|
||||
{DEMOGRAPHIC_DATA.map((item) => (
|
||||
<Progress
|
||||
key={item.age}
|
||||
value={item.percentage}
|
||||
aria-label={item.age}
|
||||
>
|
||||
<ProgressLabel>{item.age}</ProgressLabel>
|
||||
<ProgressValue>
|
||||
{(formattedValue) => `${formattedValue}`}
|
||||
</ProgressValue>
|
||||
</Progress>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="link" className="w-full">
|
||||
View all source <MoveRightIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { cn } from "cn"
|
||||
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
|
||||
type Metric = {
|
||||
label: string
|
||||
value: string
|
||||
comparison: string
|
||||
change: string
|
||||
trend: "up" | "down"
|
||||
}
|
||||
|
||||
const METRIC_CARDS: Metric[] = [
|
||||
{
|
||||
label: "Total visitors",
|
||||
value: "248.5k",
|
||||
comparison: "12.4%",
|
||||
change: "vs last period",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
label: "Unique readers",
|
||||
value: "182.1k",
|
||||
comparison: "8.7%",
|
||||
change: "vs last period",
|
||||
trend: "up",
|
||||
},
|
||||
{
|
||||
label: "Avg. time on page",
|
||||
value: "3m 42s",
|
||||
comparison: "1.2%",
|
||||
change: "vs last period",
|
||||
trend: "down",
|
||||
},
|
||||
{
|
||||
label: "Bounce rate",
|
||||
value: "42.8%",
|
||||
comparison: "3.5%",
|
||||
change: "vs last period",
|
||||
trend: "down",
|
||||
},
|
||||
]
|
||||
|
||||
export function MetricsGrid() {
|
||||
return (
|
||||
<>
|
||||
{METRIC_CARDS.map((metric) => (
|
||||
<MetricCard
|
||||
key={metric.label}
|
||||
metric={metric}
|
||||
className="col-span-full md:col-span-6 lg:col-span-3"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
metric,
|
||||
className,
|
||||
}: {
|
||||
metric: Metric
|
||||
className: string
|
||||
}) {
|
||||
return (
|
||||
<Card className={cn("gap-0", className)}>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<CardDescription className="text-xs uppercase">
|
||||
{metric.label}
|
||||
</CardDescription>
|
||||
<CardTitle className="text-5xl tracking-tight lowercase">
|
||||
{metric.value}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{metric.trend === "up" ? (
|
||||
<TrendingUpIcon className="inline-block size-2.5 text-muted-foreground" />
|
||||
) : (
|
||||
<TrendingDownIcon className="inline-block size-2.5 text-muted-foreground" />
|
||||
)}{" "}
|
||||
<span className="text-foreground">{metric.comparison}</span>{" "}
|
||||
<span>{metric.change}</span>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon, DownloadIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import { ButtonGroup } from "@/styles/base-sera/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
|
||||
const EXPORT_DATE_OPTIONS = [
|
||||
{
|
||||
label: "Last 7 days",
|
||||
value: "last-7-days",
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
value: "last-30-days",
|
||||
},
|
||||
{
|
||||
label: "This month",
|
||||
value: "this-month",
|
||||
},
|
||||
{
|
||||
label: "Last month",
|
||||
value: "last-month",
|
||||
},
|
||||
]
|
||||
|
||||
export function PreviewHeader() {
|
||||
const [selectedDateRange, setSelectedDateRange] =
|
||||
React.useState("last-30-days")
|
||||
|
||||
const selectedDateRangeLabel = React.useMemo(() => {
|
||||
const selectedOption = EXPORT_DATE_OPTIONS.find(
|
||||
(option) => option.value === selectedDateRange
|
||||
)
|
||||
|
||||
if (!selectedOption) {
|
||||
return "Last 30 days"
|
||||
}
|
||||
|
||||
return selectedOption.label
|
||||
}, [selectedDateRange])
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="container flex flex-col items-center justify-center gap-(--gap) py-(--gap) sm:flex-row sm:justify-between">
|
||||
<div className="flex flex-col gap-2 text-center sm:text-left">
|
||||
<h1 className="line-clamp-1 font-heading text-3xl tracking-wide uppercase md:text-3xl lg:text-4xl">
|
||||
Audience Analytics
|
||||
</h1>
|
||||
<div className="line-clamp-1 text-sm font-medium tracking-wider text-muted-foreground uppercase">
|
||||
Editorial Performance Dashboard
|
||||
</div>
|
||||
</div>
|
||||
<ButtonGroup className="gap-2 sm:ml-auto md:gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-background hover:bg-background/80 data-popup-open:bg-background"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{selectedDateRangeLabel}{" "}
|
||||
<ChevronDownIcon data-icon="inline-end" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedDateRange}
|
||||
onValueChange={setSelectedDateRange}
|
||||
>
|
||||
{EXPORT_DATE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button>
|
||||
<DownloadIcon data-icon="inline-start" />
|
||||
<span className="lg:hidden">Export</span>
|
||||
<span className="hidden lg:inline">Export Report</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ArrowDownIcon, MoreHorizontalIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/base-sera/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-sera/ui/dropdown-menu"
|
||||
import { Spinner } from "@/styles/base-sera/ui/spinner"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/styles/base-sera/ui/table"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/styles/base-sera/ui/toggle-group"
|
||||
|
||||
type EditorialMetric = "views" | "time" | "shares"
|
||||
|
||||
type EditorialRow = {
|
||||
rank: number
|
||||
title: string
|
||||
author: string
|
||||
published: string
|
||||
pageviews: string
|
||||
avgTime: string
|
||||
}
|
||||
|
||||
const METRIC_LABEL: Record<EditorialMetric, string> = {
|
||||
views: "VIEWS",
|
||||
time: "TIME",
|
||||
shares: "SHARES",
|
||||
}
|
||||
|
||||
const EDITORIAL_ROWS: EditorialRow[] = [
|
||||
{
|
||||
rank: 1,
|
||||
title: "The New Vanguard of Minimalist Architecture",
|
||||
author: "Elena Rostova",
|
||||
published: "Oct 12",
|
||||
pageviews: "45.2k",
|
||||
avgTime: "04:15",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
title: "Autumn Sartorial Code: Deconstructed Classics",
|
||||
author: "Julian Vance",
|
||||
published: "Oct 05",
|
||||
pageviews: "38.9k",
|
||||
avgTime: "03:42",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
title: "Interview: Director Sofia Coppola on The Aesthetics of Isolation",
|
||||
author: "Marcus Trent",
|
||||
published: "Sep 28",
|
||||
pageviews: "31.4k",
|
||||
avgTime: "06:20",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
title: "Sourcing Ceramics from Kyoto's Oldest Kilns",
|
||||
author: "Sarah Lin",
|
||||
published: "Oct 18",
|
||||
pageviews: "22.1k",
|
||||
avgTime: "02:55",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
title: "Field Notes from Copenhagen Design Week",
|
||||
author: "Noah Bennett",
|
||||
published: "Oct 21",
|
||||
pageviews: "19.7k",
|
||||
avgTime: "03:18",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
title: "A Studio Visit with Milan's Most Elusive Lighting Designer",
|
||||
author: "Claire Duval",
|
||||
published: "Oct 09",
|
||||
pageviews: "17.4k",
|
||||
avgTime: "04:02",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
title: "Collecting the New Avant-Garde in Contemporary Furniture",
|
||||
author: "Tommy Rhodes",
|
||||
published: "Sep 30",
|
||||
pageviews: "15.9k",
|
||||
avgTime: "03:36",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
title: "Inside Lisbon's Quiet Culinary Renaissance",
|
||||
author: "Amara Iqbal",
|
||||
published: "Oct 14",
|
||||
pageviews: "14.2k",
|
||||
avgTime: "05:08",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
title: "Why Slow Interiors Are Defining the Next Luxury Wave",
|
||||
author: "Henry Vale",
|
||||
published: "Oct 03",
|
||||
pageviews: "12.7k",
|
||||
avgTime: "03:11",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
title: "The Return of Print: Independent Magazine Covers to Watch",
|
||||
author: "Mina Okafor",
|
||||
published: "Sep 26",
|
||||
pageviews: "11.3k",
|
||||
avgTime: "02:49",
|
||||
},
|
||||
]
|
||||
|
||||
type TopEditorialProps = React.ComponentProps<typeof Card> & {
|
||||
selectedMetric?: EditorialMetric
|
||||
}
|
||||
|
||||
export function TopEditorial({
|
||||
selectedMetric = "views",
|
||||
...props
|
||||
}: TopEditorialProps) {
|
||||
const [visibleCount, setVisibleCount] = React.useState(5)
|
||||
const [isLoadingMore, setIsLoadingMore] = React.useState(false)
|
||||
const hasMoreRows = visibleCount < EDITORIAL_ROWS.length
|
||||
const visibleRows = EDITORIAL_ROWS.slice(0, visibleCount)
|
||||
|
||||
const handleLoadMore = React.useCallback(() => {
|
||||
if (!hasMoreRows || isLoadingMore) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoadingMore(true)
|
||||
|
||||
window.setTimeout(() => {
|
||||
setVisibleCount(EDITORIAL_ROWS.length)
|
||||
setIsLoadingMore(false)
|
||||
}, 2000)
|
||||
}, [hasMoreRows, isLoadingMore])
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-(--gap) sm:flex-row">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<CardTitle className="text-2xl">Top Editorials</CardTitle>
|
||||
<CardDescription>Ranked by engagement</CardDescription>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
aria-label="Top editorials metric selector"
|
||||
value={[selectedMetric]}
|
||||
variant="outline"
|
||||
className="w-full sm:ml-auto sm:w-fit"
|
||||
>
|
||||
{(["views", "time", "shares"] as const).map((metric) => {
|
||||
return (
|
||||
<ToggleGroupItem key={metric} value={metric} className="flex-1">
|
||||
{METRIC_LABEL[metric]}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
})}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 **:data-[slot=table-container]:no-scrollbar **:data-[slot=table-container]:overflow-y-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Published</TableHead>
|
||||
<TableHead>Page Views</TableHead>
|
||||
<TableHead>Read Time</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{visibleRows.map((row) => (
|
||||
<TableRow key={row.rank}>
|
||||
<TableCell className="translate-y-1 align-text-top">
|
||||
{row.rank}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-heading text-xl tracking-tight text-foreground">
|
||||
{row.title}
|
||||
</p>
|
||||
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
By {row.author}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.published}</TableCell>
|
||||
<TableCell>{row.pageviews}</TableCell>
|
||||
<TableCell>{row.avgTime}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
aria-label={`Open actions for ${row.title}`}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Publish</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
{hasMoreRows ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
Load more content{" "}
|
||||
{isLoadingMore ? (
|
||||
<Spinner data-icon="inline-end" />
|
||||
) : (
|
||||
<ArrowDownIcon data-icon="inline-end" />
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
|
||||
const TrafficOverviewContent = dynamic(
|
||||
() => import("./traffic-overview").then((mod) => mod.TrafficOverview),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <TrafficOverviewFallback />,
|
||||
}
|
||||
)
|
||||
|
||||
export function TrafficOverviewDeferred({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<TrafficOverviewContent {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrafficOverviewFallback() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Traffic for the last 30 days has increased by 12.4% compared to the
|
||||
previous period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="flex h-82 w-full flex-col justify-end gap-6 overflow-hidden bg-muted/40 p-5"
|
||||
>
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
<div className="h-px w-full bg-border" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUpIcon } from "lucide-react"
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceDot,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
import { Badge } from "@/styles/base-sera/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/styles/base-sera/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/styles/base-sera/ui/chart"
|
||||
|
||||
const TRAFFIC_OVERVIEW_DATA = [
|
||||
{ date: "2025-10-01", views: 2600, unique: 1600 },
|
||||
{ date: "2025-10-04", views: 4500, unique: 3000 },
|
||||
{ date: "2025-10-08", views: 3500, unique: 2500 },
|
||||
{ date: "2025-10-10", views: 6400, unique: 4500 },
|
||||
{ date: "2025-10-13", views: 5400, unique: 4000 },
|
||||
{ date: "2025-10-15", views: 8300, unique: 6500 },
|
||||
{ date: "2025-10-17", views: 7400, unique: 6000 },
|
||||
{ date: "2025-10-18", views: 9240, unique: 7105 },
|
||||
{ date: "2025-10-22", views: 7700, unique: 6400 },
|
||||
{ date: "2025-10-26", views: 8800, unique: 7000 },
|
||||
{ date: "2025-10-29", views: 9800, unique: 8400 },
|
||||
]
|
||||
|
||||
const TRAFFIC_CHART_CONFIG = {
|
||||
views: {
|
||||
label: "Views",
|
||||
theme: {
|
||||
light: "var(--chart-5)",
|
||||
dark: "var(--chart-1)",
|
||||
},
|
||||
},
|
||||
unique: {
|
||||
label: "Unique",
|
||||
theme: {
|
||||
light: "var(--chart-1)",
|
||||
dark: "var(--chart-2)",
|
||||
},
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const X_AXIS_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
})
|
||||
|
||||
function formatYAxisTick(value: number) {
|
||||
if (value === 0) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
if (value % 1000 === 0) {
|
||||
return `${value / 1000}k`
|
||||
}
|
||||
|
||||
return `${value / 1000}k`
|
||||
}
|
||||
|
||||
function formatXAxisTick(value: string) {
|
||||
const date = new Date(`${value}T00:00:00Z`)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value
|
||||
}
|
||||
|
||||
return X_AXIS_DATE_FORMATTER.format(date)
|
||||
}
|
||||
|
||||
export function TrafficOverview({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Traffic Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Traffic for the last 30 days has increased by 12.4% compared to the
|
||||
previous period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={TRAFFIC_CHART_CONFIG} className="h-82 w-full">
|
||||
<LineChart data={TRAFFIC_OVERVIEW_DATA}>
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
strokeDasharray="3 6"
|
||||
stroke="var(--border)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
interval="preserveStartEnd"
|
||||
tickMargin={10}
|
||||
tickFormatter={formatXAxisTick}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={44}
|
||||
domain={[0, 10000]}
|
||||
ticks={[0, 2500, 5000, 7500, 10000]}
|
||||
tickFormatter={formatYAxisTick}
|
||||
hide
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="views"
|
||||
stroke="var(--color-views)"
|
||||
strokeWidth={2.2}
|
||||
dot={false}
|
||||
activeDot={{ r: 3.5, fill: "var(--color-views)" }}
|
||||
/>
|
||||
<Line
|
||||
type="linear"
|
||||
dataKey="unique"
|
||||
stroke="var(--color-unique)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="4 6"
|
||||
dot={false}
|
||||
activeDot={false}
|
||||
/>
|
||||
<ReferenceDot
|
||||
x="2025-10-18"
|
||||
y={9240}
|
||||
r={2.5}
|
||||
fill="var(--color-views)"
|
||||
stroke="var(--color-views)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user