Last active
February 2, 2026 19:16
-
-
Save sorah/1004aba425e0f7c6743629ff06393eaa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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