@@ -62,8 +120,10 @@
Users
-
+
+
+
+
Import Project
+
Select a project to load its code into the current room:
+
+
+
+
+
+ Import Now
+ Cancel
+
+
+
+
+
+
\ No newline at end of file
diff --git a/script.js b/script.js
index 25e4e84..21dae9f 100644
--- a/script.js
+++ b/script.js
@@ -1,248 +1,1037 @@
// --- VARIABLES GLOBALES ---
-let socket;
-let myId = null;
-let myUsername = "";
-let room = "";
-let selectedAvatar = "";
-let targetId = null;
+let socket, myId, myUsername, room, myAvatar, authToken, codeEditor;
+let currentProjectId = null, currentProjectName = null;
+let selectedAvatar = null;
let isSignUp = false;
let chatHistory = [];
-let dbUsers = {}; // Almacena {id: {username, avatar}}
-let authToken = null; // JWT
-const HOST = "localhost:3000";
-
-// --- ELEMENTOS DEL DOM ---
-const editor = document.getElementById('editor');
-const chatMessages = document.getElementById("messages-log");
-const msgInput = document.getElementById("msg-input");
-const btnConnect = document.getElementById("btn-connect");
-const linkSwitch = document.getElementById("link-switch");
-
-// --- 1. INICIALIZACIÓN DE AVATARES ---
-const avatarGrid = document.getElementById("avatar-grid");
-for (let i = 1; i <= 8; i++) {
- const img = document.createElement("img");
- // Public Avatar api
- img.src = `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`;
- img.className = i === 1 ? "avatar-option selected" : "avatar-option";
- if(i === 1) selectedAvatar = img.src;
-
- img.onclick = () => {
- document.querySelectorAll(".avatar-option").forEach(el => el.classList.remove("selected"));
- img.classList.add("selected");
- selectedAvatar = img.src;
- };
- avatarGrid.appendChild(img);
-}
-avatarGrid.style.display = "none";
-
-// --- 2. LÓGICA DE AUTENTICACIÓN (LOGIN / SIGN UP) ---
-linkSwitch.onclick = (e) => {
- e.preventDefault();
- isSignUp = !isSignUp;
-
- const title = document.getElementById("auth-title");
- const switchText = document.getElementById("switch-text");
- const roomInput = document.getElementById("room-input");
-
- if (isSignUp) {
- title.innerText = "Create Account";
- btnConnect.innerText = "Register & Join";
- switchText.innerHTML = 'Already have an account?
Log In ';
- avatarGrid.style.display = "grid";
- roomInput.style.display = "none";
- } else {
- title.innerText = "Join SynCode Room";
- btnConnect.innerText = "Connect & Sync";
- switchText.innerHTML = 'Don\'t have an account?
Sign Up ';
- avatarGrid.style.display = "none";
- roomInput.style.display = "block";
- }
- document.getElementById("link-switch").onclick = linkSwitch.onclick;
-};
+let dbUsers = {};
+let remoteCursors = {}; // { userId: { username, cursor, selection, marker, widget } }
+let lastCursorUpdate = 0; // Throttle control
+let contentLoaded = false; // Track if content has been loaded
+const HOST = window.location.hostname + ":3000";
-btnConnect.onclick = async () => {
- const usernameInput = document.getElementById("username-input").value;
- const passwordInput = document.getElementById("password-input").value;
- const roomInput = document.getElementById("room-input").value;
+// --- 1. INICIALIZACIÓN SEGURA ---
+document.addEventListener("DOMContentLoaded", () => {
+ const editorTextArea = document.getElementById('editor');
+ const avatarGrid = document.getElementById("avatar-grid");
+ const linkSwitch = document.getElementById("link-switch");
+ const btnConnect = document.getElementById("btn-connect");
+ const msgInput = document.getElementById("msg-input");
+ const btnSend = document.getElementById("btn-send");
- if (!usernameInput || !passwordInput || (isSignUp && !selectedAvatar) || (!isSignUp && !roomInput)) {
- alert("Please fill in all fields.");
- return;
+ // A. Reconexión automática
+ authToken = localStorage.getItem("authToken");
+ myUsername = localStorage.getItem("myUsername");
+ myAvatar = localStorage.getItem("myAvatar");
+ room = localStorage.getItem("room");
+
+ if (authToken && myUsername) {
+ if (room) connectWebSocket();
+ else showLobby();
}
- const endpoint = isSignUp ? '/api/auth/register' : '/api/auth/login';
- const payload = { username: usernameInput, password: passwordInput, avatar: selectedAvatar };
+ // B. Inicializar CodeMirror
+ if (editorTextArea) {
+ codeEditor = CodeMirror.fromTextArea(editorTextArea, {
+ lineNumbers: true, mode: "python", theme: "dracula",
+ tabSize: 4, indentUnit: 4, lineWrapping: true
+ });
+ codeEditor.on("change", (instance, change) => {
+ if (change.origin !== "setValue" && socket?.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify({ type: 'code-update', content: instance.getValue() }));
+ }
+ });
- try {
- const response = await fetch(`http://${HOST}${endpoint}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
+ // Cursor activity tracking with throttle (50ms)
+ codeEditor.on("cursorActivity", (instance) => {
+ const now = Date.now();
+ if (now - lastCursorUpdate < 50) return; // Throttle
+ lastCursorUpdate = now;
+
+ const cursor = instance.getCursor();
+ let selection = null;
+
+ if (instance.somethingSelected()) {
+ const selections = instance.listSelections();
+ if (selections.length > 0) {
+ const sel = selections[0];
+
+ // Use anchor and head (CodeMirror 5 standard)
+ const from = sel.anchor.line < sel.head.line ||
+ (sel.anchor.line === sel.head.line && sel.anchor.ch < sel.head.ch)
+ ? sel.anchor : sel.head;
+ const to = from === sel.anchor ? sel.head : sel.anchor;
+
+ selection = [
+ { line: from.line, ch: from.ch },
+ { line: to.line, ch: to.ch }
+ ];
+ }
+ }
+
+ if (socket?.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify({
+ type: 'cursor-update',
+ userId: myId,
+ cursor: cursor,
+ selection: selection
+ }));
+ }
});
+ }
- const data = await response.json();
+ // C. Avatares
+ if (avatarGrid) {
+ for (let i = 1; i <= 8; i++) {
+ const img = document.createElement("img");
+ img.src = `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`;
+ img.className = i === 1 ? "avatar-option selected" : "avatar-option";
+ if(i === 1) selectedAvatar = img.src;
+ img.onclick = () => {
+ document.querySelectorAll(".avatar-option").forEach(el => el.classList.remove("selected"));
+ img.classList.add("selected");
+ selectedAvatar = img.src;
+ };
+ avatarGrid.appendChild(img);
+ }
+ avatarGrid.style.display = "none";
+ }
- if (response.ok) {
+ // D. Eventos UI & Auth
+ if (linkSwitch) {
+ linkSwitch.onclick = (e) => {
+ e.preventDefault();
+ isSignUp = !isSignUp;
+ const title = document.getElementById("auth-title");
if (isSignUp) {
- alert("Account created! Now you can log in.");
- linkSwitch.click();
+ title.innerText = "Create Account";
+ btnConnect.innerText = "Register & Join";
+ document.getElementById("switch-text").innerHTML = 'Already have an account?
Log In ';
+ avatarGrid.style.display = "grid";
} else {
- authToken = data.token;
- myUsername = data.username;
- selectedAvatar = data.avatar;
- room = roomInput;
-
- localStorage.setItem('syncode_token', authToken);
- connectWebSocket();
+ title.innerText = "Enter SynCode";
+ btnConnect.innerText = "Connect & Sync";
+ document.getElementById("switch-text").innerHTML = 'Don\'t have an account?
Sign Up ';
+ avatarGrid.style.display = "none";
}
- } else {
- alert(data.error || "Authentication failed");
- }
- } catch (error) {
- alert("Error connecting to server.");
+ document.getElementById("link-switch").onclick = linkSwitch.onclick;
+ };
}
-};
-// --- 3. WEBSOCKET ---
+ if (btnConnect) btnConnect.onclick = handleAuth;
+ if (btnSend) btnSend.onclick = sendMessage;
+ if (msgInput) msgInput.onkeydown = (e) => { if (e.key === "Enter") sendMessage(); };
-function connectWebSocket() {
- socket = new WebSocket(`ws://${HOST}/room/${room}`);
- setupSocket();
+ // E. Emojis
+ document.querySelectorAll(".emoji-btn").forEach(btn => {
+ btn.onclick = () => { if(msgInput) { msgInput.value += btn.innerText; msgInput.focus(); } };
+ });
+
+ // F. Botones de Modal Proyectos
+ document.getElementById("btn-create-room-cancel").onclick = () => {
+ document.getElementById("create-room-modal").style.display = "none";
+ };
+ document.getElementById("btn-cancel-import").onclick = () => {
+ document.getElementById("import-project-modal").style.display = "none";
+ };
+
+ // G. Download button
+ document.getElementById("btn-download").onclick = downloadCode;
+});
+
+// --- 2. AUTENTICACIÓN Y WEBSOCKET ---
+
+function handleExpiredToken() {
+ alert("Tu sesión ha expirado. Por favor, inicia sesión nuevamente.");
+ localStorage.removeItem("authToken");
+ localStorage.removeItem("myUsername");
+ localStorage.removeItem("myAvatar");
+ localStorage.removeItem("room");
+ location.reload();
}
-function setupSocket() {
- socket.onopen = () => {
- socket.send(JSON.stringify({
- type: 'login',
- username: myUsername,
- avatar: selectedAvatar,
- token: authToken
- }));
-
+async function secureFetch(url, options = {}) {
+ const response = await fetch(url, options);
+ if (response.status === 403) {
+ handleExpiredToken();
+ throw new Error("Token expired");
+ }
+ return response;
+}
+
+async function handleAuth() {
+ const userIn = document.getElementById("username-input").value;
+ const passIn = document.getElementById("password-input").value;
+ if (!userIn || (!isSignUp && !passIn)) return alert("Fill all fields");
+
+ const endpoint = isSignUp ? '/api/auth/register' : '/api/auth/login';
+ try {
+ const resp = await fetch(`http://${HOST}${endpoint}`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username: userIn, password: passIn, avatar: selectedAvatar})
+ });
+ const data = await resp.json();
+ if (resp.ok) {
+ if (isSignUp) {
+ alert("Account created! Please log in.");
+ document.getElementById("link-switch").click();
+ } else {
+ authToken = data.token;
+ myUsername = data.username;
+ myAvatar = data.avatar;
+ localStorage.setItem("authToken", authToken);
+ localStorage.setItem("myUsername", myUsername);
+ localStorage.setItem("myAvatar", myAvatar);
+ showLobby();
+ }
+ } else alert(data.error);
+ } catch (e) { alert("Server error connecting to API"); }
+}
+
+function connectWebSocket() {
+ socket = new WebSocket(`ws://${HOST}/room/${room}?token=${authToken}`);
+
+ socket.onopen = async () => {
+ socket.send(JSON.stringify({ type: 'login', username: myUsername, avatar: myAvatar }));
document.getElementById("login-screen").style.display = "none";
document.getElementById("chat-app").style.display = "flex";
document.getElementById("room-display").innerText = "Room: " + room;
+ document.getElementById("lobby-screen").style.display = "none";
updateUserList();
+
+ // Load current project info
+ await loadProjectInfo();
+
+ // Fallback: if no code arrives after 1s, load from endpoint
+ setTimeout(async () => {
+ if (!contentLoaded) {
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/rooms/${room}/content`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const data = await resp.json();
+ if (data.content && !contentLoaded) {
+ codeEditor.setValue(data.content);
+ contentLoaded = true;
+ }
+ } catch (e) { console.error("Error loading project content", e); }
+ }
+ }, 1000);
+
+ setTimeout(() => codeEditor.refresh(), 100);
+ };
+
+ socket.onmessage = handleSocketMessage;
+ socket.onclose = (e) => {
+ if (e.code === 4004) {
+ localStorage.removeItem("room");
+ alert("Room does not exist.");
+ };
+ contentLoaded = false;
+ location.reload();
};
+}
+
+function handleSocketMessage(event) {
+ const data = JSON.parse(event.data);
+ switch (data.type) {
+ case 'set-id':
+ myId = data.id;
+ break;
+ case 'existing-users':
+ if (data.users && Array.isArray(data.users)) {
+ data.users.forEach(user => {
+ if (user.id !== myId) {
+ dbUsers[user.id] = { username: user.username, avatar: user.avatar };
+ }
+ });
+ updateUserList();
- socket.onmessage = (event) => {
- const data = JSON.parse(event.data);
- switch (data.type) {
- case 'set-id': myId = data.id; break;
- case 'user-connected':
- appendMessage("System", `User ${data.id} joined`, "#5865F2", false);
- checkAndSendHistory(data.id);
- break;
- case 'login':
+ // Request code from the oldest user immediately
+ const oldestUser = data.users.reduce((oldest, current) =>
+ current.id < oldest.id ? current : oldest
+ );
+ if (oldestUser && socket?.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify({
+ type: 'code-request',
+ targetId: oldestUser.id
+ }));
+ }
+ }
+ break;
+ case 'user-connected':
+ if (data.id !== myId) {
+ dbUsers[data.id] = { username: data.username, avatar: data.avatar };
+ updateUserList();
+ }
+ checkAndSendHistory(data.id);
+ break;
+ case 'login':
+ if (data.authorId !== myId) {
dbUsers[data.authorId] = { username: data.username, avatar: data.avatar };
updateUserList();
- break;
- case 'history-sync':
- editor.value = data.code;
+ }
+ break;
+ case 'code-response':
+ codeEditor.setValue(data.code);
+ contentLoaded = true;
+ if (data.chat) {
data.chat.forEach(m => appendMessage(m.user, m.text, getUsernameColor(m.user), false));
chatHistory = data.chat;
- dbUsers = { ...dbUsers, ...data.users };
- updateUserList();
- break;
- case 'code-update':
- if (data.content !== editor.value) editor.value = data.content;
- break;
- case 'chat':
- appendMessage(data.user, data.text, getUsernameColor(data.user), false);
- chatHistory.push({ user: data.user, text: data.text });
- break;
- case 'user-disconnected':
- appendMessage("System", `User ${data.id} left`, "#ff4444", false);
- delete dbUsers[data.id];
+ }
+ if (data.users) {
+ const filteredUsers = Object.keys(data.users).reduce((acc, userId) => {
+ if (parseInt(userId) !== myId) {
+ acc[userId] = data.users[userId];
+ }
+ return acc;
+ }, {});
+ dbUsers = { ...dbUsers, ...filteredUsers };
updateUserList();
- break;
+ }
+ break;
+ case 'send-code-request':
+ // Another user is requesting my code
+ if (socket?.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify({
+ type: 'code-response',
+ targetId: data.targetId,
+ code: codeEditor.getValue(),
+ chat: chatHistory,
+ users: { [myId]: { username: myUsername, avatar: myAvatar }, ...dbUsers }
+ }));
+ }
+ break;
+ case 'history-sync':
+ codeEditor.setValue(data.code);
+ contentLoaded = true;
+ data.chat.forEach(m => appendMessage(m.user, m.text, getUsernameColor(m.user), false));
+ chatHistory = data.chat;
+ // Filtrar para que no incluya al usuario actual
+ const filteredUsers = Object.keys(data.users).reduce((acc, userId) => {
+ if (parseInt(userId) !== myId) {
+ acc[userId] = data.users[userId];
+ }
+ return acc;
+ }, {});
+ dbUsers = { ...dbUsers, ...filteredUsers };
+ updateUserList();
+ break;
+ case 'code-update':
+ if (data.content !== codeEditor.getValue()) {
+ const cur = codeEditor.getCursor();
+ codeEditor.setValue(data.content);
+ codeEditor.setCursor(cur);
+ clearAllRemoteCursors(); // Clear cursors when code changes
+ }
+ break;
+ case 'cursor-update':
+ if (data.userId !== myId) {
+ updateRemoteCursor(data.userId, data.cursor, data.selection);
+ }
+ break;
+ case 'chat':
+ appendMessage(data.user, data.text, getUsernameColor(data.user), false);
+ chatHistory.push({ user: data.user, text: data.text });
+ break;
+ case 'project-changed':
+ currentProjectId = data.projectId;
+ currentProjectName = data.projectName;
+ if (data.content !== undefined) {
+ codeEditor.setValue(data.content);
+ }
+ updateProjectDisplay();
+ break;
+ case 'user-disconnected':
+ delete dbUsers[data.id];
+ clearRemoteCursor(data.id); // Clean up remote cursor
+ updateUserList();
+ break;
+ }
+}
+
+async function loadProjectInfo() {
+ try {
+ // Fetch all rooms to find the current one
+ const roomsResp = await secureFetch(`http://${HOST}/api/rooms`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const rooms = await roomsResp.json();
+ const currentRoom = rooms.find(r => r.room_name === room);
+
+ if (currentRoom && currentRoom.actual_project_id) {
+ currentProjectId = currentRoom.actual_project_id;
+
+ // Fetch the project info (works even if user doesn't own it)
+ const projectResp = await secureFetch(`http://${HOST}/api/projects/${currentProjectId}/info`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const project = await projectResp.json();
+
+ if (project && project.project_name) {
+ currentProjectName = project.project_name;
+ updateProjectDisplay();
+ }
+ } else {
+ currentProjectId = null;
+ currentProjectName = null;
+ document.getElementById("project-display").innerText = "Project: (None)";
}
- };
+ } catch (e) {
+ console.error("Error loading project info", e);
+ }
+}
+
+function updateProjectDisplay() {
+ if (currentProjectName) {
+ document.getElementById("project-display").innerText = "Project: " + currentProjectName;
+ } else {
+ document.getElementById("project-display").innerText = "Project: (None)";
+ }
}
-// --- 4. SINCRONIZACIÓN DEL EDITOR ---
+// --- 3. GESTIÓN DE SALAS Y LOBBY ---
-editor.addEventListener('input', () => {
- socket.send(JSON.stringify({ type: 'code-update', content: editor.value }));
-});
+async function showLobby() {
+ document.getElementById("login-screen").style.display = "none";
+ document.getElementById("chat-app").style.display = "none";
+ document.getElementById("lobby-screen").style.display = "flex";
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/rooms`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ renderRooms(await resp.json());
+ } catch (e) { alert("Error loading rooms"); }
+}
+
+function renderRooms(roomsArray) {
+ const container = document.getElementById("rooms-container");
+ container.innerHTML = "";
+
+ if (roomsArray.length === 0) {
+ const p = document.createElement("p");
+ p.textContent = "No rooms found.";
+ container.appendChild(p);
+ return;
+ }
+
+ roomsArray.forEach(r => {
+ const card = document.createElement("div");
+ card.className = "room-card";
+
+ const h4 = document.createElement("h4");
+ h4.textContent = r.room_name;
+ card.appendChild(h4);
+
+ const p = document.createElement("p");
+ p.textContent = r.description || '';
+ card.appendChild(p);
+
+ card.onclick = () => {
+ room = r.room_name;
+ localStorage.setItem("room", room);
+ connectWebSocket();
+ };
+ container.appendChild(card);
+ });
+}
+
+document.getElementById("btn-open-create-room").onclick = async () => {
+ const select = document.getElementById("select-project-choice");
+ select.innerHTML = '
-- Create New Empty Project -- ';
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const projects = await resp.json();
+ projects.forEach(p => {
+ const opt = document.createElement("option");
+ opt.value = p.id; opt.innerText = p.project_name;
+ select.appendChild(opt);
+ });
+ } catch (e) { console.error(e); }
+ document.getElementById("create-room-modal").style.display = "flex";
+};
+
+document.getElementById("btn-create-room-save").onclick = async () => {
+ const roomName = document.getElementById("new-room-name").value;
+ const roomDesc = document.getElementById("new-room-desc").value;
+ const projectChoice = document.getElementById("select-project-choice").value;
+ const newProjectName = document.getElementById("new-project-name-input").value;
+
+ if (!roomName) return alert("Room name required");
+
+ let finalProjectId = projectChoice;
+
+ try {
+ // A. Si elige crear un proyecto nuevo
+ if (projectChoice === "new") {
+ if (!newProjectName) return alert("Please name your new project");
+
+ const projResp = await secureFetch(`http://${HOST}/api/projects`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
+ body: JSON.stringify({ project_name: newProjectName })
+ });
+ const projData = await projResp.json();
+ finalProjectId = projData.id; // Obtenemos el ID del proyecto recién creado
+ }
+
+ // B. Crear la sala vinculada al proyecto (nuevo o existente)
+ const roomResp = await secureFetch(`http://${HOST}/api/rooms`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
+ body: JSON.stringify({
+ room_name: roomName,
+ description: roomDesc,
+ actual_project_id: finalProjectId
+ })
+ });
+
+ if (roomResp.ok) {
+ document.getElementById("create-room-modal").style.display = "none";
+ showLobby();
+ } else {
+ alert("Error creating room");
+ }
+ } catch (e) {
+ console.error(e);
+ alert("Server error");
+ }
+};
+
+document.getElementById("select-project-choice").onchange = (e) => {
+ const input = document.getElementById("new-project-name-input");
+ input.style.display = (e.target.value === "new") ? "block" : "none";
+};
+
+// --- 4. IMPORTAR Y GUARDAR PROYECTOS ---
+
+document.getElementById("btn-save").onclick = async () => {
+ const label = prompt("Version label (optional):", "");
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects/save-current`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
+ body: JSON.stringify({
+ room_name: room,
+ content: codeEditor.getValue(),
+ version_label: label || undefined
+ })
+ });
+ if (resp.ok) alert("Saved! ✅");
+ else alert((await resp.json()).error);
+ } catch (e) { alert("Error saving"); }
+};
+
+document.getElementById("btn-import").onclick = async () => {
+ const select = document.getElementById("select-import-project");
+ select.innerHTML = '
-- Select Project -- ';
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const projects = await resp.json();
+
+ for (const p of projects) {
+ const opt = document.createElement("option");
+ opt.value = p.id;
+ opt.innerText = p.project_name;
+ opt.dataset.projectId = p.id;
+ select.appendChild(opt);
+ }
+ document.getElementById("import-project-modal").style.display = "flex";
+ } catch (e) { alert("Error loading projects"); }
+};
+
+document.getElementById("btn-confirm-import").onclick = async () => {
+ const select = document.getElementById("select-import-project");
+ const projectId = parseInt(select.value, 10);
+ if (!projectId) return;
+
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects/${projectId}/history`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const history = await resp.json();
+
+ if (history.length === 0) {
+ alert("Project has no saved versions yet");
+ return;
+ }
+
+ // Get the latest version
+ const latestVersion = history[0];
+ codeEditor.setValue(latestVersion.content_snapshot);
+
+ // Update room's actual_project_id
+ const updateResp = await secureFetch(`http://${HOST}/api/rooms/${room}/project`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
+ body: JSON.stringify({ project_id: projectId })
+ });
+
+ if (updateResp.ok) {
+ // Update the current project variables and display
+ const projResp = await secureFetch(`http://${HOST}/api/projects`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const projects = await projResp.json();
+ const project = projects.find(p => p.id === projectId);
+ if (project) {
+ currentProjectId = projectId;
+ currentProjectName = project.project_name;
+ updateProjectDisplay();
+
+ // Broadcast to all users in the room
+ if (socket?.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify({
+ type: 'project-changed',
+ projectId: projectId,
+ projectName: project.project_name,
+ content: codeEditor.getValue()
+ }));
+ }
+ }
+ } else {
+ console.error("Failed to link project to room");
+ }
+
+ document.getElementById("import-project-modal").style.display = "none";
+ } catch (e) {
+ alert("Error loading project content");
+ console.error(e);
+ }
+};
+
+// --- 6. PROJECTS MENU ---
+
+document.getElementById("btn-projects").onclick = async () => {
+ document.getElementById("projects-menu-modal").style.display = "flex";
+ await loadProjectsMenu();
+};
+
+document.getElementById("btn-close-projects-menu").onclick = () => {
+ document.getElementById("projects-menu-modal").style.display = "none";
+};
+
+async function loadProjectsMenu() {
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const projects = await resp.json();
+
+ const projectsList = document.getElementById("projects-list");
+ projectsList.innerHTML = "";
+
+ if (projects.length === 0) {
+ projectsList.innerHTML = '
No projects yet
';
+ return;
+ }
+
+ projects.forEach(p => {
+ const btn = document.createElement("button");
+ btn.className = "tool-btn";
+ btn.style.cssText = "text-align: left; padding: 12px; border: 1px solid #555; justify-content: flex-start;";
+
+ const strong = document.createElement("strong");
+ strong.textContent = p.project_name;
+ btn.appendChild(strong);
+
+ btn.appendChild(document.createElement("br"));
+
+ const small = document.createElement("small");
+ small.style.cssText = "color: #888; font-size: 0.85em;";
+ small.textContent = new Date(p.updated_at).toLocaleDateString();
+ btn.appendChild(small);
+
+ btn.onclick = () => loadProjectVersions(p.id, p.project_name);
+ projectsList.appendChild(btn);
+ });
+ } catch (e) {
+ console.error("Error loading projects menu", e);
+ const projectsList = document.getElementById("projects-list");
+ projectsList.innerHTML = "";
+ const p = document.createElement("p");
+ p.style.color = "red";
+ p.textContent = "Error loading projects";
+ projectsList.appendChild(p);
+ }
+}
+
+async function loadProjectVersions(projectId, projectName) {
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects/${projectId}/history`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const history = await resp.json();
+
+ document.getElementById("selected-project-name").innerText = `${projectName} - Versions`;
+ document.getElementById("versions-section").style.display = "block";
+ document.getElementById("no-selection").style.display = "none";
+
+ const versionsList = document.getElementById("versions-list");
+ versionsList.innerHTML = "";
+
+ if (history.length === 0) {
+ versionsList.innerHTML = '
No versions saved yet
';
+ return;
+ }
+
+ history.forEach((v, idx) => {
+ const div = document.createElement("div");
+ div.className = "tool-btn";
+ div.style.cssText = "text-align: left; padding: 12px; border: 1px solid #555; cursor: pointer; display: flex; justify-content: space-between; align-items: center;";
+ div.onclick = () => {
+ // Remove active class from all versions
+ document.querySelectorAll("#versions-list > div").forEach(d => d.classList.remove("active"));
+ // Add active class to this version
+ div.classList.add("active");
+ clickVersion(v.id, projectId);
+ };
+ const label = v.version_label || `Auto-save ${idx + 1}`;
+ const date = new Date(v.saved_at).toLocaleDateString();
+
+ // Left side: version info
+ const leftDiv = document.createElement("div");
+
+ const strong = document.createElement("strong");
+ strong.textContent = label;
+ leftDiv.appendChild(strong);
+
+ leftDiv.appendChild(document.createElement("br"));
+
+ const small = document.createElement("small");
+ small.style.color = "#888";
+ small.textContent = `by ${v.username} • ${date}`;
+ leftDiv.appendChild(small);
+
+ div.appendChild(leftDiv);
+
+ // Right side: buttons
+ const rightDiv = document.createElement("div");
+ rightDiv.style.cssText = "display: flex; gap: 8px;";
+
+ const restoreBtn = document.createElement("button");
+ restoreBtn.className = "tool-btn success";
+ restoreBtn.style.cssText = "padding: 6px 12px; font-size: 0.9em;";
+ restoreBtn.textContent = "Restore";
+ restoreBtn.onclick = () => restoreVersion(projectId, v.id, label);
+ rightDiv.appendChild(restoreBtn);
+
+ div.appendChild(rightDiv);
+ versionsList.appendChild(div);
+ });
+
+ // Show preview of latest version by default
+ if (history.length > 0) {
+ // Add active class to first version
+ const firstVersionDiv = document.querySelector("#versions-list > div");
+ if (firstVersionDiv) firstVersionDiv.classList.add("active");
+ showVersionPreview(history[0].id, projectId);
+ }
+ } catch (e) {
+ console.error("Error loading versions", e);
+ document.getElementById("versions-list").innerHTML = '
Error loading versions
';
+ }
+}
+
+async function clickVersion(historyId, projectId) {
+ await showVersionPreview(historyId, projectId);
+ document.getElementById("versions-section").scrollIntoView({ behavior: "smooth", block: "end" });
+}
+
+async function showVersionPreview(historyId, projectId) {
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects/${projectId}/history/${historyId}`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const version = await resp.json();
+ document.getElementById("version-preview").value = version.content_snapshot || "(empty)";
+
+ } catch (e) {
+ console.error("Error loading preview", e);
+ document.getElementById("version-preview").value = "Error loading preview";
+ }
+}
+
+async function restoreVersion(projectId, historyId, label) {
+ const confirm = window.confirm(`Restore version: "${label}"?`);
+ if (!confirm) return;
+
+ try {
+ const resp = await secureFetch(`http://${HOST}/api/projects/${projectId}/history/${historyId}/restore`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }
+ });
+
+ if (resp.ok) {
+ alert("Version restored! ✅");
+
+ // If this is the current project, update the editor and sync to all users
+ if (projectId === currentProjectId) {
+ const histResp = await secureFetch(`http://${HOST}/api/projects/${projectId}/history/${historyId}`, {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+ const version = await histResp.json();
+ codeEditor.setValue(version.content_snapshot);
+
+ // Broadcast to all users in the room
+ if (socket?.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify({
+ type: 'code-update',
+ content: version.content_snapshot
+ }));
+ }
+ }
+
+ // Reload versions list
+ await loadProjectVersions(projectId, document.getElementById("selected-project-name").innerText.split(" - ")[0]);
+
+ // Close the projects menu
+ document.getElementById("projects-menu-modal").style.display = "none";
+ } else {
+ alert("Error restoring version");
+ }
+ } catch (e) {
+ console.error("Error restoring version", e);
+ alert("Error restoring version");
+ }
+}
+
+// --- 5. FUNCIONES AUXILIARES ---
function checkAndSendHistory(newId) {
- const userIds = Object.keys(dbUsers).map(id => parseInt(id));
- const isOldest = userIds.every(id => id >= myId);
- if (isOldest && socket.readyState === WebSocket.OPEN) {
+ // Only the oldest user (lowest ID) in the room sends
+ const ids = Object.keys(dbUsers).map(Number);
+ const allIds = [myId, ...ids];
+ const minId = Math.min(...allIds);
+
+ if (myId === minId && socket?.readyState === 1) {
socket.send(JSON.stringify({
type: 'history-sync', targetId: newId,
- code: editor.value, chat: chatHistory,
- users: { [myId]: { username: myUsername, avatar: selectedAvatar }, ...dbUsers }
+ code: codeEditor.getValue(), chat: chatHistory,
+ users: { [myId]: { username: myUsername, avatar: myAvatar }, ...dbUsers }
}));
}
}
-// --- 5. CHAT Y UI ---
+function sendMessage() {
+ const inp = document.getElementById("msg-input");
+ if (!inp.value.trim()) return;
+ const msg = { type: 'chat', user: myUsername, text: inp.value };
+ socket.send(JSON.stringify(msg));
+ appendMessage(myUsername, inp.value, getUsernameColor(myUsername), true);
+ chatHistory.push({ user: myUsername, text: inp.value });
+ inp.value = "";
+}
function appendMessage(user, text, color, isOwn) {
+ const log = document.getElementById("messages-log");
const div = document.createElement("div");
- const isMe = user === myUsername || isOwn;
- div.className = `message-row ${isMe ? 'own-message' : 'other-message'}`;
- div.innerHTML = `
${user} ${text}
`;
- chatMessages.appendChild(div);
- chatMessages.scrollTop = chatMessages.scrollHeight;
+ div.className = `message-row ${user === myUsername || isOwn ? 'own-message' : 'other-message'}`;
+
+ const bubble = document.createElement("div");
+ bubble.className = "bubble";
+
+ const username = document.createElement("strong");
+ username.style.color = color;
+ username.textContent = user;
+ bubble.appendChild(username);
+
+ bubble.appendChild(document.createElement("br"));
+
+ const textNode = document.createTextNode(text);
+ bubble.appendChild(textNode);
+
+ div.appendChild(bubble);
+ log.appendChild(div);
+ log.scrollTop = log.scrollHeight;
}
function updateUserList() {
const list = document.getElementById("users-list");
list.innerHTML = "";
-
- // Usuario local
- const selfLi = document.createElement("li");
- selfLi.className = "user-item";
- selfLi.innerHTML = `
${myUsername} (You) `;
- list.appendChild(selfLi);
-
- // Otros usuarios
+
+ // Add current user
+ const li = document.createElement("li");
+ li.className = "user-item";
+
+ const img = document.createElement("img");
+ img.src = myAvatar;
+ img.className = "avatar";
+ li.appendChild(img);
+
+ const span = document.createElement("span");
+ span.textContent = `${myUsername} (You)`;
+ li.appendChild(span);
+
+ list.appendChild(li);
+
+ // Add other users
for (let id in dbUsers) {
- const user = dbUsers[id];
+ const u = dbUsers[id];
const li = document.createElement("li");
li.className = "user-item";
- li.innerHTML = `
${user.username} `;
+
+ const img = document.createElement("img");
+ img.src = u.avatar;
+ img.className = "avatar";
+ li.appendChild(img);
+
+ const span = document.createElement("span");
+ span.textContent = u.username;
+ li.appendChild(span);
+
list.appendChild(li);
}
}
-// --- 6. CONTROLES DE INTERFAZ ---
-
-function toggleSidebar() {
- const layout = document.getElementById("main-layout");
- const btnShow = document.getElementById("btn-show-sidebar");
- layout.classList.toggle("sidebar-hidden");
- btnShow.style.display = layout.classList.contains("sidebar-hidden") ? "block" : "none";
+function changeLanguage() {
+ const lang = codeEditor.getOption("mode") === "python" ? "javascript" : "python";
+ codeEditor.setOption("mode", lang);
+ const indicator = document.getElementById("lang-indicator");
+ indicator.innerText = lang === "python" ? "🐍 Python" : "🟨 JavaScript";
+ indicator.style.backgroundColor = lang === "python" ? "#3776ab" : "#f0db4f";
}
-function toggleChat() {
- document.getElementById("chat-collapsible").classList.toggle("chat-hidden");
+function toggleSidebar() { document.getElementById("main-layout").classList.toggle("sidebar-hidden"); }
+function toggleChat() { document.getElementById("chat-collapsible").classList.toggle("chat-hidden"); }
+function logout() { localStorage.clear(); location.reload(); }
+function changeRoom() { localStorage.removeItem("room"); location.reload(); }
+function downloadCode() {
+ const code = codeEditor.getValue();
+ const lang = codeEditor.getOption("mode");
+ const ext = lang === "python" ? "py" : "js";
+ const filename = currentProjectName ? `${currentProjectName}.${ext}` : `code.${ext}`;
+
+ const blob = new Blob([code], { type: "text/plain" });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+}
+function getUsernameColor(u) {
+ let hash = 0;
+ for (let i = 0; i < u.length; i++) hash = u.charCodeAt(i) + ((hash << 5) - hash);
+ return `hsl(${Math.abs(hash % 360)}, 70%, 60%)`;
}
-function sendMessage() {
- const text = msgInput.value.trim();
- if (!text) return;
- socket.send(JSON.stringify({ type: 'chat', user: myUsername, text: text }));
- appendMessage(myUsername, text, getUsernameColor(myUsername), true);
- chatHistory.push({ user: myUsername, text: text });
- msgInput.value = "";
+// --- REMOTE CURSORS & SELECTION RENDERING ---
+
+function updateRemoteCursor(userId, cursor, selection) {
+ if (!cursor) return;
+
+ // Validate cursor position within document bounds
+ const lineCount = codeEditor.lineCount();
+ if (cursor.line < 0 || cursor.line >= lineCount) return;
+
+ const username = dbUsers[userId]?.username || `User ${userId}`;
+ renderRemoteCursor(userId, username, cursor, selection);
}
-document.getElementById("btn-send").onclick = sendMessage;
-msgInput.addEventListener("keydown", (e) => { if (e.key === "Enter") sendMessage(); });
+function renderRemoteCursor(userId, username, cursor, selection) {
+ // Clear old cursors for this user
+ if (remoteCursors[userId]) {
+ if (remoteCursors[userId].marker) remoteCursors[userId].marker.clear();
+ if (remoteCursors[userId].widget) remoteCursors[userId].widget.clear();
+ // Remove injected style
+ const style = document.getElementById(`remote-cursor-style-${userId}`);
+ if (style) style.remove();
+ }
-document.querySelectorAll(".emoji-btn").forEach(btn => {
- btn.onclick = () => { msgInput.value += btn.innerText; msgInput.focus(); };
-});
+ const color = getUsernameColor(username);
+ let marker = null;
-// --- UTILIDADES ---
+ // Render selection as a text marker
+ if (selection && selection.length === 2) {
+ const from = selection[0];
+ const to = selection[1];
-function getUsernameColor(username) {
- let hash = 0;
- for (let i = 0; i < username.length; i++) hash = username.charCodeAt(i) + ((hash << 5) - hash);
- return `hsl(${Math.abs(hash % 360)}, 70%, 60%)`;
+ // Validate selection positions
+ const lineCount = codeEditor.lineCount();
+ if (from && to &&
+ from.line >= 0 && from.line < lineCount &&
+ to.line >= 0 && to.line < lineCount &&
+ from.ch !== undefined && to.ch !== undefined) {
+
+ // Create a unique CSS class for this user's selection
+ const className = `remote-selection-${userId}`;
+
+ // Use a contrasting pink background for better visibility with any text color
+ const bgColor = `rgba(255, 100, 200, 0.3)`;
+
+ // Inject CSS style - no opacity, just a light background color
+ const style = document.createElement('style');
+ style.id = `remote-cursor-style-${userId}`;
+ style.textContent = `.${className} { background-color: ${bgColor} !important; }`;
+ document.head.appendChild(style);
+
+ marker = codeEditor.markText(
+ from,
+ to,
+ {
+ className: className,
+ inclusiveRight: false
+ }
+ );
+ }
+ }
+
+ // Render cursor widget (username label)
+ const widgetElement = document.createElement('span');
+ widgetElement.className = 'remote-cursor-label';
+ widgetElement.textContent = username;
+ widgetElement.style.backgroundColor = color;
+ widgetElement.style.color = '#000';
+ widgetElement.style.padding = '1px 4px';
+ widgetElement.style.borderRadius = '3px';
+ widgetElement.style.fontSize = '10px';
+ widgetElement.style.fontWeight = 'bold';
+ widgetElement.style.whiteSpace = 'nowrap';
+ widgetElement.style.display = 'inline-block';
+ widgetElement.style.position = 'absolute';
+ widgetElement.style.margin = '0';
+ widgetElement.style.pointerEvents = 'none';
+
+ const bookmark = codeEditor.setBookmark(
+ { line: cursor.line, ch: cursor.ch },
+ { widget: widgetElement, insertLeft: true }
+ );
+
+ // Store references
+ remoteCursors[userId] = {
+ username: username,
+ cursor: cursor,
+ selection: selection,
+ marker: marker,
+ widget: bookmark
+ };
+}
+
+function clearRemoteCursor(userId) {
+ if (remoteCursors[userId]) {
+ if (remoteCursors[userId].marker) {
+ remoteCursors[userId].marker.clear();
+ }
+ if (remoteCursors[userId].widget) {
+ remoteCursors[userId].widget.clear();
+ }
+ // Remove injected style
+ const style = document.getElementById(`remote-cursor-style-${userId}`);
+ if (style) style.remove();
+
+ delete remoteCursors[userId];
+ }
+}
+
+function clearAllRemoteCursors() {
+ for (let userId in remoteCursors) {
+ clearRemoteCursor(userId);
+ }
}
\ No newline at end of file
diff --git a/style.css b/style.css
index 15db38a..87927dc 100644
--- a/style.css
+++ b/style.css
@@ -1,15 +1,15 @@
+/* --- GLOBAL & RESET --- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
-html,
-body {
+html, body {
height: 100%;
width: 100%;
overflow: hidden;
- font-family: 'Segoe UI', sans-serif;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #121212;
color: white;
}
@@ -57,9 +57,7 @@ body {
transform: scale(1.1);
}
-#username-input,
-#password-input,
-#room-input {
+#username-input, #password-input, #room-input {
width: 100%;
padding: 10px;
margin-top: 10px;
@@ -67,6 +65,7 @@ body {
border: none;
background: #2c2c2c;
color: white;
+ outline: none;
}
#btn-connect {
@@ -79,18 +78,11 @@ body {
border-radius: 6px;
cursor: pointer;
font-weight: bold;
+ transition: background 0.2s;
}
-.auth-switch {
- margin-top: 15px;
- font-size: 0.85em;
- color: #b9bbbe;
-}
-
-#link-switch {
- color: #5865F2;
- text-decoration: none;
- font-weight: bold;
+#btn-connect:hover {
+ background: #4752c4;
}
/* --- MAIN LAYOUT --- */
@@ -131,9 +123,43 @@ body {
list-style: none;
padding: 10px;
overflow-y: auto;
+ flex-grow: 1;
+}
+
+/* --- NUEVA SECCIÓN: BOTONES INFERIORES SIDEBAR --- */
+.sidebar-footer {
+ padding: 15px;
+ border-top: 1px solid #333;
+ background: #1a1a1a;
+}
+
+.footer-buttons {
+ display: flex;
+ gap: 8px; /* Espacio entre los dos botones */
+}
+
+.footer-buttons .logout-btn,
+.footer-buttons .tool-btn {
+ flex: 1; /* Ambos botones ocupan el mismo ancho */
+ padding: 8px 2px;
+ font-size: 12px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+}
+
+/* Estilo específico para botón de sala (azul suave/grisáceo) */
+.tool-btn.secondary {
+ background: #4f545c;
+ color: white;
}
-/* Main Content Area */
+.tool-btn.secondary:hover {
+ background: #686d73;
+}
+
+/* --- MAIN CONTENT AREA --- */
.main-content {
display: flex;
flex-direction: column;
@@ -142,7 +168,7 @@ body {
}
.chat-header {
- padding: 10px 15px;
+ padding: 11px 15px;
background: #1e1e1e;
display: flex;
justify-content: space-between;
@@ -156,34 +182,52 @@ body {
gap: 10px;
}
+.editor-toolbar {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.editor-toolbar .tool-btn {
+ padding: 8px 16px;
+ font-size: 12px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
/* Editor Area */
.editor-wrapper {
flex: 1;
position: relative;
- background: #1e1e1e;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ background: #282a36; /* Fondo Dracula */
}
-#editor {
- width: 100%;
- height: 100%;
- background: #1e1e1e;
- color: #d4d4d4;
- border: none;
- padding: 20px;
+.CodeMirror {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ height: 100% !important;
+ width: 100% !important;
font-family: 'Consolas', monospace;
- font-size: 16px;
- resize: none;
- outline: none;
+ font-size: 15px;
}
-/* Collapsible Chat Panel */
+/* Chat Panel */
#chat-collapsible {
- height: 50%;
+ height: 40%;
background: #121212;
display: flex;
flex-direction: column;
transition: height 0.3s ease, opacity 0.2s;
- border-top: 1px solid #333;
+ border-top: 2px solid #333;
}
#chat-collapsible.chat-hidden {
@@ -193,7 +237,6 @@ body {
border: none;
}
-/* Chat Log & Input */
#messages-log {
flex: 1;
overflow-y: auto;
@@ -211,21 +254,6 @@ body {
gap: 10px;
}
-#emoji-bar {
- margin-bottom: 8px;
- display: flex;
- gap: 5px;
-}
-
-.emoji-btn {
- background: #2c2c2c;
- border: none;
- color: white;
- padding: 5px 8px;
- border-radius: 4px;
- cursor: pointer;
-}
-
#msg-input {
flex: 1;
padding: 8px 15px;
@@ -245,7 +273,18 @@ body {
cursor: pointer;
}
-/* UI Elements */
+/* UI ELEMENTS */
+#lang-indicator {
+ background: #3776ab;
+ color: #000000;
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: bold;
+ cursor: pointer;
+ border: 1px solid #ffde57;
+}
+
.toggle-btn {
background: transparent;
border: 1px solid #444;
@@ -253,11 +292,15 @@ body {
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
- font-size: 12px;
}
-#btn-toggle-chat {
- font-size: 20px;
+/* Ocultar botón de mostrar sidebar cuando el sidebar está visible */
+.show-sidebar-btn {
+ display: none;
+}
+
+.sidebar-hidden .show-sidebar-btn {
+ display: inline-block;
}
.tool-btn {
@@ -268,22 +311,25 @@ body {
border-radius: 4px;
font-size: 13px;
cursor: pointer;
- transition: background 0.2s, color 0.2s;
+ transition: all 0.2s ease;
}
.tool-btn:hover {
- background: #444;
- color: #fff;
+ background: #44494f;
+ color: white;
}
-.show-sidebar-btn {
- display: none;
+.logout-btn {
+ background-color: #ed4245; /* Rojo Discord */
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: bold;
}
-.avatar {
- width: 24px;
- height: 24px;
- border-radius: 50%;
+.logout-btn:hover {
+ background-color: #c03537;
}
.user-item {
@@ -292,12 +338,12 @@ body {
gap: 10px;
padding: 8px;
border-radius: 6px;
- cursor: pointer;
- font-size: 0.9em;
}
-.user-item:hover {
- background: #2c2c2c;
+.avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
}
/* Bubbles */
@@ -320,30 +366,504 @@ body {
.own-message .bubble {
background: #5865F2;
- border-bottom-right-radius: 2px;
}
-#password-input {
+
+.auth-switch {
+ margin-top: 15px;
+ font-size: 0.85em;
+ color: #b9bbbe;
+}
+
+#link-switch {
+ color: #5865F2;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+#emoji-bar {
+ margin-bottom: 8px;
+ display: flex;
+ gap: 5px;
+}
+
+.emoji-btn {
+ background: #2c2c2c;
+ border: none;
+ color: white;
+ padding: 5px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+/* --- LOBBY SCREEN STYLES --- */
+#lobby-screen {
+ justify-content: center;
+ align-items: center;
+ background: radial-gradient(circle, #1a1a1a 0%, #121212 100%);
+}
+
+.lobby-box {
+ background: #1e1e1e;
+ padding: 30px;
+ border-radius: 12px;
+ width: 600px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.7);
+ border: 1px solid #333;
+}
+
+.lobby-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #333;
+}
+
+/* Contenedor de las Cards */
+#rooms-container {
+ flex: 1;
+ overflow-y: auto;
+ display: grid;
+ grid-template-columns: 1fr 1fr; /* Dos columnas de salas */
+ gap: 15px;
+ padding-right: 10px;
+}
+
+/* Estilo de las Cards de Sala */
+.room-card {
+ background: #2c2c2c;
+ padding: 15px;
+ border-radius: 8px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all 0.2s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 4px;
+ margin-bottom: 4px;
+}
+
+.room-card:hover {
+ background: #36393f;
+ border-color: #5865F2;
+ transform: translateY(-2px);
+}
+
+.room-card h4 {
+ color: #5865F2;
+ font-size: 1.1em;
+}
+
+.room-card p {
+ font-size: 0.85em;
+ color: #b9bbbe;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.room-card small {
+ font-size: 0.75em;
+ color: #72767d;
+}
+
+.lobby-footer {
+ margin-top: 20px;
+ text-align: center;
+}
+
+.link-btn {
+ background: none;
+ border: none;
+ color: #ed4245;
+ cursor: pointer;
+ text-decoration: underline;
+ font-size: 0.9em;
+}
+
+/* --- MODAL STYLES --- */
+.modal {
+ position: fixed;
+ top: 0; left: 0;
+ width: 100%; height: 100%;
+ background: rgba(0, 0, 0, 0.85);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
+}
+
+.modal-content {
+ background: #1e1e1e;
+ padding: 25px;
+ border-radius: 12px;
+ width: 400px;
+ border: 1px solid #444;
+ box-shadow: 0 0 30px rgba(0,0,0,1);
+}
+
+.modal-content h3 {
+ margin-bottom: 15px;
+ color: white;
+}
+
+.modal-content input,
+.modal-content textarea {
width: 100%;
padding: 10px;
- margin-top: 10px;
+ margin-bottom: 15px;
border-radius: 6px;
border: none;
background: #2c2c2c;
color: white;
+ font-family: inherit;
+ outline: none;
}
-.auth-switch {
- margin-top: 15px;
+.modal-content textarea {
+ height: 100px;
+ resize: none;
+}
+
+.modal-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+/* Colores de botones específicos */
+.tool-btn.success {
+ background: #248046;
+ color: white;
+}
+.tool-btn.success:hover {
+ background: #1a6334;
+}
+
+.tool-btn.secondary {
+ background: #4f545c;
+}
+
+/* Texto de carga */
+.loading-text {
+ grid-column: span 2;
+ text-align: center;
+ color: #72767d;
+ padding: 40px;
+}
+
+/* Scrollbar personalizada para el lobby */
+#rooms-container::-webkit-scrollbar {
+ width: 6px;
+}
+#rooms-container::-webkit-scrollbar-thumb {
+ background: #333;
+ border-radius: 10px;
+}
+
+/* --- PROJETOS & FORM EXTENSIONS --- */
+
+/* Etiquetas dentro de modales */
+.modal-label {
+ display: block;
+ margin-bottom: 8px;
font-size: 0.85em;
color: #b9bbbe;
+ text-align: left;
}
-#link-switch {
+/* Estilos para selectores (dropdowns) */
+#select-project-choice,
+#select-import-project {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 15px;
+ border-radius: 6px;
+ border: none;
+ background: #2c2c2c;
+ color: white;
+ font-family: inherit;
+ outline: none;
+ cursor: pointer;
+ border: 1px solid #333;
+}
+
+#select-project-choice:focus,
+#select-import-project:focus {
+ border-color: #5865F2;
+}
+
+/* Texto de información pequeña */
+.small-info {
+ font-size: 0.75em;
+ color: #72767d;
+ margin-bottom: 15px;
+ text-align: left;
+ font-style: italic;
+}
+
+/* Input para nombre de nuevo proyecto (condicional) */
+#new-project-name-input {
+ display: block; /* El JS controlará si se oculta o no */
+ border: 1px dashed #5865F2 !important;
+ background: #1a1a1a !important;
+}
+
+/* Agrupación de botones en el header del Lobby */
+.header-btns {
+ display: flex;
+ gap: 10px;
+}
+
+/* Botón de éxito (Verde) */
+.tool-btn.success {
+ background: #248046;
+ color: white;
+}
+
+.tool-btn.success:hover {
+ background: #1a6334;
+}
+
+/* Animación simple para aparición de modales */
+@keyframes modalFadeIn {
+ from { opacity: 0; transform: translateY(-20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.modal-content {
+ animation: modalFadeIn 0.3s ease-out;
+}
+
+/* --- PROJECTS MENU STYLES --- */
+#projects-menu-modal .modal-content {
+ max-width: 900px !important;
+ max-height: 80vh !important;
+ width: 900px;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+#projects-menu-modal .modal-content h3 {
+ padding: 20px 25px;
+ border-bottom: 1px solid #333;
+ margin-bottom: 0;
+ background: #16171b;
+}
+
+#projects-menu-modal .modal-content > div {
+ display: flex;
+ gap: 20px;
+ padding: 20px;
+ flex: 1;
+ overflow: hidden;
+}
+
+/* Projects List Panel */
+#projects-list {
+ border-right: 1px solid #555;
+ padding-right: 20px !important;
+}
+
+#projects-list h4 {
color: #5865F2;
- text-decoration: none;
- font-weight: bold;
+ margin-bottom: 15px;
+ font-size: 0.95em;
+ text-transform: uppercase;
+ letter-spacing: 1px;
}
-#link-switch:hover {
- text-decoration: underline;
-}
\ No newline at end of file
+#projects-list button {
+ width: 100%;
+ text-align: left;
+ padding: 12px !important;
+ border: 1px solid #555 !important;
+ background: #2c2c2c !important;
+ color: white !important;
+ margin-bottom: 10px;
+ transition: all 0.2s ease;
+ font-size: 13px;
+}
+
+#projects-list button:hover {
+ background: #36393f !important;
+ border-color: #5865F2 !important;
+}
+
+#projects-list button strong {
+ display: block;
+ color: #5865F2;
+ margin-bottom: 4px;
+}
+
+#projects-list button small {
+ color: #888;
+ font-size: 0.8em;
+}
+
+/* Versions Panel */
+#versions-section h4 {
+ color: #5865F2;
+ margin-bottom: 15px;
+ font-size: 0.95em;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+#versions-list {
+ margin-bottom: 20px !important;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #555;
+}
+
+#versions-list > div {
+ padding: 12px !important;
+ border: 1px solid #555 !important;
+ background: #2c2c2c !important;
+ margin-bottom: 10px;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ display: flex !important;
+ justify-content: space-between !important;
+ align-items: center !important;
+}
+
+#versions-list > div:hover {
+ background: #36393f !important;
+ border-color: #5865F2 !important;
+}
+
+#versions-list > div.active {
+ background: #36393f !important;
+ border-color: #5865F2 !important;
+}
+
+#versions-list > div > div:first-child strong {
+ color: #5865F2;
+ display: block;
+ margin-bottom: 4px;
+}
+
+#versions-list > div > div:first-child small {
+ color: #888;
+ font-size: 0.8em;
+}
+
+#versions-list button {
+ padding: 6px 12px !important;
+ font-size: 0.85em !important;
+}
+
+#versions-list .tool-btn.success {
+ background: #248046 !important;
+}
+
+#versions-list .tool-btn.success:hover {
+ background: #1a6334 !important;
+}
+
+#versions-list .tool-btn.secondary {
+ background: #4f545c !important;
+}
+
+#versions-list .tool-btn.secondary:hover {
+ background: #686d73 !important;
+}
+
+/* Preview Area */
+#version-preview {
+ width: 100% !important;
+ height: 150px !important;
+ padding: 10px !important;
+ border: 1px solid #555 !important;
+ border-radius: 4px !important;
+ background: #1e1e1e !important;
+ color: #e0e0e0 !important;
+ font-family: 'Consolas', monospace !important;
+ resize: none !important;
+ font-size: 13px !important;
+ line-height: 1.4 !important;
+}
+
+#version-preview::-webkit-scrollbar {
+ width: 8px;
+}
+
+#version-preview::-webkit-scrollbar-thumb {
+ background: #555;
+ border-radius: 4px;
+}
+
+#no-selection {
+ color: #888;
+ text-align: center;
+ padding-top: 50px;
+}
+
+/* Modal Buttons */
+#projects-menu-modal .modal-buttons {
+ padding: 20px;
+ border-top: 1px solid #333;
+ background: #16171b;
+ justify-content: flex-end;
+}
+
+#projects-menu-modal .modal-buttons button {
+ padding: 10px 20px !important;
+}
+
+/* Scrollbars for projects and versions list */
+#projects-list {
+ overflow-y: auto !important;
+ scrollbar-width: thin;
+ scrollbar-color: #555 #2c2c2c;
+}
+
+#projects-list::-webkit-scrollbar {
+ width: 6px;
+}
+
+#projects-list::-webkit-scrollbar-track {
+ background: #2c2c2c;
+}
+
+#projects-list::-webkit-scrollbar-thumb {
+ background: #555;
+ border-radius: 3px;
+}
+
+#versions-list {
+ scrollbar-width: thin;
+ scrollbar-color: #555 #2c2c2c;
+}
+
+.versions-section {
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: #555 #2c2c2c;
+}
+
+/* --- REMOTE CURSORS & SELECTION STYLES --- */
+
+.CodeMirror-cursor {
+ border-left: 2px solid #5865F2;
+}
+
+.remote-cursor-label {
+ position: relative;
+ display: inline-block;
+ margin-right: 2px;
+ pointer-events: none;
+ z-index: 100;
+}
+
+/* Selection marker styling handled inline with dynamic color */
\ No newline at end of file