feat: implementação full-stack de chat realtime com Socket.IO e Clean…#191
feat: implementação full-stack de chat realtime com Socket.IO e Clean…#191glauccoeng-prog wants to merge 1 commit intomate-academy:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Implementa a camada de front-end do chat realtime (UI + handlers Socket.IO) com módulos ES para estado, salas, mensagens, modal de rename e comportamento mobile, seguindo a proposta de “Clean Architecture” no cliente.
Changes:
- Adiciona módulos JS (state/app/rooms/messages/ui/modal/mobile) para orquestração do chat e integração com eventos Socket.IO.
- Cria a página HTML do chat (username flow, sidebar de salas, modal) e o design system em CSS (tema glassmorphism + responsivo).
- Implementa renderização segura de mensagens com escape de HTML e auto-scroll.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| public/js/ui.js | Renderização de mensagens, empty state, scroll e escape anti-XSS. |
| public/js/state.js | Estado global do cliente + inicialização do socket via io(). |
| public/js/rooms.js | Renderização/listeners de salas e ações (join/create/delete/rename modal). |
| public/js/modal.js | Modal de renomear sala + atalhos (Enter/Escape). |
| public/js/mobile.js | Toggle da sidebar/overlay em mobile. |
| public/js/messages.js | Handlers de histórico/mensagens realtime + envio via form + online count. |
| public/js/app.js | Orquestração, fluxo de username, auto-login e reconexão. |
| public/index.html | Estrutura completa da UI + carregamento do Socket.IO client e app module. |
| public/css/styles.css | Estilos completos (layout, componentes, responsividade, modal). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <!-- Socket.IO Client (carregado do servidor como script global) --> | ||
| <script src="/socket.io/socket.io.js"></script> |
There was a problem hiding this comment.
O cliente depende do endpoint "/socket.io/socket.io.js" e do global io(), mas o repositório não contém dependência/configuração de Socket.IO (ex.: socket.io/servidor Express) — desse jeito o script vai 404 e o app não inicializa. Inclua a camada de servidor que expõe esse endpoint (ou altere para importar/bundlar socket.io-client).
| <!-- Socket.IO Client (carregado do servidor como script global) --> | |
| <script src="/socket.io/socket.io.js"></script> | |
| <!-- Socket.IO Client (carregado de CDN como script global) --> | |
| <script src="https://cdn.socket.io/4.7.5/socket.io.min.js" integrity="sha384-3qG3kzFj1gRG5fiBSzZndKyIsmUZSuO5c12QNbq3cQ83r9E2DyPp4uM9bLaWG+gC" crossorigin="anonymous"></script> |
| socket.on('connect', () => { | ||
| if (state.username && !usernameScreen.style.display) { | ||
| joinWithUsername(state.username); | ||
|
|
||
| if (state.currentRoomId) { | ||
| socket.emit('room:join', { roomId: state.currentRoomId }); | ||
| } | ||
| } |
There was a problem hiding this comment.
A condição de reconexão está invertida: após login você define usernameScreen.style.display = 'none', então !usernameScreen.style.display vira false e o bloco não roda em reconexões. Troque para uma checagem explícita (ex.: usernameScreen.style.display === 'none' ou chatApp.classList.contains('active')) para que o re-join aconteça de fato.
| // Se ainda não está em nenhuma sala, entra na padrão | ||
| if (!state.currentRoomId && state.defaultRoomId) { | ||
| state.currentRoomId = state.defaultRoomId; | ||
|
|
||
| const defaultRoom = rooms.find((r) => r.id === state.defaultRoomId); | ||
|
|
||
| if (defaultRoom) { | ||
| currentRoomName.textContent = defaultRoom.name; | ||
| } | ||
| } |
There was a problem hiding this comment.
O comentário diz que o cliente "entra na padrão", mas aqui você só atualiza state.currentRoomId/header e não emite room:join. Se o servidor não fizer auto-join, o usuário ficará sem histórico/mensagens até clicar em uma sala; emita socket.emit('room:join', { roomId: state.defaultRoomId }) nesse fluxo (ou documente/garanta o auto-join no backend).
| .room-item:hover .room-actions { | ||
| opacity: 1; | ||
| } | ||
|
|
There was a problem hiding this comment.
As ações de sala ficam com opacity: 0 e só aparecem em :hover. Em dispositivos touch (sem hover) isso tende a deixar renomear/excluir inacessível/invisível. Considere deixar as ações sempre visíveis em mobile (media query) ou exibir em :focus-within/ao selecionar a sala.
| .room-item:hover .room-actions { | |
| opacity: 1; | |
| } | |
| .room-item:hover .room-actions, | |
| .room-item:focus-within .room-actions, | |
| .room-item.active .room-actions { | |
| opacity: 1; | |
| } | |
| @media (max-width: 768px) { | |
| .room-actions { | |
| opacity: 1; | |
| } | |
| } |
| const renameBtn = document.createElement('button'); | ||
|
|
||
| renameBtn.textContent = '✏️'; | ||
| renameBtn.title = 'Renomear'; | ||
|
|
||
| renameBtn.addEventListener('click', (e) => { | ||
| e.stopPropagation(); | ||
| openRenameModal(room.id, room.name); | ||
| }); | ||
|
|
||
| const deleteBtn = document.createElement('button'); | ||
|
|
||
| deleteBtn.textContent = '🗑️'; | ||
| deleteBtn.title = 'Excluir'; | ||
| deleteBtn.className = 'delete'; | ||
|
|
||
| deleteBtn.addEventListener('click', (e) => { | ||
| e.stopPropagation(); | ||
| socket.emit('room:delete', { roomId: room.id }); |
There was a problem hiding this comment.
Os botões de renomear/excluir são apenas emojis e não têm aria-label (nem type="button"). Isso prejudica leitores de tela e pode causar comportamento inesperado se algum dia esses botões forem colocados dentro de um <form>. Adicione aria-label descritivo (e defina type="button").
| id="mobileMenuBtn" | ||
| class="btn-icon mobile-menu-btn" | ||
| style="display: none;" | ||
| type="button" |
There was a problem hiding this comment.
O botão do menu mobile é apenas um ícone (☰) e não tem nome acessível. Adicione um aria-label (ex.: "Abrir menu de salas") para melhorar acessibilidade por leitor de tela/controle por voz.
| type="button" | |
| type="button" | |
| aria-label="Abrir menu de salas" |
| autocomplete="off" | ||
| maxlength="30" | ||
| > | ||
| <button type="submit" class="btn btn-primary btn-sm">+</button> |
There was a problem hiding this comment.
O botão "+" de criar sala não tem nome acessível (apenas símbolo). Para acessibilidade, adicione aria-label (ex.: "Criar sala") ou texto visível.
| <button type="submit" class="btn btn-primary btn-sm">+</button> | |
| <button type="submit" class="btn btn-primary btn-sm" aria-label="Criar sala">+</button> |
… Architecture