From c7abe39728aaff6c21f1f8ea342191013347a97f Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sat, 30 May 2026 16:31:27 +0700 Subject: [PATCH] refactor: remove Review tab, make UI state client-side, fix Jakarta time in analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Review tab completely (was redundant with Messages flagged view) - UI state now client-side only (localStorage) — no server API calls for tab/channel/guild selection - Fixes dashboard crash when server is down — now loads fully client-side - Live panel still uses server API for voice/media operations (only what needs it) - Analytics hourly chart labels now show Jakarta time (WIB/UTC+7) instead of UTC - Analytics formatTimeAgo uses Jakarta time reference - Reduced tabs to 3: Live, Messages, Analytics - Removed unused uiState API imports and server-side state fetching Co-Authored-By: Claude Opus 4.8 --- frontend/src/App.tsx | 14 ++--- .../components/analytics/AnalyticsPanel.tsx | 13 ++++- frontend/src/components/layout/Header.tsx | 2 - frontend/src/components/layout/Sidebar.tsx | 3 +- frontend/src/hooks/useUIState.ts | 57 ++++++++++--------- frontend/src/types/ui.ts | 2 +- 6 files changed, 46 insertions(+), 45 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 592aff1..78f0074 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DashboardLayout } from "./components/layout/DashboardLayout"; import { LivePanel } from "./components/live/LivePanel"; import { MessagesPanel } from "./components/messages/MessagesPanel"; -import { ReviewPanel } from "./components/review/ReviewPanel"; import { Tabs, TabsContent } from "./components/ui/tabs"; import { AnalyticsPanel } from "./components/analytics/AnalyticsPanel"; import { AuthOverlay } from "./components/layout/AuthOverlay"; @@ -122,8 +121,8 @@ export default function App() { }, [socket.socketRef]); const toggleStreaming = useCallback(async () => { - if (isStreaming) { stopStreamingLocal(); await patchUIState({ isStreaming: false }); } - else { await startStreamingLocal(); await patchUIState({ isStreaming: true }); } + if (isStreaming) { stopStreamingLocal(); patchUIState({ isStreaming: false }); } + else { await startStreamingLocal(); patchUIState({ isStreaming: true }); } }, [isStreaming, startStreamingLocal, stopStreamingLocal, patchUIState]); useEffect(() => { if (selectedVoiceGuild) voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); }, [selectedVoiceGuild]); @@ -131,15 +130,15 @@ export default function App() { useEffect(() => { if (selectedTextChannel) messages.fetchMessages(selectedTextChannel).catch(() => undefined); }, [selectedTextChannel]); const toggleListening = useCallback(async () => { - if (isListening) { await audioContextListenRef.current?.suspend(); userTimelinesRef.current.clear(); setIsListening(false); await patchUIState({ isListening: false }); return; } + if (isListening) { await audioContextListenRef.current?.suspend(); userTimelinesRef.current.clear(); setIsListening(false); patchUIState({ isListening: false }); return; } const AudioContextCtor = window.AudioContext || window.webkitAudioContext; audioContextListenRef.current ??= new AudioContextCtor({ sampleRate: SAMPLE_RATE }); await audioContextListenRef.current.resume(); setIsListening(true); - await patchUIState({ isListening: true }); + patchUIState({ isListening: true }); }, [isListening, patchUIState]); - const tabs = useMemo(() => ["live", "messages", "analytics", "review"] as DashboardTab[], []); + const tabs = useMemo(() => ["live", "messages", "analytics"] as DashboardTab[], []); return ( patchUIState({ selectedTextChannel: channelId })} /> - - - ); diff --git a/frontend/src/components/analytics/AnalyticsPanel.tsx b/frontend/src/components/analytics/AnalyticsPanel.tsx index 6e893cc..b945d96 100644 --- a/frontend/src/components/analytics/AnalyticsPanel.tsx +++ b/frontend/src/components/analytics/AnalyticsPanel.tsx @@ -499,7 +499,12 @@ function HourlyChart({ hourly, loading }: { hourly: HourlyBucket[] | undefined; } const maxCount = Math.max(...hourly.map((b) => b.count), 1); - const labels = hourly.map((b) => b.hour.slice(11, 16)); + // Convert UTC hour buckets to Jakarta time (UTC+7) + const labels = hourly.map((b) => { + const utcHour = parseInt(b.hour.slice(11, 13), 10); + const jakartaHour = (utcHour + 7) % 24; + return `${String(jakartaHour).padStart(2, "0")}:00`; + }); return (
@@ -553,7 +558,7 @@ function HourlyChart({ hourly, loading }: { hourly: HourlyBucket[] | undefined;
{/* Hover tooltip */}
- {bucket.hour.slice(11, 16)} — {bucket.count} msgs + {labels[hourly.indexOf(bucket)]} — {bucket.count} msgs
); @@ -987,7 +992,9 @@ function pct(part: number, total: number): number { } function formatTimeAgo(ts: number): string { - const diff = Date.now() - ts; + // Use Jakarta time as reference for "ago" calculations + const jakartaNow = new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Jakarta" })); + const diff = jakartaNow.getTime() - ts; const minutes = Math.floor(diff / 60000); if (minutes < 1) return "baru saja"; if (minutes < 60) return `${minutes}m lalu`; diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index d41955b..39ce5ab 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -8,14 +8,12 @@ const titles: Record = { live: "Voice, Media & Recordings", messages: "Messages & Moderation", analytics: "Analytics & Insights", - review: "Moderation Review", }; const subtitles: Record = { live: "Join voice channels, play media, stream audio, and browse recordings.", messages: "Capture, analyse, and moderate Discord messages.", analytics: "Server moderation statistics and trends.", - review: "Review AI-flagged messages for moderation.", }; interface HeaderProps { diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index ff46576..9f2d406 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,4 +1,4 @@ -import { Bot, BarChart3, MessageSquare, ShieldAlert, Radio } from "lucide-react"; +import { Bot, BarChart3, MessageSquare, Radio } from "lucide-react"; import type { DashboardTab } from "../../types/ui"; import { cn } from "../../lib/utils"; import { Button } from "../ui/button"; @@ -7,7 +7,6 @@ const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Radio }> = { id: "live", label: "Live", icon: Radio }, { id: "messages", label: "Messages", icon: MessageSquare }, { id: "analytics", label: "Analytics", icon: BarChart3 }, - { id: "review", label: "Review", icon: ShieldAlert }, ]; interface SidebarProps { diff --git a/frontend/src/hooks/useUIState.ts b/frontend/src/hooks/useUIState.ts index eee2ece..016ea9a 100644 --- a/frontend/src/hooks/useUIState.ts +++ b/frontend/src/hooks/useUIState.ts @@ -1,35 +1,36 @@ -import { useCallback, useEffect, useState } from "react"; -import { getUIState, updateUIState } from "../api/uiState"; +import { useCallback, useState } from "react"; import type { UIState } from "../types/ui"; -export function useUIState() { - const [uiState, setUIState] = useState({ activeTab: "live" }); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +const STORAGE_KEY = "bete-dashboard-ui-state"; - useEffect(() => { - let cancelled = false; - getUIState() - .then((state) => { - if (!cancelled) setUIState({ activeTab: "live", ...state }); - }) - .catch((err) => { - if (!cancelled) setError(err instanceof Error ? err.message : String(err)); - }) - .finally(() => { - if (!cancelled) setLoading(false); - }); - return () => { - cancelled = true; - }; - }, []); +function loadState(): UIState { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) return JSON.parse(raw) as UIState; + } catch { + // ignore parse errors + } + return { activeTab: "live" }; +} + +function saveState(state: UIState): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch { + // ignore quota errors + } +} + +export function useUIState() { + const [uiState, setUIState] = useState(loadState); - const patchUIState = useCallback(async (patch: Partial) => { - setUIState((prev) => ({ ...prev, ...patch })); - const next = await updateUIState(patch); - setUIState((prev) => ({ ...prev, ...next })); - return next; + const patchUIState = useCallback((patch: Partial) => { + setUIState((prev) => { + const next = { ...prev, ...patch }; + saveState(next); + return next; + }); }, []); - return { uiState, setUIState, patchUIState, loading, error }; + return { uiState, setUIState, patchUIState, loading: false, error: null }; } diff --git a/frontend/src/types/ui.ts b/frontend/src/types/ui.ts index 46373e2..82f5705 100644 --- a/frontend/src/types/ui.ts +++ b/frontend/src/types/ui.ts @@ -1,4 +1,4 @@ -export type DashboardTab = "live" | "messages" | "review" | "analytics"; +export type DashboardTab = "live" | "messages" | "analytics"; export interface UIState { selectedGuild?: string;