feat: migrate frontend to Astro SSG with design system

- Replace monolithic React SPA (App.tsx) with 7 Astro pages (+ routing)
- Implement full design system from design/ (OKLCH tokens, Tailwind 4 @theme)
- Add 12 Astro components (Button, Badge, Card, Sidebar, Header, states)
- Add 11 React islands (AuthGuard, MessageFeed, VoiceControls, AudioVisualizer, etc.)
- Add 3 Zustand stores (UI, voice, message state)
- Add CSS architecture: dark/light themes, glassmorphism, fluid typography, animations
- Add 404, login, settings, recordings pages
- Remove 40+ legacy React SPA files
- Add Particles background and MascotChat deferred islands
- Wire WebSocket bridge for real-time messages and voice events

Build: 7 pages in ~900ms
Typecheck: clean (0 errors)
Lint: clean (0 errors)
This commit is contained in:
asepharyana
2026-07-02 01:18:45 +07:00
parent 5e40b1ad8c
commit 8ad888da28
110 changed files with 3396 additions and 4980 deletions
@@ -1,126 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import type { DashboardStats } from "../../../entities/dashboard/types.js";
import {
getDashboardChannelDetail,
getDashboardStats,
getDashboardUserDetail,
listDashboardChannels,
listDashboardUsers,
} from "../../../shared/api/client.js";
import { useItemDetail } from "../../../shared/hooks/useItemDetail";
import { usePaginatedList } from "../../../shared/hooks/usePaginatedList";
import { createLogger } from "../../../shared/lib/logger.js";
const logger = createLogger("use-dashboard");
/**
* Fetch dashboard aggregate stats.
*/
export function useDashboardStats() {
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 getDashboardStats();
setStats(data);
} catch (e) {
const msg = e instanceof Error ? e.message : "Failed to load stats";
setError(msg);
logger.error("[useDashboardStats]", { error: msg });
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetch().catch(() => undefined);
}, [fetch]);
return { stats, loading, error, refetch: fetch };
}
/**
* Fetch paginated user list with optional search.
*/
export function useDashboardUsers() {
const paginated = usePaginatedList(
(params) =>
listDashboardUsers({
limit: params.limit,
search: params.search,
cursor: params.cursor,
}).then((r) => ({ data: r.data, nextCursor: r.nextCursor })),
"",
);
return {
users: paginated.data,
loading: paginated.loading,
error: paginated.error,
search: paginated.search,
setSearch: paginated.setSearch,
loadMore: paginated.loadMore,
hasMore: paginated.hasMore,
refetch: paginated.refetch,
};
}
/**
* Fetch a single user detail by userId.
*/
export function useDashboardUserDetail(userId: string | null) {
const { data, loading, error, refetch } = useItemDetail(
(_guildId, entityId) => getDashboardUserDetail(entityId),
"",
userId,
"user",
);
return { detail: data, loading, error, refetch };
}
/**
* Fetch paginated channel list with optional search.
*/
export function useDashboardChannels() {
const paginated = usePaginatedList(
(params) =>
listDashboardChannels({
limit: params.limit,
search: params.search,
guild_id: params.guildId,
cursor: params.cursor,
}).then((r) => ({ data: r.data, nextCursor: r.nextCursor })),
"",
);
return {
channels: paginated.data,
loading: paginated.loading,
error: paginated.error,
search: paginated.search,
setSearch: paginated.setSearch,
loadMore: paginated.loadMore,
hasMore: paginated.hasMore,
refetch: paginated.refetch,
};
}
/**
* Fetch a single channel detail by channelId.
*/
export function useDashboardChannelDetail(channelId: string | null) {
const { data, loading, error, refetch } = useItemDetail(
(_guildId, entityId) => getDashboardChannelDetail(entityId),
"",
channelId,
"channel",
);
return { detail: data, loading, error, refetch };
}