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
@@ -0,0 +1,169 @@
import { useCallback, useState } from "react";
import { dashboardApi } from "@/lib/api";
import type {
DashboardChannel,
DashboardChannelDetail,
DashboardStats,
DashboardUser,
DashboardUserDetail,
} from "@/lib/types";
// ── Stats ───────────────────────────────────────
interface UseStatsReturn {
stats: DashboardStats | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
export function useStats(): UseStatsReturn {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetch = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await dashboardApi.getStats();
setStats(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load stats");
} finally {
setLoading(false);
}
}, []);
return { stats, loading, error, refetch: fetch };
}
// ── Users ───────────────────────────────────────
interface UseUsersReturn {
users: DashboardUser[];
loading: boolean;
search: string;
setSearch: (q: string) => void;
refetch: () => void;
}
export function useUsers(): UseUsersReturn {
const [users, setUsers] = useState<DashboardUser[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const fetch = useCallback(async (q?: string) => {
setLoading(true);
try {
const result = await dashboardApi.listUsers(20, undefined, q);
setUsers(result.data);
} catch {
// silently fail
} finally {
setLoading(false);
}
}, []);
const fetchWithSearch = useCallback(() => {
fetch(search || undefined);
}, [fetch, search]);
return {
users,
loading,
search,
setSearch,
refetch: fetchWithSearch,
};
}
// ── Channels ────────────────────────────────────
interface UseChannelsReturn {
channels: DashboardChannel[];
loading: boolean;
search: string;
setSearch: (q: string) => void;
refetch: () => void;
}
export function useChannels(guildId: string): UseChannelsReturn {
const [channels, setChannels] = useState<DashboardChannel[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const fetch = useCallback(
async (q?: string) => {
setLoading(true);
try {
const result = await dashboardApi.listChannels(
20,
q,
guildId || undefined,
);
setChannels(result.data);
} catch {
// silently fail
} finally {
setLoading(false);
}
},
[guildId],
);
const fetchWithSearch = useCallback(() => {
fetch(search || undefined);
}, [fetch, search]);
return {
channels,
loading,
search,
setSearch,
refetch: fetchWithSearch,
};
}
// ── User Detail ─────────────────────────────────
export function useUserDetail() {
const [user, setUser] = useState<DashboardUserDetail | null>(null);
const [loading, setLoading] = useState(false);
const fetch = useCallback(async (userId: string) => {
setLoading(true);
try {
const detail = await dashboardApi.getUserDetail(userId);
setUser(detail);
} catch {
// ignore
} finally {
setLoading(false);
}
}, []);
return { user, loading, fetch };
}
// ── Channel Detail ──────────────────────────────
export function useChannelDetail() {
const [channel, setChannel] = useState<DashboardChannelDetail | null>(null);
const [loading, setLoading] = useState(false);
const fetch = useCallback(async (channelId: string) => {
setLoading(true);
try {
const detail = await dashboardApi.getChannelDetail(channelId);
setChannel(detail);
} catch {
// ignore
} finally {
setLoading(false);
}
}, []);
return { channel, loading, fetch };
}