Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { type Request } from "./api/types";

type AppProps = {
socket: WebSocket | null;
token: string;
userName: string;
};

export function App({ socket }: AppProps) {
export function App({ socket, token, userName }: AppProps) {
const [roomID, setRoomID] = useState<string>("");
const [rooms, setRooms] = useState<RoomSummary[]>([]);
const [joinedRooms, setJoinedRooms] = useState<Set<string>>(new Set());
Expand All @@ -27,10 +29,19 @@ export function App({ socket }: AppProps) {
listRoomsRequest({ type: "list_rooms" });
}, [listRoomsRequest]);

const connectRequest = useWsRequest(socket, undefined);
useEffect(() => {
if (!token || !userName) {
return;
}

connectRequest({ type: "connect", token, name: userName });
}, [connectRequest, token, userName]);

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;
Expand Down
109 changes: 99 additions & 10 deletions frontend/src/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,103 @@
export function TopBar() {
import type { FormEvent } from "react";
import { useEffect, useRef, useState } from "react";

type TopBarProps = {
userName: string;
onRename: (nextName: string) => void;
};

export function TopBar({ userName, onRename }: TopBarProps) {
const dialogRef = useRef<HTMLDialogElement | null>(null);
const [draftName, setDraftName] = useState(userName);

useEffect(() => {
setDraftName(userName);
}, [userName]);

const openDialog = () => {
setDraftName(userName);
dialogRef.current?.showModal();
};

const closeDialog = () => {
dialogRef.current?.close();
};

const submitRename = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const nextName = draftName.trim();
if (!nextName) {
return;
}
onRename(nextName);
closeDialog();
};

return (
<div className="navbar border-b border-base-300 bg-base-100 px-4 shadow-sm">
<div className="navbar-start">
<a className="btn btn-ghost text-lg font-semibold">echx</a>
<>
<div className="navbar border-b border-base-300 bg-base-100 px-4 shadow-sm">
<div className="navbar-start">
<a className="btn btn-ghost text-lg font-semibold">echx</a>
</div>
<div className="navbar-end gap-2">
<div className="hidden items-center gap-3 rounded-full border border-base-300 bg-base-200 px-3 py-1.5 text-sm md:flex">
<span className="text-base-content/60">Signed in as</span>
<span className="font-semibold text-base-content">{userName}</span>
</div>
<button type="button" className="btn btn-primary btn-sm" onClick={openDialog}>
Rename
</button>
</div>
</div>
<div className="navbar-end">
<button type="button" className="btn btn-ghost btn-sm">
New room
</button>
</div>
</div>

<dialog ref={dialogRef} className="modal">
<div className="modal-box">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold">Change your name</h3>
<form method="dialog">
<button type="submit" className="btn btn-circle btn-ghost btn-sm" aria-label="Close">
</button>
</form>
</div>
<p className="mt-2 text-sm text-base-content/70">
Pick a name that appears to everyone you chat with. This updates immediately for your
session.
</p>
<form className="mt-6 space-y-4" onSubmit={submitRename}>
<label className="form-control w-full">
<div className="label">
<span className="label-text font-semibold">Display name</span>
</div>
<input
type="text"
value={draftName}
onChange={(event) => setDraftName(event.target.value)}
placeholder="Type a fresh name"
className="input input-bordered w-full"
maxLength={40}
required
/>
</label>
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-base-content/50">Max 40 characters.</span>
<div className="flex items-center gap-2">
<button type="button" className="btn btn-ghost" onClick={closeDialog}>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Save name
</button>
</div>
</div>
</form>
</div>
<form method="dialog" className="modal-backdrop">
<button type="submit" aria-label="Close rename dialog">
close
</button>
</form>
</dialog>
</>
);
}
48 changes: 46 additions & 2 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const queryClient = new QueryClient();

export function Main() {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [token, setToken] = useState<string>(() => readStoredToken());
const [userName, setUserName] = useState<string>(() => readStoredName());

useEffect(() => {
const nextSocket = new WebSocket(resolveWebSocketUrl());
Expand All @@ -33,12 +35,20 @@ export function Main() {
};
}, []);

useEffect(() => {
storeToken(token);
}, [token]);

useEffect(() => {
storeName(userName);
}, [userName]);

return (
<div className="h-screen flex flex-col overflow-hidden">
<TopBar />
<TopBar userName={userName} onRename={setUserName} />
<div className="flex flex-1 min-h-0 overflow-hidden">
<QueryClientProvider client={queryClient}>
<App socket={socket} />
<App socket={socket} token={token} userName={userName} />
</QueryClientProvider>
</div>
</div>
Expand All @@ -60,3 +70,37 @@ function resolveWebSocketUrl(): string {

return url.toString();
}

function readStoredToken(): string {
if (typeof window === "undefined") {
return "guest";
}
const stored = window.localStorage.getItem("echx_token");
if (stored) {
return stored;
}
const nextToken = window.crypto?.randomUUID?.() ?? `guest-${Date.now()}`;
window.localStorage.setItem("echx_token", nextToken);
return nextToken;
}

function readStoredName(): string {
if (typeof window === "undefined") {
return "Guest";
}
return window.localStorage.getItem("echx_name") ?? "Guest";
}

function storeToken(token: string) {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem("echx_token", token);
}

function storeName(name: string) {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem("echx_name", name);
}