feat: add app header, sidebar, and mobile navigation components
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:
asepharyana
2026-07-26 16:14:32 +07:00
parent 726ea8fca5
commit d5a547eb25
35 changed files with 2385 additions and 1733 deletions
+89
View File
@@ -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 };
}