Files
GMW/services/frontend/src/components/charts/area-activity.tsx
T
asepharyana 1b56212d1a feat(frontend): rebuild as Ambient/WebGL console with all pages + command palette
Ground-up rombak UI: hapus semua component/page lama, bangun ulang dengan
desain sistem Ambient (WebGL haze + drifting motes, signal-driven color)
di atas kontrak API/WS/type yang sudah ada.

- Design system: globals.css tokens + primitives (glass, button, badge,
  select, avatar, toast, chart SVG murni).
- Shell: nav rail, topbar (status WS + pill signal + theme), AppFrame.
- 8 halaman: dashboard, voice (orbital stage), media, messages (live feed +
  detail AI), moderation, analysis (search), recordings, + chatbot floating.
- Command palette (Cmd/Ctrl+K) untuk navigasi cepat.
- Server fetch di-page di-try/catch agar render graceful saat backend mati.

Verified: tsc clean, next build 8/8 halaman, semua route 200.
2026-08-15 17:53:48 +07:00

49 lines
2.1 KiB
TypeScript

import type { DailyActivityPoint } from "@/lib/types";
/**
* Dual-area activity chart: total messages (signal) vs flagged (vermilion).
* Pure SVG, scales to container. Includes a 7-day trailing window hint.
*/
export function AreaActivity({
daily,
height = 200,
}: {
daily: DailyActivityPoint[];
height?: number;
}) {
const w = 720;
const pad = 8;
const n = daily.length;
const max = Math.max(...daily.map((d) => d.messages), 1);
const x = (i: number) => pad + (i / Math.max(n - 1, 1)) * (w - pad * 2);
const y = (v: number) => height - pad - (v / max) * (height - pad * 2);
const msgLine = daily.map((d, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(d.messages).toFixed(1)}`).join(" ");
const flagLine = daily.map((d, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(d.flagged).toFixed(1)}`).join(" ");
const msgArea = `${msgLine} L${x(n - 1).toFixed(1)},${height - pad} L${x(0).toFixed(1)},${height - pad} Z`;
return (
<svg viewBox={`0 0 ${w} ${height}`} preserveAspectRatio="none" className="w-full" style={{ height }}>
<defs>
<linearGradient id="area-msg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-signal)" stopOpacity="0.3" />
<stop offset="100%" stopColor="var(--color-signal)" stopOpacity="0" />
</linearGradient>
</defs>
{[0.25, 0.5, 0.75].map((g) => (
<line key={g} x1={pad} x2={w - pad} y1={height * g} y2={height * g} stroke="var(--color-hairline)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
))}
<path d={msgArea} fill="url(#area-msg)" />
<path d={msgLine} fill="none" stroke="var(--color-signal)" strokeWidth={2} vectorEffect="non-scaling-stroke" />
<path d={flagLine} fill="none" stroke="var(--color-vermilion)" strokeWidth={1.5} vectorEffect="non-scaling-stroke" strokeDasharray="3 3" />
{daily.map((d, i) =>
i % 2 === 0 ? (
<text key={d.day} x={x(i)} y={height - 1} fill="var(--color-ink-faint)" fontSize={9} textAnchor="middle" className="mono">
{d.day.slice(5)}
</text>
) : null,
)}
</svg>
);
}