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
581 changes: 581 additions & 0 deletions public/css/styles.css

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Chat em tempo real com salas e WebSockets">
<title>💬 Chat em Tempo Real</title>
<link rel="stylesheet" href="/css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💬</text></svg>">
</head>
<body>
<!-- ===== TELA 1: Username ===== -->
<div id="usernameScreen" class="username-screen">
<div>
<h1>💬 <span>Chat</span></h1>
<p>Digite seu nome para entrar no chat em tempo real</p>
</div>
<form id="usernameForm" class="username-form">
<input
type="text"
id="usernameInput"
placeholder="Seu nome de usuário..."
autocomplete="off"
maxlength="20"
required
>
<button type="submit" class="btn btn-primary">Entrar</button>
</form>
<div id="usernameError" class="username-error"></div>
</div>

<!-- ===== TELA 2: Chat ===== -->
<div id="chatApp" class="chat-app">
<!-- Overlay para menu mobile -->
<div id="sidebarOverlay" class="sidebar-overlay"></div>

<!-- === Sidebar: Lista de Salas === -->
<aside id="sidebar" class="sidebar">
<div class="sidebar-header">
<h2>🏠 Salas</h2>
</div>

<div id="roomList" class="room-list">
<!-- Salas renderizadas via JS -->
</div>

<div class="sidebar-footer">
<form id="createRoomForm" class="create-room-form">
<input
type="text"
id="newRoomInput"
placeholder="Nova sala..."
autocomplete="off"
maxlength="30"
>
<button type="submit" class="btn btn-primary btn-sm">+</button>
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<button type="submit" class="btn btn-primary btn-sm">+</button>
<button type="submit" class="btn btn-primary btn-sm" aria-label="Criar sala">+</button>

Copilot uses AI. Check for mistakes.
</form>
</div>
</aside>

<!-- === Área Principal do Chat === -->
<main class="chat-main">
<!-- Header do Chat -->
<div class="chat-header">
<div style="display: flex; align-items: center; gap: 12px;">
<button
id="mobileMenuBtn"
class="btn-icon mobile-menu-btn"
style="display: none;"
type="button"
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
type="button"
type="button"
aria-label="Abrir menu de salas"

Copilot uses AI. Check for mistakes.
>☰</button>
<div class="chat-header-info">
<h3 id="currentRoomName">Geral</h3>
<span id="currentRoomId"></span>
</div>
</div>
<div class="online-users">
<span class="online-dot"></span>
<span id="onlineCount">0 online</span>
</div>
</div>

<!-- Mensagens -->
<div id="messagesContainer" class="messages-container">
<div class="empty-state">
<div class="icon">💬</div>
<p>Nenhuma mensagem ainda. Seja o primeiro!</p>
</div>
</div>

<!-- Input de mensagem -->
<div class="message-input-area">
<form id="messageForm" class="message-form">
<input
type="text"
id="messageInput"
placeholder="Digite sua mensagem..."
autocomplete="off"
maxlength="500"
>
<button type="submit" class="btn btn-primary">Enviar</button>
</form>
</div>
</main>
</div>

<!-- === Modal: Renomear Sala === -->
<div id="renameModal" class="modal-overlay">
<div class="modal">
<h3>✏️ Renomear Sala</h3>
<input
type="text"
id="renameInput"
placeholder="Novo nome..."
autocomplete="off"
maxlength="30"
>
<div class="modal-actions">
<button id="renameCancelBtn" class="btn btn-ghost" type="button">Cancelar</button>
<button id="renameConfirmBtn" class="btn btn-primary" type="button">Salvar</button>
</div>
</div>
</div>

<!-- Socket.IO Client (carregado do servidor como script global) -->
<script src="/socket.io/socket.io.js"></script>
Comment on lines +125 to +126
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
<!-- 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>

Copilot uses AI. Check for mistakes.
<!-- App principal (ES Module que importa os sub-módulos) -->
<script type="module" src="/js/app.js"></script>
</body>
</html>
89 changes: 89 additions & 0 deletions public/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* app.js — Orquestrador principal do Chat.
*
* Módulo raiz que importa e inicializa todos os sub-módulos.
* Responsabilidades:
* - Gerenciar fluxo de username (localStorage + join)
* - Reconexão automática via Socket.IO
* - Inicializar rooms, messages, modal e mobile
*
* @module app
*/

/* eslint-env browser */

import { socket, state } from './state.js';
import { initRoomHandlers } from './rooms.js';
import { initMessageHandlers } from './messages.js';
import { initModalHandlers } from './modal.js';
import { initMobileHandlers } from './mobile.js';

// === Referências DOM (apenas as do fluxo de username) ===
const usernameScreen = document.getElementById('usernameScreen');
const usernameForm = document.getElementById('usernameForm');
const usernameInput = document.getElementById('usernameInput');
const usernameError = document.getElementById('usernameError');
const chatApp = document.getElementById('chatApp');
const messageInput = document.getElementById('messageInput');

// === Fluxo de Username ===

/**
* Envia o username ao servidor e aguarda resposta.
* Se sucesso → mostra chat. Se erro → mostra mensagem.
* @param {string} userName - Nome de usuário desejado
*/
function joinWithUsername(userName) {
socket.emit('user:join', { username: userName }, (response) => {
if (response && response.success) {
state.username = userName;
localStorage.setItem('chat_username', userName);
usernameScreen.style.display = 'none';
chatApp.classList.add('active');
messageInput.focus();
usernameError.textContent = '';
} else {
usernameError.textContent = response
? response.error
: 'Erro ao conectar';
usernameScreen.style.display = '';
chatApp.classList.remove('active');
}
});
}

// Formulário de username
usernameForm.addEventListener('submit', (e) => {
e.preventDefault();

const userName = usernameInput.value.trim();

if (!userName) {
return;
}

joinWithUsername(userName);
});

// === Reconexão automática ===
socket.on('connect', () => {
if (state.username && !usernameScreen.style.display) {
joinWithUsername(state.username);

if (state.currentRoomId) {
socket.emit('room:join', { roomId: state.currentRoomId });
}
}
Comment on lines +69 to +76
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
});

// === Inicializar todos os módulos ===
initRoomHandlers();
initMessageHandlers();
initModalHandlers();
initMobileHandlers();

// === Auto-login se já tiver username salvo ===
if (state.username) {
usernameInput.value = state.username;
joinWithUsername(state.username);
}
56 changes: 56 additions & 0 deletions public/js/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* messages.js — Handlers de mensagens (Socket.IO + UI).
*
* Responsabilidades:
* - Receber histórico e novas mensagens do servidor
* - Enviar mensagens pelo formulário
* - Atualizar contagem de usuários online
*
* @module messages
*/

/* eslint-env browser */

import { socket } from './state.js';
import { renderMessages, appendMessage, scrollToBottom } from './ui.js';

/**
* Inicializa todos os handlers de mensagem.
* Chamado uma única vez pelo app.js na inicialização.
*/
export function initMessageHandlers() {
const messageForm = document.getElementById('messageForm');
const messageInput = document.getElementById('messageInput');
const onlineCount = document.getElementById('onlineCount');

// room:history — Recebe histórico ao entrar na sala
socket.on('room:history', (messages) => {
renderMessages(messages);
});

// message:new — Nova mensagem em tempo real
socket.on('message:new', (message) => {
appendMessage(message);
scrollToBottom();
});

// user:list — Atualiza contagem de online
socket.on('user:list', (users) => {
onlineCount.textContent = users.length + ' online';
});

// Enviar mensagem via formulário
messageForm.addEventListener('submit', (e) => {
e.preventDefault();

const text = messageInput.value.trim();

if (!text) {
return;
}

socket.emit('message:send', { text });
messageInput.value = '';
messageInput.focus();
});
}
37 changes: 37 additions & 0 deletions public/js/mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* mobile.js — Menu responsivo da sidebar.
*
* Responsabilidades:
* - Abrir/fechar sidebar em telas pequenas
* - Controlar overlay de fundo
*
* @module mobile
*/

/* eslint-env browser */

const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');

/**
* Fecha a sidebar mobile e o overlay.
*/
export function closeMobileSidebar() {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('open');
}

/**
* Inicializa os event listeners do menu mobile.
* Chamado uma única vez pelo app.js na inicialização.
*/
export function initMobileHandlers() {
const mobileMenuBtn = document.getElementById('mobileMenuBtn');

mobileMenuBtn.addEventListener('click', () => {
sidebar.classList.toggle('open');
sidebarOverlay.classList.toggle('open');
});

sidebarOverlay.addEventListener('click', closeMobileSidebar);
}
Loading
Loading