Ground-up rebuild of the GMW frontend as an Ambient Field console: - WebGL ambient background (Three.js shader, drifting motes, reduced-motion aware) - Glassmorphism dark cyber theme across all 8 routes - SSR page + client view split with SWR fallback; realtime via WebSocket - Command palette (Cmd+K), chatbot FAB, guild/channel pickers - Chart primitives: donut, radial-gauge, area-activity, sparkline, equalizer Cleanup (review pass): - Remove stray Puppeteer nav-test/nav-debug scripts - Replace non-null assertions with guards (dashboard/moderation) - Drop unused useGuilds fetches in messages/voice views - Type implicit-any `let` declarations across pages - Add a11y roles/labels to SVG charts and audio, tidy imports
71 lines
1.8 KiB
TypeScript
71 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Select, type SelectOption } from "@/components/primitives";
|
|
import { useGuilds, useTextChannels, useVoiceChannels } from "@/hooks";
|
|
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>
|
|
);
|
|
}
|