From 3ecb7bd15cf66e7017b378963fce604277756a55 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 4 Jun 2026 14:16:59 +0700 Subject: [PATCH] chore: session auto-commit --- .../components/AttachmentStatsPanel.tsx | 217 ++++++++++++------ .../analytics/components/TopicList.tsx | 70 +++--- .../analytics/components/UserTable.tsx | 25 +- 3 files changed, 201 insertions(+), 111 deletions(-) diff --git a/services/frontend/src/features/analytics/components/AttachmentStatsPanel.tsx b/services/frontend/src/features/analytics/components/AttachmentStatsPanel.tsx index bb942e0..87a5954 100644 --- a/services/frontend/src/features/analytics/components/AttachmentStatsPanel.tsx +++ b/services/frontend/src/features/analytics/components/AttachmentStatsPanel.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import type { AttachmentStats } from "../../../shared/api/client"; import { cn } from "../../../shared/lib/utils"; import { @@ -13,14 +14,99 @@ interface AttachmentStatsPanelProps { loading: boolean; } +const UPLOAD_COLORS = { + uploaded: { color: "#38bdf8", label: "Uploaded" }, + pending: { color: "#facc15", label: "Pending" }, + failed: { color: "#f472b6", label: "Failed" }, +} as const; + +function UploadDonut({ + uploaded, + pending, + failed, + total, +}: { uploaded: number; pending: number; failed: number; total: number }) { + const [hoveredKey, setHoveredKey] = useState(null); + const size = 120; + const strokeWidth = 20; + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const center = size / 2; + + const entries = [ + { key: "uploaded" as const, ...UPLOAD_COLORS.uploaded, value: uploaded }, + { key: "pending" as const, ...UPLOAD_COLORS.pending, value: pending }, + { key: "failed" as const, ...UPLOAD_COLORS.failed, value: failed }, + ].filter((e) => e.value > 0); + + let cumulative = 0; + const segments = entries.map((e) => { + const offset = cumulative; + const length = (e.value / total) * circumference; + cumulative += length; + return { ...e, length, offset }; + }); + + return ( +
+ + + {segments.map((seg) => { + const isHovered = hoveredKey === seg.key; + return ( + setHoveredKey(seg.key)} + onMouseLeave={() => setHoveredKey(null)} + /> + ); + })} + +
+ + {total} + + Total +
+ {hoveredKey && ( +
+ {(() => { + const e = entries.find((en) => en.key === hoveredKey); + if (!e) return null; + return `${e.label}: ${e.value}`; + })()} +
+ )} +
+ ); +} + export function AttachmentStatsPanel({ stats, loading, }: AttachmentStatsPanelProps) { - if (loading && !stats) { - return ; - } - + if (loading && !stats) return ; if (!stats || stats.total_attachments === 0) { return ( @@ -35,53 +121,18 @@ export function AttachmentStatsPanel({ stats.total_attachments > 0 ? Math.round((stats.uploaded / stats.total_attachments) * 100) : 0; - const failedPct = stats.total_attachments > 0 ? Math.round((stats.failed / stats.total_attachments) * 100) : 0; - const totalSizeMB = stats.total_size_bytes / (1024 * 1024); - const cards = [ - { - label: "Total Media", - value: formatNum(stats.total_attachments), - accent: "text-foreground", - }, + const metricCards = [ + { label: "Total Media", value: formatNum(stats.total_attachments), accent: "text-foreground" }, { label: "Upload Success", value: `${uploadPct}%`, accent: "text-primary" }, { label: "Gagal Upload", value: `${failedPct}%`, accent: "text-accent" }, - { - label: "Total Ukuran", - value: `${totalSizeMB.toFixed(1)} MB`, - accent: "text-muted-foreground", - }, - { - label: "Pengupload", - value: formatNum(stats.unique_uploaders), - accent: "text-primary", - }, - ]; - - const statusCards = [ - { - label: "Uploaded", - value: formatNum(stats.uploaded), - total: stats.total_attachments, - barClass: "bg-gradient-to-r from-primary to-teal-300", - }, - { - label: "Pending", - value: formatNum(stats.pending), - total: stats.total_attachments, - barClass: "bg-yellow-400/60", - }, - { - label: "Failed", - value: formatNum(stats.failed), - total: stats.total_attachments, - barClass: "bg-accent/70", - }, + { label: "Total Ukuran", value: `${totalSizeMB.toFixed(1)} MB`, accent: "text-muted-foreground" }, + { label: "Pengupload", value: formatNum(stats.unique_uploaders), accent: "text-primary" }, ]; return ( @@ -106,12 +157,12 @@ export function AttachmentStatsPanel({
- {cards.map((c) => ( + {metricCards.map((c) => (
-
+
{c.label}
-
-

- Status Upload -

- {statusCards.map((s) => ( -
- - {s.label} - -
-
-
- - {s.value} - -
- ))} + {/* Donut + status bars */} +
+ + + {/* Status legend with inline bars */} +
+ {[ + { + key: "uploaded", + ...UPLOAD_COLORS.uploaded, + value: stats.uploaded, + }, + { + key: "pending", + ...UPLOAD_COLORS.pending, + value: stats.pending, + }, + { + key: "failed", + ...UPLOAD_COLORS.failed, + value: stats.failed, + }, + ].map((s) => { + const pct = (s.value / stats.total_attachments) * 100; + return ( +
+ + {s.label} + +
+
+
+ + {s.value} + +
+ ); + })} +
@@ -163,7 +242,7 @@ function LoadingBox() { return ( - + Memuat data... diff --git a/services/frontend/src/features/analytics/components/TopicList.tsx b/services/frontend/src/features/analytics/components/TopicList.tsx index d74d5f2..9073b69 100644 --- a/services/frontend/src/features/analytics/components/TopicList.tsx +++ b/services/frontend/src/features/analytics/components/TopicList.tsx @@ -14,11 +14,16 @@ interface TopicListProps { loading: boolean; } -export function TopicList({ topics, loading }: TopicListProps) { - if (loading && !topics?.length) { - return ; - } +const TOPIC_COLORS = [ + "from-primary to-sky-300", + "from-accent to-pink-300", + "from-orange-400 to-yellow-300", + "from-emerald-400 to-teal-300", + "from-violet-400 to-purple-300", +]; +export function TopicList({ topics, loading }: TopicListProps) { + if (loading && !topics?.length) return ; if (!topics?.length) { return ( @@ -44,31 +49,40 @@ export function TopicList({ topics, loading }: TopicListProps) { -
- {topics.map((topic, i) => ( -
- - {i + 1} - - - {topic.topic} - -
-
-
-
- - {topic.count} +
+ {topics.map((topic, i) => { + const colorClass = + TOPIC_COLORS[i % TOPIC_COLORS.length]; + return ( +
+ + {i + 1} + + {topic.topic} + +
+
+
+
+ + {topic.count} + +
-
- ))} + ); + })}
@@ -80,7 +94,7 @@ function LoadingBox() { return ( - + Memuat data... diff --git a/services/frontend/src/features/analytics/components/UserTable.tsx b/services/frontend/src/features/analytics/components/UserTable.tsx index 0dd13de..d3506f8 100644 --- a/services/frontend/src/features/analytics/components/UserTable.tsx +++ b/services/frontend/src/features/analytics/components/UserTable.tsx @@ -17,10 +17,7 @@ interface UserTableProps { } export function UserTable({ users, loading }: UserTableProps) { - if (loading && !users?.length) { - return ; - } - + if (loading && !users?.length) return ; if (!users?.length) { return ( @@ -49,7 +46,7 @@ export function UserTable({ users, loading }: UserTableProps) { - + @@ -57,13 +54,13 @@ export function UserTable({ users, loading }: UserTableProps) { - + {users.map((user, i) => (
# User PesanFlag
@@ -75,11 +72,11 @@ export function UserTable({ users, loading }: UserTableProps) { ) : ( -
+
{user.username.charAt(0).toUpperCase()}
)} @@ -90,15 +87,15 @@ export function UserTable({ users, loading }: UserTableProps) {
-
+
- + {user.message_count}
@@ -134,7 +131,7 @@ function LoadingBox() { return ( - + Memuat data...