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