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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user