Skip to content

Commit

Permalink
Modularize index.html script, localize markdown rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
The-Best-Codes committed Nov 22, 2024
1 parent e116981 commit 40a8f62
Show file tree
Hide file tree
Showing 8 changed files with 409 additions and 304 deletions.
2 changes: 1 addition & 1 deletion public/css/tailwind.css

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions public/js/marked.min.js

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions public/pages/index/modules/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
export class ChatManager {
constructor(websocketManager, uiManager) {
this.websocketManager = websocketManager;
this.uiManager = uiManager;
this.typingTimeout = null;
this.isTyping = false;

this.setupEventListeners();
}

setupEventListeners() {
// Handle form submission
this.uiManager.messageForm.addEventListener("submit", (e) => this.handleSubmit(e));

// Handle keydown for Enter key
this.uiManager.messageInput.addEventListener("keydown", (e) => this.handleKeydown(e));

// Handle typing status
this.uiManager.messageInput.addEventListener("input", () => this.handleTyping());

// Handle reconnect button
this.uiManager.reconnectButton.addEventListener("click", () => {
this.websocketManager.manualReconnect();
});
}

handleSubmit(e) {
e.preventDefault();

// Prevent sending if not connected
if (!this.websocketManager.isConnectedStatus()) {
return;
}

const content = this.uiManager.getMessageContent();
if (content) {
this.uiManager.showSending();

const success = this.websocketManager.send({
type: "message",
content,
});

if (success) {
this.uiManager.clearMessageInput();
}

this.uiManager.hideSending();
}
}

handleKeydown(e) {
if (e.key === "Enter" && !e.shiftKey) {
// Prevent sending if not connected
if (!this.websocketManager.isConnectedStatus()) {
e.preventDefault();
return;
}

const content = this.uiManager.getMessageContent();
if (content) {
e.preventDefault();
this.uiManager.messageForm.dispatchEvent(new Event("submit"));
}
}
}

handleTyping() {
if (!this.isTyping) {
this.isTyping = true;
this.sendTypingStatus(true);
}

// Clear previous timeout
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
}

// Set new timeout
this.typingTimeout = setTimeout(() => {
this.isTyping = false;
this.sendTypingStatus(false);
}, 1000);
}

sendTypingStatus(isTyping) {
this.websocketManager.send({
type: "typing",
isTyping: isTyping
});
}

async handleInitialLoad() {
try {
const response = await fetch("/messages");
if (!response.ok) {
throw new Error("Failed to load messages");
}
const messages = await response.json();
messages.reverse();

messages.forEach((msg) => {
this.uiManager.appendMessage(msg);
});
this.uiManager.updateEmptyState();
} catch (error) {
console.error("Error loading messages:", error);
} finally {
this.uiManager.hideLoadingState();
requestAnimationFrame(() => {
this.uiManager.scrollToBottom(false);
});
}
}

handleWebSocketMessage(event) {
const data = JSON.parse(event.data);

switch (data.type) {
case "message":
this.uiManager.appendMessage(data);
break;

case "typing":
this.uiManager.updateTypingIndicator(data.message);
break;
}
}
}
33 changes: 33 additions & 0 deletions public/pages/index/modules/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { WebSocketManager } from './websocket.js';
import { UIManager } from './ui.js';
import { ChatManager } from './chat.js';

document.addEventListener("DOMContentLoaded", () => {
// Initialize managers
const uiManager = new UIManager();

// Show initial loading state
uiManager.showLoadingState();

// Initialize WebSocket manager with callbacks
const websocketManager = new WebSocketManager(
// onOpen callback
async () => {
await chatManager.handleInitialLoad();
},
// onMessage callback
(event) => {
chatManager.handleWebSocketMessage(event);
},
// onConnectionStatusChange callback
(status, message) => {
uiManager.updateConnectionStatus(status, message);
}
);

// Initialize chat manager
const chatManager = new ChatManager(websocketManager, uiManager);

// Start WebSocket connection
websocketManager.connect();
});
144 changes: 144 additions & 0 deletions public/pages/index/modules/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
export class UIManager {
constructor() {
this.messagesElement = document.getElementById("messages");
this.messagesLoading = document.getElementById("messages-loading");
this.messageForm = document.getElementById("message-form");
this.messageInput = document.getElementById("message-input");
this.typingIndicator = document.getElementById("typing-indicator");
this.emptyState = document.getElementById("empty-state");
this.sendButton = this.messageForm.querySelector('button[type="submit"]');
this.sendText = this.sendButton.querySelector(".send-text");
this.sendingText = this.sendButton.querySelector(".sending-text");
this.reconnectButton = document.getElementById("reconnect-button");
this.connectionStatus = document.getElementById("connection-status");
this.connectionText = document.getElementById("connection-text");

this.setupTextareaAutoResize();
}

updateConnectionStatus(status, message) {
this.connectionText.textContent = message;
this.reconnectButton.classList.toggle("hidden", status !== "disconnected");

// Disable/enable send button based on connection status
this.sendButton.disabled = status !== "connected";
this.sendButton.classList.toggle("opacity-50", status !== "connected");
this.messageInput.disabled = status !== "connected";

switch (status) {
case "connected":
this.connectionStatus.className = "h-2 w-2 rounded-full bg-green-500";
this.connectionText.className = "text-sm text-green-600 dark:text-green-400";
break;
case "connecting":
this.connectionStatus.className = "h-2 w-2 rounded-full bg-yellow-500";
this.connectionText.className = "text-sm text-yellow-600 dark:text-yellow-400";
break;
case "disconnected":
this.connectionStatus.className = "h-2 w-2 rounded-full bg-red-500";
this.connectionText.className = "text-sm text-red-600 dark:text-red-400";
break;
}
}

createMessageElement(msg) {
const messageElement = document.createElement("div");
messageElement.className =
"bg-gray-50 dark:bg-gray-700 rounded-lg p-4 transition-colors duration-200 mb-4 last:mb-0";

// Sanitize and render markdown
const renderedContent = marked.parse(msg.content);

messageElement.innerHTML = `
<div class="flex justify-between items-start">
<span class="font-medium text-blue-600 dark:text-blue-400">${msg.username}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">${new Date(
msg.timestamp || new Date()
).toLocaleTimeString()}</span>
</div>
<div class="mt-2 text-gray-800 dark:text-gray-200">${renderedContent}</div>
`;
return messageElement;
}

appendMessage(msg) {
const messageElement = this.createMessageElement(msg);
this.messagesElement.appendChild(messageElement);
this.updateEmptyState();
if (this.isNearBottom()) {
this.scrollToBottom(true);
}
}

updateTypingIndicator(message) {
this.typingIndicator.textContent = message;
}

isNearBottom() {
const threshold = 150; // pixels from bottom
return (
this.messagesElement.scrollHeight -
this.messagesElement.scrollTop -
this.messagesElement.clientHeight <=
threshold
);
}

scrollToBottom(smooth = false) {
this.messagesElement.scrollTo({
top: this.messagesElement.scrollHeight,
behavior: smooth ? "smooth" : "auto"
});
}

updateEmptyState() {
if (this.messagesElement.children.length === 1) {
// Only empty-state div present
this.emptyState.classList.remove("hidden");
} else {
this.emptyState.classList.add("hidden");
}
}

showLoadingState() {
this.messagesElement.style.display = "none";
this.messagesLoading.style.display = "flex";
}

hideLoadingState() {
this.messagesLoading.style.display = "none";
this.messagesElement.style.display = "block";
}

setupTextareaAutoResize() {
this.messageInput.addEventListener("input", () => {
// Reset height to auto to get proper scrollHeight
this.messageInput.style.height = "auto";
// Set new height but cap it at max height
const maxHeight = 150; // 150px max height
const newHeight = Math.min(this.messageInput.scrollHeight + 2, maxHeight);
this.messageInput.style.height = newHeight + "px";
});
}

getMessageContent() {
return this.messageInput.value.trim();
}

clearMessageInput() {
this.messageInput.value = "";
this.messageInput.style.height = "auto";
}

showSending() {
this.sendText.classList.add("hidden");
this.sendingText.classList.remove("hidden");
this.sendButton.disabled = true;
}

hideSending() {
this.sendText.classList.remove("hidden");
this.sendingText.classList.add("hidden");
this.sendButton.disabled = false;
}
}
Loading

0 comments on commit 40a8f62

Please sign in to comment.