feat: add dashboard charts — message trend, activity heatmap, top channels
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user