feat: initial scaffold of uniweb — unibus web frontend (SPA + gateway)
Extracted from unibus v0.13.0: the chat SPA (web/, React+Mantine, per-user BIP39 wallet) and the web gateway (cmd/webgw, REST+SSE) that acts as a bus peer for the browser. Consumes unibus as a Go module via replace => ../unibus, keeping its own replace fn-registry for the cybersecurity primitives. go build/vet/test and pnpm build green in the new location.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Flex, Box, Center, Loader, Stack, Text, Button } from "@mantine/core";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { ChatPanel } from "./ChatPanel";
|
||||
import { api } from "./api";
|
||||
import type { Room, User } from "./types";
|
||||
|
||||
export function ChatShell({
|
||||
user,
|
||||
onLogout,
|
||||
}: {
|
||||
user: User;
|
||||
onLogout: () => void;
|
||||
}) {
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [activeId, setActiveId] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
api
|
||||
.listRooms()
|
||||
.then((rs) => {
|
||||
setRooms(rs);
|
||||
setActiveId((cur) => cur || rs[0]?.id || "");
|
||||
setError(null);
|
||||
})
|
||||
.catch((e) => setError(e?.message ?? "No se pudieron cargar las rooms"))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const active = rooms.find((r) => r.id === activeId);
|
||||
|
||||
// El panel derecho muestra el estado de carga/error/empty sin tocar el layout.
|
||||
let panel = <ChatPanel room={active} />;
|
||||
if (loading && rooms.length === 0) {
|
||||
panel = (
|
||||
<Center h="100%">
|
||||
<Loader color="brand" />
|
||||
</Center>
|
||||
);
|
||||
} else if (error) {
|
||||
panel = (
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="sm">
|
||||
<Text c="red" size="sm">
|
||||
{error}
|
||||
</Text>
|
||||
<Button variant="light" color="brand" onClick={load}>
|
||||
Reintentar
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
} else if (rooms.length === 0) {
|
||||
panel = (
|
||||
<Center h="100%">
|
||||
<Text c="dimmed">No perteneces a ninguna room todavía</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex h="100vh" w="100vw" style={{ overflow: "hidden" }}>
|
||||
<Box
|
||||
w={320}
|
||||
h="100%"
|
||||
bg="dark.8"
|
||||
style={{
|
||||
borderRight: "1px solid var(--mantine-color-dark-4)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Sidebar
|
||||
user={user}
|
||||
rooms={rooms}
|
||||
activeId={activeId}
|
||||
onSelect={setActiveId}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} h="100%" bg="dark.7" style={{ minWidth: 0 }}>
|
||||
{panel}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user