diff --git a/frontend/js/leaderboard/compare.js b/frontend/js/leaderboard/compare.js
index c5f51b48..902689e9 100644
--- a/frontend/js/leaderboard/compare.js
+++ b/frontend/js/leaderboard/compare.js
@@ -231,19 +231,33 @@ function updateFloatingCompareBar() {
bar.style.display = "flex";
- const namesStr = window.selectedUsers.map((u) => u.name).join(", ");
const count = window.selectedUsers.length;
+ // Static markup (no user-controlled data) is safe via innerHTML.
bar.innerHTML = `
-
- Comparing (${count}/3): ${namesStr}
-
+
`;
+
+ // Names are user-controlled, so build this part with safe DOM nodes
+ // (textContent) instead of interpolating into innerHTML.
+ const listDiv = bar.querySelector(".selected-list");
+ listDiv.appendChild(document.createTextNode("Comparing ("));
+ const tagSpan = document.createElement("span");
+ tagSpan.className = "selected-tag";
+ tagSpan.textContent = `${count}/3`;
+ listDiv.appendChild(tagSpan);
+ listDiv.appendChild(document.createTextNode("): "));
+ window.selectedUsers.forEach((u, idx) => {
+ listDiv.appendChild(document.createTextNode(u.name));
+ if (idx < window.selectedUsers.length - 1) {
+ listDiv.appendChild(document.createTextNode(", "));
+ }
+ });
}
/**
@@ -498,22 +512,34 @@ function populateComparisonTable() {
});
}
- let html = "";
+ table.innerHTML = "";
+
+ const thead = document.createElement("thead");
+ const headerRow = document.createElement("tr");
headers.forEach((h) => {
- html += `| ${h} | `;
+ const th = document.createElement("th");
+ // Header may include user-controlled names (textContent only).
+ th.textContent = h;
+ headerRow.appendChild(th);
});
- html += "
";
+ thead.appendChild(headerRow);
+ const tbody = document.createElement("tbody");
metrics.forEach((m) => {
- html += `| ${m.label} | `;
+ const row = document.createElement("tr");
+ const labelCell = document.createElement("td");
+ labelCell.textContent = m.label;
+ row.appendChild(labelCell);
m.values.forEach((v) => {
- html += `${v} | `;
+ const cell = document.createElement("td");
+ cell.textContent = v;
+ row.appendChild(cell);
});
- html += "
";
+ tbody.appendChild(row);
});
- html += "";
- table.innerHTML = html;
+ table.appendChild(thead);
+ table.appendChild(tbody);
}
/**
diff --git a/frontend/js/leaderboard/sync-changes.js b/frontend/js/leaderboard/sync-changes.js
index 50549b7f..3766f8c6 100644
--- a/frontend/js/leaderboard/sync-changes.js
+++ b/frontend/js/leaderboard/sync-changes.js
@@ -47,24 +47,63 @@ document.addEventListener("DOMContentLoaded", () => {
btn.classList.add("pulse");
- let html = "";
+ const fragment = document.createDocumentFragment();
+
if (changes.rank_changes) {
changes.rank_changes.forEach((rc) => {
const arrow = rc.rank_delta > 0 ? "↑" : "↓";
const color = rc.rank_delta > 0 ? "var(--green)" : "var(--red)";
- html += `${arrow} ${rc.username} moved #${rc.old_rank} → #${rc.new_rank}
`;
+
+ const line = document.createElement("div");
+ line.className = "changes-content-line";
+
+ const arrowSpan = document.createElement("span");
+ arrowSpan.style.color = color;
+ arrowSpan.textContent = arrow;
+ line.appendChild(arrowSpan);
+ line.appendChild(document.createTextNode(" "));
+
+ const detailSpan = document.createElement("span");
+ const nameStrong = document.createElement("strong");
+ nameStrong.style.color = "#fff";
+ // User-controlled display name (textContent only).
+ nameStrong.textContent = rc.username;
+ detailSpan.appendChild(nameStrong);
+ detailSpan.appendChild(
+ document.createTextNode(` moved #${rc.old_rank} → `),
+ );
+ const newRankStrong = document.createElement("strong");
+ newRankStrong.style.color = "var(--green)";
+ newRankStrong.textContent = `#${rc.new_rank}`;
+ detailSpan.appendChild(newRankStrong);
+
+ line.appendChild(detailSpan);
+ fragment.appendChild(line);
});
}
if (changes.new_users && changes.new_users.length > 0) {
- html += `${changes.new_users.length} new user(s) joined
`;
+ const line = document.createElement("div");
+ line.className = "changes-content-line";
+ const span = document.createElement("span");
+ span.style.color = "#fff";
+ span.textContent = `${changes.new_users.length} new user(s) joined`;
+ line.appendChild(span);
+ fragment.appendChild(line);
}
if (changes.total_new_solves && changes.total_new_solves > 0) {
- html += `${changes.total_new_solves} new problems solved across ${changes.users_with_new_solves} users
`;
+ const line = document.createElement("div");
+ line.className = "changes-content-line";
+ const span = document.createElement("span");
+ span.style.color = "#fff";
+ span.textContent = `${changes.total_new_solves} new problems solved across ${changes.users_with_new_solves} users`;
+ line.appendChild(span);
+ fragment.appendChild(line);
}
- body.innerHTML = html;
+ body.innerHTML = "";
+ body.appendChild(fragment);
}
loadChanges();
diff --git a/frontend/registration.html b/frontend/registration.html
index 8bee62fa..7d4d77f3 100644
--- a/frontend/registration.html
+++ b/frontend/registration.html
@@ -60,6 +60,7 @@ Join the Leaderboard
name="name"
class="form-input"
placeholder="Enter your full name"
+ maxlength="100"
required
/>