feat: add app header, sidebar, and mobile navigation components
Deploy to VPS / deploy (push) Failing after 1m45s
Deploy to VPS / deploy (push) Failing after 1m45s
- Implemented AppHeader component with theme toggle and connection status. - Created AppSidebar component for navigation with connection status indicator. - Added MobileNav component for mobile navigation with responsive design. - Introduced shared components: DetailStat, EmptyState, ErrorState, LoadingSkeleton, and StatCard for consistent UI. - Developed hooks for async data fetching: useAsync, useConfig, useDashboard, useGuilds, useMedia, useMessages, useRecordings, and useVoice. - Added chatbot API functions for sending messages and managing chat history.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { voiceApi } from "@/lib/api";
|
||||
import type { ActiveSpeaker, VoiceStatus } from "@/lib/types";
|
||||
import type { WsEventType } from "@/lib/ws/types";
|
||||
|
||||
type WsHook = {
|
||||
on: <E extends WsEventType>(
|
||||
eventType: E,
|
||||
handler: (data: unknown) => void,
|
||||
) => () => void;
|
||||
};
|
||||
|
||||
interface UseVoiceStatusReturn {
|
||||
voiceStatus: VoiceStatus | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
export function useVoiceStatus(): UseVoiceStatusReturn {
|
||||
const [voiceStatus, setVoiceStatus] = useState<VoiceStatus | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const status = await voiceApi.getStatus();
|
||||
setVoiceStatus(status);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { voiceStatus, refresh };
|
||||
}
|
||||
|
||||
interface UseVoiceChannelsReturn {
|
||||
channels: Array<{ id: string; name: string }>;
|
||||
loading: boolean;
|
||||
fetch: (guildId: string) => void;
|
||||
}
|
||||
|
||||
export function useVoiceChannels(): UseVoiceChannelsReturn {
|
||||
const [channels, setChannels] = useState<Array<{ id: string; name: string }>>(
|
||||
[],
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetch = useCallback(async (guildId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const ch = await voiceApi.getVoiceChannels(guildId);
|
||||
setChannels(ch);
|
||||
} catch {
|
||||
setChannels([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { channels, loading, fetch };
|
||||
}
|
||||
|
||||
interface UseSpeakersReturn {
|
||||
speakers: ActiveSpeaker[];
|
||||
subscribe: (ws: WsHook) => () => void;
|
||||
}
|
||||
|
||||
export function useSpeakers(): UseSpeakersReturn {
|
||||
const [speakers, setSpeakers] = useState<ActiveSpeaker[]>([]);
|
||||
|
||||
const subscribe = useCallback((ws: WsHook) => {
|
||||
const unsub = ws.on("voice_active_user", (data) => {
|
||||
const speaker = data as ActiveSpeaker;
|
||||
setSpeakers((prev) => {
|
||||
const idx = prev.findIndex((s) => s.userId === speaker.userId);
|
||||
if (idx >= 0) {
|
||||
const next = [...prev];
|
||||
next[idx] = speaker;
|
||||
return next;
|
||||
}
|
||||
return [...prev, speaker];
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
setSpeakers([]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { speakers, subscribe };
|
||||
}
|
||||
Reference in New Issue
Block a user