Files
GMW/services/frontend/src/hooks/use-dashboard.ts
T
asepharyana f20889868d feat(frontend): rebuild as SSR with server-authoritative shared state
Rombak total alur data frontend: dari static-export CSR (tiap browser
fetch sendiri + akumulasi state voice per-tab) jadi server-side rendering.

Frontend (Next.js):
- next.config: output export -> standalone; halaman jadi server components
- server data layer baru src/lib/api/server.ts (GMW_BACKEND_URL, no window)
- dashboard/media/messages/moderation/recordings/voice page -> RSC yang
  fetch backend di render-time, seed ke client view (SWR fallbackData)
- hook-hook utama terima initialData -> first paint data server, revalidate
  SWR setelahnya, tanpa spinner-blank-load
- messages: guild/channel/tab/selected dibaca dari URL di server, page awal
  di-fetch server-side

Shared realtime state (voice) server-authoritative:
- backend src/modules/voice/live-speaker.ts: agregat voice_active_user dari
  gateway jadi snapshot authoritatif (single source of truth semua browser)
- GET /api/voice/status kini include activeSpeakers
- WS initial states kirim voice_state snapshot saat connect (late join
  langsung dapat state yang sama, bukan daftar kosong)
- useSpeakers seed dari server snapshot + voice_state full-replace +
  voice_active_user delta upsert

Deploy:
- flake.nix: frontend package build SSR standalone (server.js wrapper,
  GMW_FRONTEND_PORT=4017); proxy nginx template proxy / -> Next server,
  /api + /ws tetap ke backend :4001
2026-08-07 10:44:03 +07:00

91 lines
2.2 KiB
TypeScript

import useSWR from "swr";
import { dashboardApi } from "@/lib/api";
import type {
DashboardActivity,
DashboardChannelDetail,
DashboardStats,
DashboardUserDetail,
TopReactedMessage,
TopReactor,
} from "@/lib/types";
/**
* Server-seeded SWR hooks.
*
* SSR pages fetch the initial payload on the server and hand it here as
* `initialData` — the first render is server data, and SWR takes over for
* revalidation from then on (no blank-spinner-first-load).
*/
export function useStats(initialData?: DashboardStats) {
return useSWR<DashboardStats>(
["dashboard-stats"],
() => dashboardApi.getStats(),
{ fallbackData: initialData },
);
}
export function useActivity(days = 14, initialData?: DashboardActivity) {
return useSWR<DashboardActivity>(
["dashboard-activity", days],
() => dashboardApi.getActivity(days),
{ fallbackData: initialData },
);
}
export function useUsers(search?: string) {
return useSWR(
["dashboard-users", search ?? ""],
async () => {
const res = await dashboardApi.listUsers(20, undefined, search);
return res.data;
},
{
keepPreviousData: true,
},
);
}
export function useChannels(guildId?: string, search?: string) {
return useSWR(
["dashboard-channels", guildId ?? "__all__", search ?? ""],
async () => {
const res = await dashboardApi.listChannels(
20,
search,
guildId || undefined,
);
return res.data;
},
{
keepPreviousData: true,
},
);
}
export function useUserDetail(userId: string | null) {
return useSWR<DashboardUserDetail>(
userId ? ["dashboard-user", userId] : null,
() => dashboardApi.getUserDetail(userId!),
);
}
export function useChannelDetail(channelId: string | null) {
return useSWR<DashboardChannelDetail>(
channelId ? ["dashboard-channel", channelId] : null,
() => dashboardApi.getChannelDetail(channelId!),
);
}
export function useTopReactions() {
return useSWR<TopReactedMessage[]>(["dashboard-reactions"], () =>
dashboardApi.getTopReactions(20),
);
}
export function useTopReactors() {
return useSWR<TopReactor[]>(["dashboard-reactors"], () =>
dashboardApi.getTopReactors(20),
);
}