feat(frontend): rebuild as Ambient/WebGL console with all pages + command palette

Ground-up rombak UI: hapus semua component/page lama, bangun ulang dengan
desain sistem Ambient (WebGL haze + drifting motes, signal-driven color)
di atas kontrak API/WS/type yang sudah ada.

- Design system: globals.css tokens + primitives (glass, button, badge,
  select, avatar, toast, chart SVG murni).
- Shell: nav rail, topbar (status WS + pill signal + theme), AppFrame.
- 8 halaman: dashboard, voice (orbital stage), media, messages (live feed +
  detail AI), moderation, analysis (search), recordings, + chatbot floating.
- Command palette (Cmd/Ctrl+K) untuk navigasi cepat.
- Server fetch di-page di-try/catch agar render graceful saat backend mati.

Verified: tsc clean, next build 8/8 halaman, semua route 200.
This commit is contained in:
asepharyana
2026-08-15 17:53:48 +07:00
parent b98101c576
commit 1b56212d1a
104 changed files with 2905 additions and 7048 deletions
@@ -0,0 +1,70 @@
"use client";
import { useEffect, useState } from "react";
import { useGuilds, useTextChannels, useVoiceChannels } from "@/hooks";
import { Select, type SelectOption } from "@/components/primitives";
import type { Guild } from "@/lib/types";
export function GuildChannelPicker({
mode,
guildsInitial,
guildId,
channelId,
onChange,
}: {
mode: "voice" | "text";
guildsInitial?: Guild[];
guildId: string | null;
channelId: string | null;
onChange: (guildId: string, channelId: string | null) => void;
}) {
const { data: guilds } = useGuilds(guildsInitial);
// Call both hooks unconditionally (rules of hooks); select by mode.
const voiceChannels = useVoiceChannels(guildId ?? "");
const textChannels = useTextChannels(guildId ?? "");
const channels = mode === "voice" ? voiceChannels.data : textChannels.data;
const [g, setG] = useState(guildId);
const [c, setC] = useState(channelId);
useEffect(() => setG(guildId), [guildId]);
useEffect(() => setC(channelId), [channelId]);
const guildOpts: SelectOption[] = (guilds ?? []).map((x) => ({
value: x.id,
label: x.name,
}));
const channelOpts: SelectOption[] = (channels ?? []).map((x) => ({
value: x.id,
label: x.name,
hint: x.type,
}));
return (
<div className="flex flex-wrap items-center gap-2">
<Select
value={g}
onChange={(v) => {
setG(v);
setC(null);
onChange(v, null);
}}
options={guildOpts}
placeholder="Guild"
size="sm"
className="w-44"
/>
<Select
value={c}
onChange={(v) => {
setC(v);
if (g) onChange(g, v);
}}
options={channelOpts}
placeholder={mode === "voice" ? "Voice channel" : "Text channel"}
size="sm"
className="w-52"
/>
</div>
);
}