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,63 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun, Wifi, WifiOff } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useWebSocket } from "@/lib/ws/context";
|
||||
|
||||
export function Header() {
|
||||
const { status } = useWebSocket();
|
||||
const [theme, setTheme] = useState<"light" | "dark">("dark");
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem("theme") as "light" | "dark" | null;
|
||||
if (stored) setTheme(stored);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const next = theme === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
localStorage.setItem("theme", next);
|
||||
document.documentElement.classList.remove("light", "dark");
|
||||
document.documentElement.classList.add(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 md:px-6">
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Connection status */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{status === "connected" ? (
|
||||
<>
|
||||
<Wifi className="size-3 text-green-500" />
|
||||
<span className="hidden sm:inline">Connected</span>
|
||||
</>
|
||||
) : status === "connecting" ? (
|
||||
<>
|
||||
<Wifi className="size-3 text-yellow-500" />
|
||||
<span className="hidden sm:inline">Connecting</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="size-3 text-destructive" />
|
||||
<span className="hidden sm:inline">Disconnected</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex size-8 items-center justify-center rounded-lg border hover:bg-muted transition-colors"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="size-4" />
|
||||
) : (
|
||||
<Moon className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { LayoutDashboard, MessageSquare, Radio } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const tabs = [
|
||||
{ id: "messages", label: "Messages", icon: MessageSquare },
|
||||
{ id: "live", label: "Live", icon: Radio },
|
||||
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof tabs)[number]["id"];
|
||||
|
||||
export function MobileTabBar({ activeTab }: { activeTab: TabId }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<nav className="md:hidden fixed bottom-0 inset-x-0 z-10 border-t bg-background">
|
||||
<div className="flex">
|
||||
{tabs.map(({ id, label, icon: Icon }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => router.push(`/dashboard?tab=${id}`)}
|
||||
data-active={activeTab === id ? "" : undefined}
|
||||
className="flex-1 flex flex-col items-center gap-0.5 py-2 text-xs font-medium text-muted-foreground data-[active]:text-primary transition-colors"
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { LayoutDashboard, LogOut, MessageSquare, Radio } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/hooks/use-auth";
|
||||
|
||||
const tabs = [
|
||||
{ id: "messages", label: "Messages", icon: MessageSquare },
|
||||
{ id: "live", label: "Live", icon: Radio },
|
||||
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof tabs)[number]["id"];
|
||||
|
||||
export function Sidebar({ activeTab }: { activeTab: TabId }) {
|
||||
const { logout } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleTabClick = (tabId: TabId) => {
|
||||
router.push(`/dashboard?tab=${tabId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="hidden md:flex md:w-56 md:flex-col md:fixed md:inset-y-0 border-r bg-sidebar">
|
||||
<div className="flex h-14 items-center gap-2 border-b px-4">
|
||||
<div className="size-7 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Radio className="size-4 text-primary" />
|
||||
</div>
|
||||
<span className="font-semibold text-sm">Bete</span>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 p-3">
|
||||
{tabs.map(({ id, label, icon: Icon }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => handleTabClick(id)}
|
||||
data-active={activeTab === id ? "" : undefined}
|
||||
className="w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[active]:bg-sidebar-accent data-[active]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Icon className="size-4 shrink-0" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="border-t p-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={logout}
|
||||
className="w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||
>
|
||||
<LogOut className="size-4 shrink-0" />
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user