feat(fe): remove Live tab from dashboard

Hapus tab Live + komponennya (LiveStream, ModQueue) dari halaman
dashboard — tab Stats/Users/Channels tetap. useReview tetap dipakai
messages page (review tab), jadi hook tidak dihapus.

Verifikasi: tsc PASS, biome 0 error, next build PASS.
This commit is contained in:
asepharyana
2026-08-01 11:10:36 +07:00
parent 8e7ba67248
commit 0771e62223
3 changed files with 2 additions and 209 deletions
@@ -10,38 +10,21 @@ import {
} from "lucide-react";
import { useState } from "react";
import { ChannelsSection } from "@/components/dashboard/channels-section";
import { LiveStream } from "@/components/dashboard/live-stream";
import type { ModQueueItem } from "@/components/dashboard/mod-queue";
import { ModQueue } from "@/components/dashboard/mod-queue";
import { StatCard } from "@/components/dashboard/stat-card";
import { TopChannelsChart } from "@/components/dashboard/top-channels-chart";
import { UsersSection } from "@/components/dashboard/users-section";
import { SubNav } from "@/components/layout/sub-nav";
import { ErrorState, LoadingSkeleton } from "@/components/shared";
import { useReview, useStats } from "@/hooks";
import { useStats } from "@/hooks";
type DashboardTab = "stats" | "live" | "users" | "channels";
type DashboardTab = "stats" | "users" | "channels";
export default function DashboardPage() {
const [tab, setTab] = useState<DashboardTab>("stats");
const { data: stats, isLoading, error, mutate: refetch } = useStats();
const { data: review = [] } = useReview();
const modQueueItems: ModQueueItem[] = review.slice(0, 10).map((msg) => ({
id: msg.id,
content: msg.content || msg.id,
username: msg.username,
metadata: msg.metadata ?? null,
severity:
msg.ai_severity && msg.ai_severity !== "none"
? (msg.ai_severity as ModQueueItem["severity"])
: "medium",
reason: msg.ai_analysis ?? "AI moderation flag",
}));
const subNavTabs = [
{ id: "stats", label: "Stats", icon: <Hash className="size-3" /> },
{ id: "live", label: "Live", icon: <Sparkles className="size-3" /> },
{ id: "users", label: "Users", icon: <Users className="size-3" /> },
{ id: "channels", label: "Channels", icon: <Hash className="size-3" /> },
];
@@ -108,13 +91,6 @@ export default function DashboardPage() {
</div>
)}
{tab === "live" && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<LiveStream />
<ModQueue items={modQueueItems} />
</div>
)}
{tab === "users" && <UsersSection />}
{tab === "channels" && <ChannelsSection />}
@@ -1,97 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils";
import { useWebSocket } from "@/lib/ws/context";
interface LiveMessage {
id: string;
content: string;
username: string;
channelName?: string;
timestamp: string;
flagged?: boolean;
metadata?: string | null;
}
export function LiveStream() {
const [messages, setMessages] = useState<LiveMessage[]>([]);
const scrollRef = useRef<HTMLDivElement>(null);
const ws = useWebSocket();
useEffect(() => {
const unsub = ws.on("message_created", (data: MessageRecord) => {
// channel name lives inside the metadata JSON (channel.channelName)
let channelName: string | undefined;
try {
const meta =
typeof data.metadata === "string"
? JSON.parse(data.metadata)
: data.metadata;
channelName = meta?.channel?.channelName;
} catch {
channelName = undefined;
}
const msg: LiveMessage = {
id: data.id,
content: data.content || "(attachment)",
username: data.username || "unknown",
channelName,
timestamp: new Date().toLocaleTimeString(),
flagged: data.ai_status === "flagged" || data.ai_status === "warn",
metadata: data.metadata ?? null,
};
setMessages((prev) => [msg, ...prev].slice(0, 50));
});
return () => unsub();
}, [ws]);
return (
<GlassCard variant="base" className="p-0 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-glass-border">
<span className="relative flex size-2">
<span className="absolute inline-flex size-full rounded-full bg-emerald-500 opacity-75 animate-pulse-ring" />
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
</span>
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Live Stream
</span>
</div>
<div
ref={scrollRef}
className="overflow-y-auto max-h-[320px] space-y-0.5 p-2"
>
{messages.length === 0 ? (
<div className="flex items-center justify-center py-8 text-text-secondary/40 text-xs">
Waiting for messages...
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
className={cn(
"flex items-start gap-2 px-3 py-2 rounded-md text-sm transition-all",
msg.flagged
? "bg-destructive/5 border-l-2 border-destructive/40"
: "hover:bg-glass-bg",
)}
>
<span className="font-medium text-xs shrink-0 text-primary">
{msg.username}
</span>
<span className="text-xs text-text-secondary truncate flex-1">
{renderMessageContent(msg.content, msg.metadata)}
</span>
<span className="text-[10px] text-text-secondary/40 shrink-0">
{msg.timestamp}
</span>
</div>
))
)}
</div>
</GlassCard>
);
}
@@ -1,86 +0,0 @@
"use client";
import { AlertCircle, Check, Trash2 } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { renderMessageContent } from "@/lib/format";
import { cn } from "@/lib/utils";
export interface ModQueueItem {
id: string;
content: string;
username: string;
severity: "low" | "medium" | "high" | "critical";
reason: string;
metadata?: string | null;
}
export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) {
const severityColor = {
low: "text-accent-amber border-accent-amber/30",
medium: "text-accent-purple border-accent-purple/30",
high: "text-destructive border-destructive/40",
critical: "text-destructive border-destructive/60 bg-destructive/10",
};
return (
<GlassCard variant="base" className="p-0 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-glass-border">
<AlertCircle className="size-3.5 text-accent-purple" />
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Mod Queue
</span>
{items.length > 0 && (
<span className="ml-auto text-[10px] font-mono text-accent-amber">
{items.length} pending
</span>
)}
</div>
<div className="overflow-y-auto max-h-[320px] space-y-1 p-2">
{items.length === 0 ? (
<div className="flex items-center justify-center py-8 text-text-secondary/40 text-xs">
No flagged messages
</div>
) : (
items.map((item) => (
<div
key={item.id}
className={cn(
"px-3 py-2 rounded-md border-l-2 text-sm space-y-1 hover:bg-glass-bg transition-colors",
severityColor[item.severity],
)}
>
<div className="flex items-center gap-2">
<span className="font-medium text-xs text-text-primary">
{item.username}
</span>
<span className="text-[10px] font-mono uppercase text-text-secondary/60">
{item.severity}
</span>
</div>
<p className="text-xs text-text-secondary line-clamp-1">
{renderMessageContent(item.content, item.metadata)}
</p>
<p className="text-[10px] text-text-secondary/50">
{item.reason}
</p>
<div className="flex gap-1 pt-1">
<button
type="button"
className="size-6 flex items-center justify-center rounded bg-emerald-500/10 text-emerald-500 hover:bg-emerald-500/20 text-xs"
>
<Check className="size-3" />
</button>
<button
type="button"
className="size-6 flex items-center justify-center rounded bg-destructive/10 text-destructive hover:bg-destructive/20 text-xs"
>
<Trash2 className="size-3" />
</button>
</div>
</div>
))
)}
</div>
</GlassCard>
);
}