diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae1f8d5..7788d66 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,9 +8,10 @@ import { type Request } from "./api/types"; type AppProps = { socket: WebSocket | null; + userName: string; }; -export function App({ socket }: AppProps) { +export function App({ socket, userName }: AppProps) { const [roomID, setRoomID] = useState(""); const [rooms, setRooms] = useState([]); const [joinedRooms, setJoinedRooms] = useState>(new Set()); @@ -30,7 +31,7 @@ export function App({ socket }: AppProps) { const sendMessageRequest = useWsRequest(socket, undefined); const onMessageSent = (content: string) => { - const chat: Chat = { content, user: { name: "You" } }; + const chat: Chat = { content, user: { name: userName } }; if (!roomID) { return; diff --git a/frontend/src/TopBar.tsx b/frontend/src/TopBar.tsx index 4ba6a27..a6f8683 100644 --- a/frontend/src/TopBar.tsx +++ b/frontend/src/TopBar.tsx @@ -1,14 +1,100 @@ -export function TopBar() { +import { useRef, useState, type FormEvent } from "react"; + +type TopBarProps = { + displayName: string; + onRename: (name: string) => void; +}; + +export function TopBar({ displayName, onRename }: TopBarProps) { + const dialogRef = useRef(null); + const [draftName, setDraftName] = useState(displayName); + const initials = getInitials(displayName); + + const openDialog = () => { + setDraftName(displayName); + dialogRef.current?.showModal(); + }; + + const closeDialog = () => { + dialogRef.current?.close(); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const trimmed = draftName.trim(); + if (!trimmed) { + return; + } + onRename(trimmed); + closeDialog(); + }; + return (
-
+
+
+ Signed in as + {displayName} +
+
+
+ {initials} +
+
+
+ +
+

Rename profile

+

+ Your new name will be used the next time you connect. +

+
+ +
+ + +
+
+
+
+ +
+
); } + +function getInitials(name: string): string { + const parts = name.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) { + return "?"; + } + if (parts.length === 1) { + return parts[0].slice(0, 2).toUpperCase(); + } + return (parts[0][0] + parts[1][0]).toUpperCase(); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 85f4fa0..0be9616 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,6 +5,8 @@ import { TopBar } from "./TopBar.tsx"; import { App } from "./App.tsx"; import { useEffect, useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useWsRequest } from "./hooks/useWsRequest"; +import { type Request } from "./api/types"; createRoot(document.getElementById("root")!).render( @@ -16,6 +18,8 @@ const queryClient = new QueryClient(); export function Main() { const [socket, setSocket] = useState(null); + const [identity, setIdentity] = useState(() => loadIdentity()); + const connectRequest = useWsRequest(socket, undefined); useEffect(() => { const nextSocket = new WebSocket(resolveWebSocketUrl()); @@ -33,12 +37,29 @@ export function Main() { }; }, []); + useEffect(() => { + saveIdentity(identity); + }, [identity]); + + useEffect(() => { + if (!identity.name.trim()) { + return; + } + + const request: Request = { type: "connect", token: identity.token, name: identity.name }; + connectRequest(request); + }, [connectRequest, identity]); + + const handleRename = (nextName: string) => { + setIdentity((prev) => ({ ...prev, name: nextName })); + }; + return (
- +
- +
@@ -60,3 +81,48 @@ function resolveWebSocketUrl(): string { return url.toString(); } + +const IDENTITY_STORAGE_KEY = "echx.identity"; +const DEFAULT_NAME = "Guest"; + +type Identity = { + token: string; + name: string; +}; + +function loadIdentity(): Identity { + if (typeof window === "undefined") { + return { token: createToken(), name: DEFAULT_NAME }; + } + + const stored = window.localStorage.getItem(IDENTITY_STORAGE_KEY); + if (stored) { + try { + const parsed = JSON.parse(stored) as Partial; + if (parsed?.token && parsed?.name) { + return { token: parsed.token, name: parsed.name }; + } + } catch { + // ignore invalid storage + } + } + + const identity = { token: createToken(), name: DEFAULT_NAME }; + window.localStorage.setItem(IDENTITY_STORAGE_KEY, JSON.stringify(identity)); + return identity; +} + +function saveIdentity(identity: Identity): void { + if (typeof window === "undefined") { + return; + } + + window.localStorage.setItem(IDENTITY_STORAGE_KEY, JSON.stringify(identity)); +} + +function createToken(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return `token-${Date.now()}`; +}