feat: rewrite settings page with glass cards and sub-nav tabs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Developer
2026-07-28 11:35:28 +07:00
co-authored by Claude Opus 4.8
parent 5033161567
commit 2b06d82450
@@ -2,18 +2,21 @@
import { Moon, Server, Shield, Sun, Wifi } from "lucide-react"; import { Moon, Server, Shield, Sun, Wifi } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { GlassDivider } from "@/components/glass/divider";
import { SubNav } from "@/components/layout/sub-nav";
import { LoadingSkeleton } from "@/components/shared"; import { LoadingSkeleton } from "@/components/shared";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { useConfig } from "@/hooks"; import { useConfig } from "@/hooks";
import { cn } from "@/lib/utils";
import { useWebSocket } from "@/lib/ws/context"; import { useWebSocket } from "@/lib/ws/context";
import { cn } from "@/lib/utils";
type SettingsTab = "connection" | "appearance" | "config" | "about";
export default function SettingsPage() { export default function SettingsPage() {
const { status } = useWebSocket(); const { status } = useWebSocket();
const { data: config, isLoading: configLoading } = useConfig(); const { data: config, isLoading: configLoading } = useConfig();
const [theme, setTheme] = useState<"light" | "dark">("dark"); const [theme, setTheme] = useState<"light" | "dark">("dark");
const [tab, setTab] = useState<SettingsTab>("connection");
useEffect(() => { useEffect(() => {
const stored = localStorage.getItem("theme") as "light" | "dark" | null; const stored = localStorage.getItem("theme") as "light" | "dark" | null;
@@ -28,147 +31,111 @@ export default function SettingsPage() {
document.documentElement.classList.add(next); document.documentElement.classList.add(next);
}; };
const statusCfg = { const statusDot = {
connected: { connected: "bg-emerald-500 shadow-[0_0_8px] shadow-emerald-500/60 animate-pulse",
label: "Connected", connecting: "bg-accent-amber animate-pulse",
variant: "default" as const, disconnected: "bg-destructive",
dot: "bg-green-500 shadow-[0_0_6px] shadow-green-500/60", error: "bg-destructive",
}, }[status];
connecting: {
label: "Connecting", const statusLabel = {
variant: "secondary" as const, connected: "Connected",
dot: "bg-yellow-500 animate-pulse", connecting: "Connecting",
}, disconnected: "Disconnected",
disconnected: { error: "Error",
label: "Disconnected",
variant: "destructive" as const,
dot: "bg-destructive",
},
error: {
label: "Error",
variant: "destructive" as const,
dot: "bg-destructive",
},
}[status]; }[status];
return ( return (
<div className="space-y-5 animate-fade-in-up max-w-2xl"> <div className="space-y-4 animate-fade-in-up max-w-2xl">
<Card> <SubNav
<CardHeader> tabs={[
<CardTitle className="flex items-center gap-2 text-sm font-semibold"> { id: "connection", label: "Connection", icon: <Wifi className="size-3" /> },
<Wifi className="size-4 text-primary" /> Connection { id: "appearance", label: "Appearance", icon: <Sun className="size-3" /> },
</CardTitle> { id: "config", label: "Config", icon: <Server className="size-3" /> },
</CardHeader> { id: "about", label: "About", icon: <Shield className="size-3" /> },
<CardContent className="space-y-3"> ]}
<div className="flex items-center justify-between"> activeTab={tab}
<span className="text-sm">WebSocket</span> onTabChange={(t) => setTab(t as SettingsTab)}
<Badge variant={statusCfg.variant} className="gap-1.5 px-2.5 py-1"> />
<span className={cn("size-1.5 rounded-full", statusCfg.dot)} />
{statusCfg.label}
</Badge>
</div>
</CardContent>
</Card>
<Card> {tab === "connection" && (
<CardHeader> <GlassCard variant="base">
<CardTitle className="flex items-center gap-2 text-sm font-semibold"> <div className="flex items-center justify-between">
{theme === "dark" ? ( <div>
<Moon className="size-4 text-primary" /> <span className="text-sm font-semibold text-text-primary">WebSocket</span>
) : ( </div>
<Sun className="size-4 text-primary" /> <div className="flex items-center gap-2">
)}{" "} <span className={cn("size-2 rounded-full", statusDot)} />
Appearance <span className="text-xs font-mono text-text-secondary">{statusLabel}</span>
</CardTitle> </div>
</CardHeader> </div>
<CardContent> </GlassCard>
)}
{tab === "appearance" && (
<GlassCard variant="base">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{theme === "dark" ? <Moon className="size-4 text-primary" /> : <Sun className="size-4 text-primary" />}
<span className="text-sm font-semibold text-text-primary">Theme</span>
</div>
<button <button
type="button" type="button"
onClick={toggleTheme} onClick={toggleTheme}
className="flex items-center justify-between w-full text-sm cursor-pointer" className="flex items-center gap-2 px-3 py-1.5 rounded-md glass hover:glass-elevated transition-all text-xs"
> >
<span>Theme</span> <span className="font-mono">{theme}</span>
<Badge variant="outline" className="capitalize">
{theme}
</Badge>
</button> </button>
</CardContent> </div>
</Card> </GlassCard>
)}
<Card> {tab === "config" && (
<CardHeader> <GlassCard variant="base">
<CardTitle className="flex items-center gap-2 text-sm font-semibold"> <div className="space-y-3">
<Server className="size-4 text-primary" /> Server Configuration
</CardTitle>
</CardHeader>
<CardContent>
{configLoading ? ( {configLoading ? (
<LoadingSkeleton count={6} height="h-6" /> <LoadingSkeleton count={6} height="h-6" />
) : config ? ( ) : config ? (
<div className="space-y-2 text-sm"> <>
<CfgRow <ConfigRow label="Monitor Guild" value={config.monitorGuildId || "Not configured"} />
label="Monitor Guild" <GlassDivider />
value={config.monitorGuildId ?? "Not configured"} <ConfigRow label="Voice Guild" value={config.voiceGuildId || "Not configured"} />
/> <GlassDivider />
<Separator /> <ConfigRow label="Voice Channel" value={config.voiceChannelId || "Not configured"} />
<CfgRow <GlassDivider />
label="Voice Guild" <ConfigRow label="AI Analysis" value={config.aiAnalysisEnabled ? "Enabled" : "Disabled"} />
value={config.voiceGuildId ?? "Not configured"} <GlassDivider />
/> <ConfigRow label="Auto-Delete Flagged" value={config.autoDeleteFlaggedEnabled ? "Enabled" : "Disabled"} />
<Separator /> </>
<CfgRow
label="Voice Channel"
value={config.voiceChannelId ?? "Not configured"}
/>
<Separator />
<CfgRow
label="AI Analysis"
value={config.aiAnalysisEnabled ? "Enabled" : "Disabled"}
/>
<Separator />
<CfgRow
label="Auto-Delete Flagged"
value={config.autoDeleteFlaggedEnabled ? "Enabled" : "Disabled"}
/>
</div>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-xs text-text-secondary/60">Unable to load config.</p>
Unable to load configuration.
</p>
)} )}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
<Shield className="size-4 text-primary" /> About
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm space-y-1">
<p>
<span className="text-gradient font-bold">Discord Automod</span>
Discord Moderation Watcher
</p>
<p className="text-xs text-muted-foreground">
AI-powered message moderation, voice recording, and real-time
monitoring for Discord communities.
</p>
</div> </div>
</CardContent> </GlassCard>
</Card> )}
{tab === "about" && (
<GlassCard variant="base">
<div className="space-y-2">
<h2 className="text-base font-bold text-primary">Discord Automod</h2>
<p className="text-xs text-text-secondary/80 leading-relaxed">
AI-powered message moderation, voice recording, and real-time monitoring for Discord communities.
</p>
<div className="text-[10px] font-mono text-text-secondary/40 mt-4">
v0.1.0
</div>
</div>
</GlassCard>
)}
</div> </div>
); );
} }
function CfgRow({ label, value }: { label: string; value: string }) { function ConfigRow({ label, value }: { label: string; value: string }) {
return ( return (
<div className="flex items-center justify-between py-1"> <div className="flex items-center justify-between py-0.5">
<span className="text-muted-foreground">{label}</span> <span className="text-xs text-text-secondary">{label}</span>
<span className="font-mono text-xs max-w-[280px] truncate text-right"> <span className="text-[11px] font-mono text-text-primary/80 max-w-[240px] truncate text-right">{value}</span>
{value}
</span>
</div> </div>
); );
} }