feat: add dashboard charts — message trend, activity heatmap, top channels

This commit is contained in:
Developer
2026-07-28 09:51:53 +07:00
parent 63ad55edb5
commit 9504edc327
3 changed files with 146 additions and 0 deletions
@@ -0,0 +1,62 @@
"use client";
import { GlassCard } from "@/components/glass/card";
import { cn } from "@/lib/utils";
const HOURS = Array.from({ length: 24 }, (_, i) => i);
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
interface ActivityHeatmapProps {
data?: Record<string, number>; // key: "day-hour", value: count
}
export function ActivityHeatmap({ data = {} }: ActivityHeatmapProps) {
const maxVal = Math.max(...Object.values(data), 1);
const getIntensity = (day: string, hour: number) => {
const val = data[`${day}-${hour}`] || 0;
const pct = val / maxVal;
if (pct === 0) return "bg-surface";
if (pct < 0.25) return "bg-primary/15";
if (pct < 0.5) return "bg-primary/30";
if (pct < 0.75) return "bg-primary/50";
return "bg-primary/70";
};
return (
<GlassCard variant="base">
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">Activity</span>
<span className="text-[10px] text-text-secondary/40">hour × day</span>
</div>
<div className="overflow-x-auto">
<div className="flex gap-0.5 min-w-[400px]">
{/* Hour labels */}
<div className="flex flex-col gap-0.5 mr-1">
<div className="h-4" />
{DAYS.map((d) => (
<div key={d} className="h-3 flex items-center text-[8px] text-text-secondary/40 font-mono">{d}</div>
))}
</div>
{/* Grid */}
<div className="flex gap-0.5">
{HOURS.map((hour) => (
<div key={hour} className="flex flex-col gap-0.5">
{DAYS.map((day) => (
<div
key={`${day}-${hour}`}
className={cn("size-3 rounded-sm transition-colors", getIntensity(day, hour))}
title={`${day} ${hour}:00 — ${data[`${day}-${hour}`] || 0}`}
/>
))}
<div className="h-3 flex items-center justify-center text-[8px] text-text-secondary/30 font-mono">
{hour % 4 === 0 ? hour : ""}
</div>
</div>
))}
</div>
</div>
</div>
</GlassCard>
);
}
@@ -0,0 +1,48 @@
"use client";
import { GlassCard } from "@/components/glass/card";
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
interface MessageTrendChartProps {
data?: { date: string; messages: number; flagged: number }[];
}
export function MessageTrendChart({ data = [] }: MessageTrendChartProps) {
return (
<GlassCard variant="base">
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">Message Trend</span>
<span className="text-[10px] text-text-secondary/40">7 days</span>
</div>
<div className="h-48">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<defs>
<linearGradient id="trend-msg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-primary)" stopOpacity={0.3} />
<stop offset="100%" stopColor="var(--color-primary)" stopOpacity={0} />
</linearGradient>
<linearGradient id="trend-flag" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-destructive)" stopOpacity={0.3} />
<stop offset="100%" stopColor="var(--color-destructive)" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fill: "oklch(0.55 0.02 245)", fontSize: 10 }} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: "oklch(0.55 0.02 245)", fontSize: 10 }} />
<Tooltip
contentStyle={{
background: "oklch(0.11 0.02 245 / 0.9)",
border: "1px solid oklch(1 0 0 / 0.08)",
borderRadius: 8,
fontSize: 12,
color: "oklch(0.93 0.01 245)",
}}
/>
<Area type="monotone" dataKey="messages" stroke="var(--color-primary)" strokeWidth={2} fill="url(#trend-msg)" />
<Area type="monotone" dataKey="flagged" stroke="var(--color-destructive)" strokeWidth={1.5} fill="url(#trend-flag)" />
</AreaChart>
</ResponsiveContainer>
</div>
</GlassCard>
);
}
@@ -0,0 +1,36 @@
"use client";
import { GlassCard } from "@/components/glass/card";
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
interface TopChannelsChartProps {
data?: { name: string; count: number }[];
}
export function TopChannelsChart({ data = [] }: TopChannelsChartProps) {
return (
<GlassCard variant="base">
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">Top Channels</span>
</div>
<div className="h-48">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} layout="vertical">
<XAxis type="number" axisLine={false} tickLine={false} tick={{ fill: "oklch(0.55 0.02 245)", fontSize: 10 }} />
<YAxis type="category" dataKey="name" axisLine={false} tickLine={false} tick={{ fill: "oklch(0.55 0.02 245)", fontSize: 10 }} width={80} />
<Tooltip
contentStyle={{
background: "oklch(0.11 0.02 245 / 0.9)",
border: "1px solid oklch(1 0 0 / 0.08)",
borderRadius: 8,
fontSize: 12,
color: "oklch(0.93 0.01 245)",
}}
/>
<Bar dataKey="count" fill="var(--color-primary)" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</GlassCard>
);
}