From 183a51003aca5894bb57f42adea13bedd81374b9 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 25 Aug 2026 22:59:19 +0700 Subject: [PATCH] feat(frontend): revamp dashboard into tactical HUD and integrate GSAP reactive stages --- .../src/app/(dashboard)/dashboard/view.tsx | 578 +++++++++--------- .../src/components/shell/nav-rail.tsx | 47 +- .../src/components/voice/voice-stage.tsx | 75 ++- 3 files changed, 383 insertions(+), 317 deletions(-) diff --git a/services/frontend/src/app/(dashboard)/dashboard/view.tsx b/services/frontend/src/app/(dashboard)/dashboard/view.tsx index a5d61dce..beba6279 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/view.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/view.tsx @@ -1,46 +1,49 @@ "use client"; import { - Activity, - Flag, + AudioWaveform, + ChevronRight, + Flame, MessageSquare, Mic, - Radio, + Shield, ShieldAlert, + Terminal, Users, } from "lucide-react"; +import Link from "next/link"; import { useEffect } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { AreaActivity, RadialGauge } from "@/components/charts"; import { GlassPanel } from "@/components/primitives"; import { ErrorState, - LoadingState, + PageTransition, SkeletonHero, SkeletonMetricRow, SkeletonPanel, } from "@/components/shared"; -import { MetricTile, SectionHeader } from "@/components/shared/section"; -import { - useActivity, - useStats, - useTopReactions, - useTopReactors, -} from "@/hooks"; +import { useActivity, useStats, useTopReactions } from "@/hooks"; +import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { formatNumber } from "@/lib/format"; import type { DashboardStats } from "@/lib/types"; -import { staggerDelay } from "@/lib/utils"; function deriveSignal(stats?: DashboardStats) { - if (!stats) return { tone: "signal" as const, label: "nominal" }; + if (!stats) + return { tone: "signal" as const, label: "NOMINAL", state: "STABLE" }; const total = stats.total_flagged + stats.total_clean || 1; const ratio = stats.total_flagged / total; if (stats.moderation_overview.error > 0) - return { tone: "vermilion" as const, label: "moderation fault" }; + return { tone: "vermilion" as const, label: "FAULT", state: "MOD_ERR" }; if (ratio > 0.25) - return { tone: "vermilion" as const, label: "elevated flags" }; - if (ratio > 0.1) return { tone: "amber" as const, label: "watch" }; - return { tone: "signal" as const, label: "nominal" }; + return { + tone: "vermilion" as const, + label: "ELEVATED", + state: "HIGH_RISK", + }; + if (ratio > 0.1) + return { tone: "amber" as const, label: "WATCH", state: "ADVISORY" }; + return { tone: "signal" as const, label: "NOMINAL", state: "OPTIMAL" }; } export function DashboardView({ @@ -57,10 +60,15 @@ export function DashboardView({ mutate: mutateStats, } = useStats(initialStats); const { data: activity } = useActivity(14, initialActivity as never); - const { data: reactors } = useTopReactors(); const { data: reactions } = useTopReactions(); const ambient = useAmbient(); + const hudRef = useStaggerReveal(".hud-tile", { + stagger: 0.05, + y: 16, + dependencies: [stats], + }); + useEffect(() => { const s = deriveSignal(stats); ambient.set( @@ -78,16 +86,12 @@ export function DashboardView({ -
- - -
); if (!stats) return ( void mutateStats()} /> ); @@ -95,286 +99,278 @@ export function DashboardView({ const s = stats; const total = s.total_flagged + s.total_clean || 1; const cleanRatio = s.total_clean / total; + const sig = deriveSignal(s); return ( -
- {/* Hero */} - -
-
-
-
GMW · Operations Grid
-

- Ambient Field -

-

- Real-time moderation, voice & media presence across the monitored - guild. {formatNumber(s.total_messages)} messages captured. -

+ +
+ {/* Tactical HUD Header Bar */} +
+
+
+ + +
+
+

+ GMW TELEMETRY · MATRIX_01 +

+ + [LIVE_MONITOR] + +
-
- - - {deriveSignal(s).label} - + +
+
+ SYS_STATUS: + {sig.state} +
+
+ VER: + v2.4-GSAP +
-
- } - className="animate-stagger" - style={staggerDelay(0)} - /> - 0 ? "vermilion" : "neutral"} - hint={`${s.today_flagged} today`} - className="animate-stagger" - style={staggerDelay(1)} - /> - } - className="animate-stagger" - style={staggerDelay(2)} - /> - } - className="animate-stagger" - style={staggerDelay(3)} - /> -
- - - {/* Activity */} - - - Activity & moderation - - } - action={ -
- - messages - - - flagged - -
- } - /> - {activity ? ( - - ) : ( - - )} -
- - {/* Two-column: channels + moderation */} -
- - -
- {s.top_channels.slice(0, 7).map((c) => { - const pct = - (c.message_count / (s.top_channels[0]?.message_count || 1)) * - 100; - return ( -
- - {c.channel_name ?? c.channel_id.slice(0, 8)} - -
-
-
- - {formatNumber(c.message_count)} + {/* Spatial HUD Grid: Live Mission Control */} +
+ {/* Main Telemetry & Activity Area */} +
+ {/* Primary Telemetry Cluster */} +
+
+
+ + Messages +
- ); - })} -
- - - - -
- 0.8 - ? "signal" - : cleanRatio > 0.6 - ? "amber" - : "vermilion" - } - label={`${Math.round(cleanRatio * 100)}%`} - sublabel="clean" - /> -
- } - label="Clean" - value={formatNumber(s.total_clean)} - /> - } - label="Flagged" - value={formatNumber(s.total_flagged)} - /> - } - label="Warned" - value={formatNumber(s.total_warned)} - /> -
-
-
- - - -
-
-
- - {/* Reactors + reactions */} -
- - -
- {(reactors ?? []).slice(0, 6).map((r, i) => { - const maxNet = reactors?.[0]?.net_count || 1; - const pct = Math.max(4, Math.round((r.net_count / maxNet) * 100)); - return ( -
- {i + 1} - - {r.username} - -
-
-
- - +{formatNumber(r.net_count)} - +
+ {formatNumber(s.total_messages)}
- ); - })} - {(reactors ?? []).length === 0 && } -
- - - - -
- {(reactions ?? []).slice(0, 5).map((m) => ( -
-
- {m.top_emojis.slice(0, 3).map((e, i) => ( - - {e.emoji} - - ))} +
+ ↑ live ingest
-
-
- {m.content || "(no text)"} -
-
- {m.username} · {m.channel_name ?? m.channel_id.slice(0, 8)} -
-
- - {m.reaction_count} -
- ))} - {(reactions ?? []).length === 0 && } + +
+
+ + Flagged + + +
+
+ {formatNumber(s.total_flagged)} +
+
+ {s.today_flagged}{" "} + today +
+
+ +
+
+ + Active 24h + + +
+
+ {formatNumber(s.active_users_24h)} +
+
+ unique accounts +
+
+ +
+
+ + Voice Captures + + +
+
+ {formatNumber(s.total_voice_recordings)} +
+
+ audio buffers +
+
+
+ + {/* Activity Waveform / Time Series */} + +
+
+
+ CHRONO TELEMETRY (14D) +
+
+ Message & Activity Stream +
+
+
+
+ + Activity Trend +
+
+
+
+ {activity?.daily ? ( + + ) : ( +
+ ACQUIRING SIGNAL... +
+ )} +
+
+ + {/* Quick Tactical Links */} +
+ +
+
+ +
+
+
+ Voice Matrix +
+
+ Realtime speaker stream +
+
+
+ + + + +
+
+ +
+
+
+ Chat Log Stream +
+
+ Live telemetry feed +
+
+
+ + + + +
+
+ +
+
+
+ Auto-Mod Radar +
+
+ Classification & audits +
+
+
+ + +
-
+ + {/* Side Telemetry Panel: Health, Dials, & Nodes */} +
+ {/* Safety & Moderation Integrity Gauge */} + +
+ INTEGRITY MATRIX +
+
+ Clean Stream Ratio +
+
+ 0.9 ? "signal" : "amber"} + label={`${Math.round(cleanRatio * 100)}%`} + sublabel="CLEAN RATIO" + /> +
+
+
+ Clean Messages: + + {formatNumber(s.total_clean)} + +
+
+ Pending Flags: + + {formatNumber(s.total_flagged)} + +
+
+
+ + {/* Top Reacted Messages / Emitters */} + +
+
+
+ FREQUENT EMITTERS +
+
+ Top Reacted Messages +
+
+ +
+ +
+ {reactions && reactions.length > 0 ? ( + reactions.slice(0, 4).map((r) => ( +
+ + {r.username ? `@${r.username}` : "anonymous"} + +
+ reacts: + + {r.reaction_count} + +
+
+ )) + ) : ( +
+ NO EMITTER TELEMETRY +
+ )} +
+
+
+
-
- ); -} - -function Row({ - icon, - label, - value, -}: { - icon: React.ReactNode; - label: string; - value: string; -}) { - return ( -
- {icon} - {label} - {value} -
- ); -} - -function Mini({ - label, - value, - tone, -}: { - label: string; - value: number; - tone: "signal" | "amber" | "vermilion"; -}) { - const color = - tone === "vermilion" - ? "text-vermilion" - : tone === "amber" - ? "text-amber" - : "text-signal"; - return ( -
-
{value}
-
{label}
-
- ); -} - -function EmptyHint() { - return ( -
- Awaiting data… -
+ ); } diff --git a/services/frontend/src/components/shell/nav-rail.tsx b/services/frontend/src/components/shell/nav-rail.tsx index ec752bb0..e4518577 100644 --- a/services/frontend/src/components/shell/nav-rail.tsx +++ b/services/frontend/src/components/shell/nav-rail.tsx @@ -1,9 +1,16 @@ "use client"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; import { usePathname } from "next/navigation"; +import { useRef } from "react"; import { isActivePath, navItems } from "@/lib/navigation"; import { cn } from "@/lib/utils"; +if (typeof window !== "undefined") { + gsap.registerPlugin(useGSAP); +} + function NavItem({ href, label, @@ -24,10 +31,10 @@ function NavItem({ aria-current={active ? "page" : undefined} style={{ "--i": index } as React.CSSProperties} className={cn( - "game-nav-item group flex size-11 items-center justify-center rounded-[13px] transition-colors", + "nav-dock-item game-nav-item group relative flex size-11 items-center justify-center rounded-[13px] transition-all duration-200", active - ? "is-active bg-white/10 text-white" - : "text-ink-faint hover:bg-white/5 hover:text-ink-soft", + ? "is-active bg-white/10 text-white shadow-sm ring-1 ring-white/20" + : "text-ink-faint hover:bg-white/5 hover:text-ink-soft hover:scale-105", )} > {active && } @@ -38,8 +45,9 @@ function NavItem({ className="relative z-10 size-[18px]" strokeWidth={active ? 2.4 : 2} /> - {/* hover tooltip — labels are hidden in the rail, so surface on hover */} - + {/* HUD Tooltip on hover */} + + {label} @@ -49,10 +57,35 @@ function NavItem({ export function NavRail() { const pathname = usePathname(); const path = pathname ?? "/"; + const railRef = useRef(null); + + useGSAP( + () => { + if (!railRef.current) return; + const items = railRef.current.querySelectorAll(".nav-dock-item"); + gsap.fromTo( + items, + { opacity: 0, x: -8, scale: 0.9 }, + { + opacity: 1, + x: 0, + scale: 1, + duration: 0.4, + stagger: 0.04, + ease: "power2.out", + clearProps: "transform", + }, + ); + }, + { scope: railRef }, + ); return ( -