Skip to content

Instantly share code, notes, and snippets.

@sorah
Last active February 2, 2026 19:16
Show Gist options
  • Select an option

  • Save sorah/1004aba425e0f7c6743629ff06393eaa to your computer and use it in GitHub Desktop.

Select an option

Save sorah/1004aba425e0f7c6743629ff06393eaa to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name cfp.rk.o Accepted speaker
// @namespace http://tampermonkey.net/
// @version 2026-02-02
// @description mark accepted speakers
// @author sorah.jp
// @match https://cfp.rubykaigi.org/events/*/proposals
// @icon https://www.google.com/s2/favicons?sz=64&domain=rubykaigi.org
// @grant none
// ==/UserScript==
(function () {
"use strict";
const TABLE_SELECTOR = "table.proposal-list";
const MULTI_MARK_CHAR = "🏅";
const WARNING_MARK_CHAR = "⚠️";
const MULTI_MARK = " " + MULTI_MARK_CHAR;
const WARNING_MARK = " " + WARNING_MARK_CHAR;
const MARK_REGEX = new RegExp(
`[${MULTI_MARK_CHAR}${WARNING_MARK_CHAR}]`,
"g",
);
const table = document.querySelector(TABLE_SELECTOR);
if (!table) {
console.error("Table not found");
return;
}
// Dynamic Column Discovery
const headers = Array.from(
table.querySelectorAll("thead tr:nth-child(2) th"),
).map((th) => th.textContent.trim());
const SPEAKER_COL_IDX = headers.findIndex(
(h) => h.toLowerCase() === "speakers",
);
const STATUS_COL_IDX = headers.findIndex((h) => h.toLowerCase() === "status");
if (SPEAKER_COL_IDX === -1 || STATUS_COL_IDX === -1) {
console.error(
"Could not find 'Speakers' or 'Status' columns dynamically.",
{ SPEAKER_COL_IDX, STATUS_COL_IDX },
);
return;
}
const rows = Array.from(table.querySelectorAll("tbody > tr"));
const speakerMap = new Map();
// 1. Group rows by speaker and normalize status
rows.forEach((row) => {
const cells = row.cells;
const speaker = cells[SPEAKER_COL_IDX].textContent.trim();
const statusCell = cells[STATUS_COL_IDX];
const rawStatusText = statusCell.textContent
.replace(MARK_REGEX, "")
.trim()
.toLowerCase();
if (!speakerMap.has(speaker)) {
speakerMap.set(speaker, []);
}
speakerMap.get(speaker).push({ statusCell, rawStatusText });
});
// 2. Apply rules per speaker and update DOM
speakerMap.forEach((entries, speaker) => {
if (!speaker) return;
const isAccepted = (e) =>
e.rawStatusText.includes("accepted") &&
!e.rawStatusText.includes("not accepted");
const acceptedCount = entries.filter(isAccepted).length;
const hasMultipleRows = entries.length > 1;
entries.forEach((e) => {
const labelEl = e.statusCell.querySelector(".label") || e.statusCell;
const baseText = labelEl.textContent.replace(MARK_REGEX, "").trim();
const entryAccepted = isAccepted(e);
let mark = "";
if (hasMultipleRows) {
if (acceptedCount >= 2) {
mark = WARNING_MARK;
} else if (acceptedCount === 1 && !entryAccepted) {
mark = MULTI_MARK;
}
}
labelEl.textContent = mark ? baseText + mark : baseText;
});
});
console.log(
`Speakers (${SPEAKER_COL_IDX}), Status (${STATUS_COL_IDX}). Processed ${speakerMap.size} speakers.`,
);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment