feat(web): SPA de chat (React + Vite + Mantine v9)
Cliente web sobre el gateway (REST + SSE). El navegador no habla NATS ni cripto: el peer Go del gateway lo hace. - Pantalla de conexión: gateway URL + identidad (persistidas en localStorage). - Navbar: crear room (con toggle de cifrado E2E), unirse por id, lista de rooms. - Centro: mensajes en vivo por SSE, burbujas con autor y hora, composer. - Lateral: miembros (rol owner), invitar por peer conectado, expulsar (owner). - Mantine v9 (createTheme + MantineProvider), @tabler/icons-react, layout con AppShell/Stack/Group; sin Tailwind ni CSS manual. React 19 (peer dep de v9). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Stack,
|
||||
TextInput,
|
||||
Checkbox,
|
||||
Button,
|
||||
Divider,
|
||||
Text,
|
||||
NavLink,
|
||||
ScrollArea,
|
||||
Group,
|
||||
Box,
|
||||
} from "@mantine/core";
|
||||
import { IconLock, IconHash, IconPlus, IconDoorEnter } from "@tabler/icons-react";
|
||||
import type { Room } from "../types";
|
||||
|
||||
interface Props {
|
||||
rooms: Room[];
|
||||
activeRoom: string | null;
|
||||
onSelect: (roomID: string) => void;
|
||||
onCreateRoom: (subject: string, encrypt: boolean) => void;
|
||||
onJoinRoom: (roomID: string) => void;
|
||||
}
|
||||
|
||||
// RoomList is the navbar: create a room, join one by id, and pick the active
|
||||
// room from the peer's known rooms.
|
||||
export function RoomList({ rooms, activeRoom, onSelect, onCreateRoom, onJoinRoom }: Props) {
|
||||
const [subject, setSubject] = useState("room.general");
|
||||
const [encrypt, setEncrypt] = useState(true);
|
||||
const [joinID, setJoinID] = useState("");
|
||||
|
||||
const create = () => {
|
||||
if (subject.trim()) onCreateRoom(subject.trim(), encrypt);
|
||||
};
|
||||
const join = () => {
|
||||
if (joinID.trim()) {
|
||||
onJoinRoom(joinID.trim());
|
||||
setJoinID("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={0} h="100%">
|
||||
<Box p="md">
|
||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase" mb="xs">
|
||||
Crear room
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
size="xs"
|
||||
placeholder="subject (room.general)"
|
||||
leftSection={<IconHash size={14} />}
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.currentTarget.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && create()}
|
||||
/>
|
||||
<Checkbox
|
||||
size="xs"
|
||||
label="Cifrado extremo a extremo"
|
||||
checked={encrypt}
|
||||
onChange={(e) => setEncrypt(e.currentTarget.checked)}
|
||||
/>
|
||||
<Button size="xs" leftSection={<IconPlus size={14} />} onClick={create}>
|
||||
Crear
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box p="md">
|
||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase" mb="xs">
|
||||
Unirse por id
|
||||
</Text>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<TextInput
|
||||
size="xs"
|
||||
placeholder="room id"
|
||||
value={joinID}
|
||||
onChange={(e) => setJoinID(e.currentTarget.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && join()}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button size="xs" variant="light" onClick={join} px="sm">
|
||||
<IconDoorEnter size={16} />
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text size="xs" fw={700} c="dimmed" tt="uppercase" px="md" pt="md" pb="xs">
|
||||
Rooms ({rooms.length})
|
||||
</Text>
|
||||
<ScrollArea style={{ flex: 1 }}>
|
||||
<Stack gap={2} px="xs" pb="md">
|
||||
{rooms.length === 0 && (
|
||||
<Text size="sm" c="dimmed" px="sm" py="lg" ta="center">
|
||||
Aún no hay rooms. Crea o únete a una.
|
||||
</Text>
|
||||
)}
|
||||
{rooms.map((r) => (
|
||||
<NavLink
|
||||
key={r.room_id}
|
||||
active={r.room_id === activeRoom}
|
||||
onClick={() => onSelect(r.room_id)}
|
||||
label={r.subject}
|
||||
description={r.room_id.slice(0, 14) + "…"}
|
||||
leftSection={
|
||||
r.encrypt ? <IconLock size={16} /> : <IconHash size={16} />
|
||||
}
|
||||
variant="filled"
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user