feat: migrate Leptos frontend to Next.js 16 (React 19)
Deploy to VPS / deploy (push) Failing after 42s
Deploy to VPS / deploy (push) Failing after 42s
Complete migration from services/frontend.old/ (Leptos 0.7 WASM + Rust) to services/frontend/ (Next.js 16 static export + TypeScript + Tailwind v4). Summary: - Port all shared types (message, guild, voice, media, dashboard, recording, ui) - Build fetch-based API client covering all 30+ backend endpoints - WebSocket client with auto-reconnect (exponential backoff, 20 attempts) - React context provider for WS with typed event subscription (22 event types) - Login page with localStorage auth + auto-redirect - Dashboard layout with sidebar, header (WS status + theme toggle) - Messages: feed, search, images tab, review tab, channel filter, detail modal - Live: voice connection, music player, recordings, mic transmit, active speakers - Dashboard: stats, user list, channel list, detail views - Mascot chatbot with history + clear - uiStateApi persistence for selected tab - Add static export config, update deploy scripts and CI
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { uiStateApi } from "@/lib/api";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import { MobileTabBar } from "@/components/layout/mobile-tab-bar";
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { MascotChatbot } from "@/features/mascot/mascot-chatbot";
|
||||
import { AuthProvider, useAuth } from "@/lib/hooks/use-auth";
|
||||
import { WsProvider } from "@/lib/ws/context";
|
||||
|
||||
function DashboardGuard({ children }: { children: React.ReactNode }) {
|
||||
const { authenticated, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !authenticated) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [authenticated, loading, router]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authenticated) return null;
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function DashboardShell({ children }: { children: React.ReactNode }) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const restored = useRef(false);
|
||||
|
||||
const activeTab = (searchParams.get("tab") ?? "messages") as
|
||||
| "messages"
|
||||
| "live"
|
||||
| "dashboard";
|
||||
|
||||
// Restore persisted tab on mount (only if no explicit tab in URL)
|
||||
useEffect(() => {
|
||||
if (restored.current) return;
|
||||
const tabParam = searchParams.get("tab");
|
||||
if (tabParam) {
|
||||
restored.current = true;
|
||||
return; // explicit tab in URL — don't override
|
||||
}
|
||||
uiStateApi
|
||||
.get()
|
||||
.then((state) => {
|
||||
restored.current = true;
|
||||
const savedTab = state.active_tab;
|
||||
if (savedTab && savedTab !== activeTab) {
|
||||
router.replace(`/dashboard?tab=${savedTab}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
restored.current = true;
|
||||
});
|
||||
}, [searchParams, activeTab, router]);
|
||||
|
||||
// Persist tab changes
|
||||
useEffect(() => {
|
||||
if (!restored.current) return;
|
||||
uiStateApi.save({ active_tab: activeTab }).catch(() => {});
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Sidebar activeTab={activeTab} />
|
||||
<div className="md:pl-56 flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-1 p-4 md:p-6 pb-20 md:pb-6">{children}</main>
|
||||
</div>
|
||||
<MobileTabBar activeTab={activeTab} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<DashboardGuard>
|
||||
<WsProvider>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DashboardShell>{children}</DashboardShell>
|
||||
</Suspense>
|
||||
<MascotChatbot />
|
||||
</WsProvider>
|
||||
</DashboardGuard>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user