-
Notifications
You must be signed in to change notification settings - Fork 266
impl node_chat #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
impl node_chat #196
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,5 +5,8 @@ | |
| # Node | ||
| node_modules | ||
|
|
||
| # Build | ||
| client/dist | ||
|
|
||
| # MacOS | ||
| .DS_Store | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Node Chat</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.jsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,331 @@ | ||
| import { useEffect, useMemo, useRef, useState } from 'react'; | ||
| import { io } from 'socket.io-client'; | ||
|
|
||
| const socket = io('http://localhost:3005'); | ||
| const USERNAME_KEY = 'node_chat_username'; | ||
|
|
||
| export default function App() { | ||
| const [username, setUsername] = useState( | ||
| localStorage.getItem(USERNAME_KEY) || '', | ||
| ); | ||
| const [draftUsername, setDraftUsername] = useState(username); | ||
| const [rooms, setRooms] = useState([]); | ||
| const [currentRoom, setCurrentRoom] = useState(null); | ||
| const [messages, setMessages] = useState([]); | ||
| const [messageText, setMessageText] = useState(''); | ||
| const [newRoomName, setNewRoomName] = useState(''); | ||
| const [renameValue, setRenameValue] = useState(''); | ||
| const [error, setError] = useState(''); | ||
| const [toast, setToast] = useState(null); | ||
| const messageEndRef = useRef(null); | ||
|
|
||
| const showToast = (message, type = 'info') => { | ||
| setToast({ message, type }); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| socket.on('roomList', setRooms); | ||
| socket.on('roomJoined', (room) => { | ||
| setCurrentRoom({ id: room.id, name: room.name }); | ||
| setMessages(room.messages || []); | ||
| setRenameValue(room.name); | ||
| }); | ||
| socket.on('roomCreated', (roomId) => { | ||
| showToast('Room created', 'success'); | ||
| joinRoom(roomId); | ||
| }); | ||
| socket.on('newMessage', (message) => { | ||
| setMessages((prev) => [...prev, message]); | ||
| }); | ||
| socket.on('systemMessage', (message) => { | ||
| setMessages((prev) => [...prev, message]); | ||
| }); | ||
| socket.on('roomRenamed', (room) => { | ||
| setCurrentRoom((prev) => (prev && prev.id === room.id ? room : prev)); | ||
| setRenameValue(room.name); | ||
| }); | ||
| socket.on('roomDeleted', (roomId) => { | ||
| setRooms((prev) => prev.filter((room) => room.id !== roomId)); | ||
| setCurrentRoom((prev) => (prev?.id === roomId ? null : prev)); | ||
| setMessages((prev) => | ||
| prev.concat({ | ||
| author: 'System', | ||
| text: 'Current room was deleted', | ||
| time: new Date().toISOString(), | ||
| }), | ||
| ); | ||
| showToast('Room deleted', 'warning'); | ||
| }); | ||
| socket.on('error', (message) => { | ||
| setError(message); | ||
| showToast(message, 'error'); | ||
| }); | ||
|
|
||
| return () => { | ||
| socket.off('roomList'); | ||
| socket.off('roomJoined'); | ||
| socket.off('roomCreated'); | ||
| socket.off('newMessage'); | ||
| socket.off('systemMessage'); | ||
| socket.off('roomRenamed'); | ||
| socket.off('roomDeleted'); | ||
| socket.off('error'); | ||
| }; | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| if (username) { | ||
| socket.emit('setUsername', username); | ||
| } | ||
| }, [username]); | ||
|
|
||
| useEffect(() => { | ||
| messageEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | ||
| }, [messages]); | ||
|
|
||
| useEffect(() => { | ||
| if (!toast) { | ||
| return; | ||
| } | ||
|
|
||
| const id = setTimeout(() => { | ||
| setToast(null); | ||
| }, 3000); | ||
|
|
||
| return () => clearTimeout(id); | ||
| }, [toast]); | ||
|
|
||
| const roomName = useMemo(() => { | ||
| if (!currentRoom) { | ||
| return 'No room selected'; | ||
| } | ||
|
|
||
| return currentRoom.name; | ||
| }, [currentRoom]); | ||
|
|
||
| const saveUsername = (event) => { | ||
| event.preventDefault(); | ||
|
|
||
| const cleaned = draftUsername.trim(); | ||
|
|
||
| if (!cleaned) { | ||
| setError('Username is required'); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| setError(''); | ||
| localStorage.setItem(USERNAME_KEY, cleaned); | ||
| setUsername(cleaned); | ||
| }; | ||
|
|
||
| const joinRoom = (roomId) => { | ||
| setError(''); | ||
| socket.emit('joinRoom', roomId); | ||
| }; | ||
|
|
||
| const sendMessage = (event) => { | ||
| event.preventDefault(); | ||
|
|
||
| const cleaned = messageText.trim(); | ||
|
|
||
| if (!cleaned || !currentRoom) { | ||
| return; | ||
| } | ||
|
|
||
| socket.emit('sendMessage', cleaned); | ||
| setMessageText(''); | ||
| }; | ||
|
|
||
| const createRoom = (event) => { | ||
| event.preventDefault(); | ||
| const cleaned = newRoomName.trim(); | ||
|
|
||
| if (!cleaned) { | ||
| return; | ||
| } | ||
|
|
||
| if (rooms.some((r) => r.name === cleaned)) { | ||
| showToast('Room already exists', 'warning'); | ||
| return; | ||
| } | ||
|
|
||
| socket.emit('createRoom', cleaned, (response) => { | ||
| if (!response?.ok) { | ||
| const message = response?.error || 'Failed to create room'; | ||
| setError(message); | ||
| showToast(message, 'error'); | ||
| return; | ||
| } | ||
|
|
||
| showToast('Room created', 'success'); | ||
| joinRoom(response.roomId); | ||
| }); | ||
|
|
||
| setNewRoomName(''); | ||
| showToast('Creating room...', 'info'); | ||
| }; | ||
|
|
||
| const renameRoom = (event) => { | ||
| event.preventDefault(); | ||
|
|
||
| if (!currentRoom) { | ||
| return; | ||
| } | ||
|
|
||
| const cleaned = renameValue.trim(); | ||
|
|
||
| if (!cleaned) { | ||
| return; | ||
| } | ||
|
|
||
| if (rooms.some((r) => r.name === cleaned)) { | ||
| showToast('Room already exists', 'warning'); | ||
| return; | ||
| } | ||
|
|
||
| socket.emit('renameRoom', { roomId: currentRoom.id, newName: cleaned }); | ||
| showToast('Room renamed', 'success'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This success toast is displayed immediately after sending the request to the server, without waiting for confirmation. If an error occurs on the server (e.g., the new room name is invalid or already exists), the user will see a success message followed by an error message. It would be better to move this toast into the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved toast to relative handler. |
||
| }; | ||
|
|
||
| const deleteRoom = () => { | ||
| if (!currentRoom) { | ||
| return; | ||
| } | ||
|
|
||
| socket.emit('deleteRoom', currentRoom.id); | ||
| showToast('Room delete requested', 'warning'); | ||
| }; | ||
|
|
||
| const resetUsername = () => { | ||
| localStorage.removeItem(USERNAME_KEY); | ||
| setUsername(''); | ||
| setDraftUsername(''); | ||
| setCurrentRoom(null); | ||
| setMessages([]); | ||
| setError(''); | ||
| }; | ||
|
|
||
| if (!username) { | ||
| return ( | ||
| <main className="auth-screen"> | ||
| <form onSubmit={saveUsername} className="auth-card"> | ||
| <h1>Welcome to Node Chat</h1> | ||
| <p>Create your username to continue</p> | ||
| <label htmlFor="username">Username</label> | ||
| <input | ||
| id="username" | ||
| value={draftUsername} | ||
| onChange={(e) => setDraftUsername(e.target.value)} | ||
| placeholder="Enter username" | ||
| autoFocus | ||
| /> | ||
| <button type="submit">Continue</button> | ||
| {error && <div className="error">{error}</div>} | ||
| </form> | ||
| </main> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {toast && ( | ||
| <div className={`toast toast-${toast.type}`}>{toast.message}</div> | ||
| )} | ||
| <div className="layout"> | ||
| <aside className="sidebar"> | ||
| <h2>Rooms</h2> | ||
|
|
||
| <div className="panel"> | ||
| <strong>{username}</strong> | ||
| <button type="button" onClick={resetUsername}> | ||
| Change user | ||
| </button> | ||
| </div> | ||
|
|
||
| <form onSubmit={createRoom} className="panel"> | ||
| <label htmlFor="new-room">Create Room</label> | ||
| <input | ||
| id="new-room" | ||
| value={newRoomName} | ||
| onChange={(e) => setNewRoomName(e.target.value)} | ||
| placeholder="Room name" | ||
| /> | ||
| <button type="submit">Create</button> | ||
| </form> | ||
|
|
||
| <div className="rooms-list"> | ||
| {rooms.map((room) => ( | ||
| <button | ||
| key={room.id} | ||
| type="button" | ||
| className={`room-button ${currentRoom?.id === room.id ? 'active' : ''}`} | ||
| onClick={() => joinRoom(room.id)} | ||
| > | ||
| {room.name} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </aside> | ||
|
|
||
| <main className="chat"> | ||
| <header className="chat-header"> | ||
| <div> | ||
| <h1>{roomName}</h1> | ||
| <p> | ||
| {username | ||
| ? `Signed in as ${username}` | ||
| : 'Set username to start'} | ||
| </p> | ||
| </div> | ||
|
|
||
| {currentRoom && ( | ||
| <div className="room-actions"> | ||
| <form onSubmit={renameRoom}> | ||
| <input | ||
| value={renameValue} | ||
| onChange={(e) => setRenameValue(e.target.value)} | ||
| placeholder="New room name" | ||
| /> | ||
| <button type="submit">Rename</button> | ||
| </form> | ||
| <button type="button" className="danger" onClick={deleteRoom}> | ||
| Delete | ||
| </button> | ||
| </div> | ||
| )} | ||
| </header> | ||
|
|
||
| {error && <p className="error">{error}</p>} | ||
|
|
||
| <section className="messages"> | ||
| {messages.map((message, index) => ( | ||
| <article key={`${message.time}-${index}`} className="message"> | ||
| <div className="meta"> | ||
| <strong>{message.author}</strong> | ||
| <time>{new Date(message.time).toLocaleTimeString()}</time> | ||
| </div> | ||
| <p>{message.text}</p> | ||
| </article> | ||
| ))} | ||
| <div ref={messageEndRef} /> | ||
| </section> | ||
|
|
||
| <form onSubmit={sendMessage} className="send-form"> | ||
| <input | ||
| value={messageText} | ||
| onChange={(e) => setMessageText(e.target.value)} | ||
| placeholder={ | ||
| currentRoom ? 'Write a message' : 'Join a room first' | ||
| } | ||
| disabled={!currentRoom || !username} | ||
| /> | ||
| <button type="submit" disabled={!currentRoom || !username}> | ||
| Send | ||
| </button> | ||
| </form> | ||
| </main> | ||
| </div> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import React from 'react'; | ||
| import { createRoot } from 'react-dom/client'; | ||
| import App from './App'; | ||
| import './styles.css'; | ||
|
|
||
| createRoot(document.getElementById('root')).render( | ||
| <React.StrictMode> | ||
| <App /> | ||
| </React.StrictMode>, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server-side
'createRoom'handler doesn't use an acknowledgment callback, so this callback function will never be executed. The logic within this callback is effectively dead code. Fortunately, the necessary logic is correctly implemented in the'roomCreated'event listener. Consider removing this callback from thesocket.emitcall.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed.