import React, { useState, useEffect, useRef } from "react"; import Sidebar from "./Sidebar"; import ChatPanel from "./ChatPanel"; import { Room, ChatMessage } from "./types"; function App() { const [rooms, setRooms] = useState([]); const [user, setUser] = useState(() => localStorage.getItem("chatUser")); const [userProfile, setUserProfile] = useState(() => { const saved = localStorage.getItem("chatProfile"); return saved ? JSON.parse(saved) : null; }); const [usernameInput, setUsernameInput] = useState(""); const [passwordInput, setPasswordInput] = useState(""); const [currentRoom, setCurrentRoom] = useState(null); const [messages, setMessages] = useState([]); const [wsKey, setWsKey] = useState(0); const [wsStatus, setWsStatus] = useState("connecting"); const ws = useRef(null); const currentRoomRef = useRef(null); const intentionalClose = useRef(false); const handleLogin = async () => { try { const res = await fetch("/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: usernameInput, password: passwordInput }), }); if (!res.ok) { alert("Invalid credentials"); return; } const profile = await res.json(); localStorage.setItem("chatUser", profile.username); localStorage.setItem("chatProfile", JSON.stringify(profile)); setUserProfile(profile); setUser(profile.username); } catch (err) { alert("Login failed"); } }; const fetchSubscribedRooms = async (username: string) => { try { const res = await fetch(`/rooms/subscribed/${username}`); const data = await res.json(); setRooms(data.map((r: any) => ({ id: r._id, topic: r.name, ...r }))); const saved = localStorage.getItem("chatRoom"); if (saved) { const found = data.find((r: any) => r._id === saved); if (found) { const room = { id: found._id, topic: found.name, ...found }; setCurrentRoom(room); currentRoomRef.current = room; } } } catch (err) { console.error("Failed to fetch subscribed rooms:", err); } }; useEffect(() => { if (!user) return; fetchSubscribedRooms(user); const interval = setInterval(() => fetchSubscribedRooms(user), 10000); return () => clearInterval(interval); }, [user]); useEffect(() => { const params = new URLSearchParams(window.location.search); const token = params.get("invite"); if (!token) return; const handleInvite = async (username: string) => { try { const infoRes = await fetch(`/invite/${token}`); if (!infoRes.ok) { console.error("Invalid invite"); return; } const { room, invitedUsername } = await infoRes.json(); if (invitedUsername !== username) { alert(`This invite is for user "${invitedUsername}". Please log in as that user.`); return; } const acceptRes = await fetch(`/invite/${token}/accept`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username }), }); if (acceptRes.ok) { window.history.replaceState({}, "", window.location.pathname); await fetchSubscribedRooms(username); const newRoom = { id: room._id, topic: room.name, ...room }; setCurrentRoom(newRoom); currentRoomRef.current = newRoom; localStorage.setItem("chatRoom", room._id); } } catch (err) { console.error("Failed to handle invite:", err); } }; if (user) { handleInvite(user); } else { localStorage.setItem("pendingInvite", token); } }, [user]); useEffect(() => { const handleVisibilityChange = () => { if (document.visibilityState === "visible") { intentionalClose.current = false; setWsKey((k) => k + 1); } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => document.removeEventListener("visibilitychange", handleVisibilityChange); }, []); useEffect(() => { if (!user) return; intentionalClose.current = false; const proto = window.location.protocol === "https:" ? "wss" : "ws"; const socket = new WebSocket(`${proto}://${window.location.host}`); ws.current = socket; socket.onopen = () => { setWsStatus("connected"); if (currentRoomRef.current) { socket.send(JSON.stringify({ type: "join", roomId: currentRoomRef.current.id })); fetch(`/messages/${currentRoomRef.current.id}`) .then(r => r.json()) .then(msgs => setMessages(msgs)) .catch(console.error); } }; socket.onmessage = (event) => { try { const msg: ChatMessage & { roomId: string } = JSON.parse(event.data); if (currentRoomRef.current && msg.roomId === currentRoomRef.current.id) { setMessages((prev) => [...prev, msg]); } } catch (e) { console.error("Failed to parse WS message:", e); } }; socket.onclose = (event) => { setWsStatus(`closed (${event.code})`); if (!intentionalClose.current) { setTimeout(() => setWsKey((k) => k + 1), 2000); } intentionalClose.current = false; }; socket.onerror = (e) => console.error("WebSocket error", e); return () => { intentionalClose.current = true; socket.onclose = null; socket.close(); }; }, [user, wsKey]); const handleSend = (text: string) => { if (!currentRoom || !ws.current || !user) return; ws.current.send(JSON.stringify({ roomId: currentRoom.id, user, text, ts: Date.now(), })); }; const handleRoomSelect = async (room: Room) => { setCurrentRoom(room); currentRoomRef.current = room; localStorage.setItem("chatRoom", room.id); if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify({ type: "join", roomId: room.id })); } else { ws.current?.addEventListener("open", () => { ws.current?.send(JSON.stringify({ type: "join", roomId: room.id })); }, { once: true }); } try { const res = await fetch(`/messages/${room.id}`); const msgs: ChatMessage[] = await res.json(); setMessages(msgs); } catch (err) { setMessages([]); } }; const handleRoomsUpdate = () => { if (user) fetchSubscribedRooms(user); }; const handleProfileUpdate = (profile: any) => { setUserProfile(profile); localStorage.setItem("chatProfile", JSON.stringify(profile)); }; const handleSignOut = () => { localStorage.clear(); window.location.reload(); }; if (!user) { return (

KafkaChat

Sign in to continue

setUsernameInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLogin()} />
setPasswordInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleLogin()} />
); } return (
); } export default App;