Skip to content

Instantly share code, notes, and snippets.

@yangirov
Last active November 14, 2025 07:03
Show Gist options
  • Select an option

  • Save yangirov/683598fc2228ec7dd4cede497d6c4854 to your computer and use it in GitHub Desktop.

Select an option

Save yangirov/683598fc2228ec7dd4cede497d6c4854 to your computer and use it in GitHub Desktop.
Раскладка UfaDevConf в виде таблицы
// ==UserScript==
// @name Ufacoder Schedule Table With Favorites + Section Toggles
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Строит таблицу программы по трекам.
// @author You
// @match https://dc.ufacoder.com/*
// @grant none
// @run-at document-idle
// @noframes
// ==/UserScript==
(() => {
if (document.querySelector('.tt-wrap')) return;
// --- CSS ---
const css = `
.tt-wrap{margin:0 auto 40px;padding:0 10px;overflow:auto}
.tt-filters{margin:10px 0 8px;display:flex;flex-wrap:wrap;gap:12px;align-items:center;font:14px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,'Noto Sans',sans-serif}
.tt-filters .tt-badge{opacity:.7}
.tt-filters label{display:inline-flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
.tt-table{width:100%;border-collapse:collapse;font:14px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,'Noto Sans',sans-serif}
.tt-table th,.tt-table td{border:1px solid #e6e6e6;padding:10px 12px;vertical-align:top;background:#fff}
.tt-table thead th{position:sticky;top:0;background:#fff;z-index:3;font-weight:600}
.tt-table tr th:first-child{position:sticky;left:0;background:#fff;z-index:2;white-space:nowrap;width:120px}
.tt-table td{min-width:180px;position:relative}
.tt-table tr:hover td{background:#fafbfc}
.tt-talk{font-weight:600;margin:0 0 4px}
.tt-speaker{font-size:12px;opacity:.9;margin:0}
.tt-desc{font-size:12px;opacity:.75;margin:2px 0 0}
.tt-break{background:#f7f8fa;color:#555}
.tt-empty{background:repeating-linear-gradient(45deg,#fff,#fff 8px,#fafafa 8px,#fafafa 16px)}
.tt-fav{background:#ffeaf3 !important; box-shadow:inset 0 0 0 2px #ff8fc2}
.tt-fav::after{content:'❤️'; position:absolute; top:6px; right:8px; font-size:14px; opacity:.9}
.tt-flash{animation:ttflash 1.2s ease-out}
@keyframes ttflash{0%{box-shadow:0 0 0 0 rgba(255,143,194,.9)}100%{box-shadow:0 0 0 16px rgba(255,143,194,0)}}
@media (max-width:960px){.tt-table td{min-width:140px}}
`;
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
// --- Колонки (обнови id при необходимости) ---
const tracks = [
{ id: '#rec1254949481', name: 'Backend' },
{ id: '#rec1254949511', name: 'Frontend' },
{ id: '#rec1367845201', name: 'AI/ML' },
{ id: '#rec1254949521', name: 'DevOps' },
{ id: '#rec1367845221', name: 'Management' },
{ id: '#rec1538298301', name: 'Management Workshop' },
{ id: '#rec1372132381', name: 'АРИТ' }
];
const waitForMany = (sels, cb, tries = 80) => {
const tick = () => {
if (sels.every(s => document.querySelector(s))) return cb();
if (tries-- <= 0) return;
setTimeout(tick, 250);
};
tick();
};
waitForMany(['#rec1254949451', '.t516__row'], build);
function build() {
const isBreak = (s) => /(перерыв|кофе|обед|регистрац)/i.test(s || '');
const normTime = (t) => String(t || '')
.replace(/\s+/g, ' ')
.replace(/[—–-]/g, '–')
.replace(/\s*–\s*/, ' – ')
.trim();
const startMinutes = (t) => {
const m = normTime(t).match(/(\d{1,2}):(\d{2})/);
return m ? (+m[1] * 60 + +m[2]) : 0;
};
// Собираем данные по всем трекам
const timesMap = new Map();
tracks.forEach(tr => {
const root = document.querySelector(tr.id);
if (!root) return;
root.querySelectorAll('.t516__row').forEach(row => {
const timeEl = row.querySelector('.t516__leftcol .t516__time');
if (!timeEl) return;
const time = normTime(timeEl.textContent);
const titleEl = row.querySelector('.t516__sectiontextwrapper .t516__title');
const talkEl = row.querySelector('.t516__sectiontextwrapper .t516__text');
const descEl = row.querySelector('.t516__sectiontextwrapper .t516__persdescr');
const talk = (talkEl ? talkEl.textContent : (titleEl ? titleEl.textContent : '')).trim();
const speaker = (talkEl && titleEl ? titleEl.textContent.trim() : '');
const desc = (descEl ? descEl.innerHTML?.split("<br>").join(", ").trim() : '');
if (!timesMap.has(time)) timesMap.set(time, {});
timesMap.get(time)[tr.name] = { talk, speaker, desc };
});
});
const times = Array.from(timesMap.keys()).sort((a, b) => startMinutes(a) - startMinutes(b));
// --- Контейнер + фильтры + таблица ---
const wrap = document.createElement('div');
wrap.className = 'tt-wrap';
const filters = document.createElement('div');
filters.className = 'tt-filters';
filters.innerHTML = `<span class="tt-badge">Секции:</span>`;
const table = document.createElement('table');
table.className = 'tt-table';
const thead = document.createElement('thead');
const hr = document.createElement('tr');
['Время', ...tracks.map(t => t.name)].forEach(h => {
const th = document.createElement('th');
th.textContent = h;
hr.appendChild(th);
});
thead.appendChild(hr);
const tbody = document.createElement('tbody');
times.forEach(time => {
const tr = document.createElement('tr');
const th = document.createElement('th');
th.textContent = time;
tr.appendChild(th);
tracks.forEach(col => {
const td = document.createElement('td');
const data = (timesMap.get(time) || {})[col.name];
if (data) {
if (isBreak(data.talk)) td.classList.add('tt-break');
const t = document.createElement('div');
t.className = 'tt-talk';
t.textContent = data.talk || '—';
td.appendChild(t);
if (data.speaker) {
const s = document.createElement('div');
s.className = 'tt-speaker';
s.textContent = data.speaker;
td.appendChild(s);
}
if (data.desc && data.speaker) {
const d = document.createElement('div');
d.className = 'tt-desc';
d.textContent = data.desc;
td.appendChild(d);
}
const key = makeKey(time, col.name);
td.dataset.key = key;
td.dataset.time = time;
td.dataset.track = col.name;
td.id = 'tt_' + key.replace(/\W+/g,'_');
} else {
td.classList.add('tt-empty');
}
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(thead);
table.appendChild(tbody);
// Вставка после блока «Программа»
const programBlock = document.querySelector('#rec1254949451');
wrap.appendChild(filters);
wrap.appendChild(table);
if (programBlock) programBlock.insertAdjacentElement('afterend', wrap);
else document.body.prepend(wrap);
// --- Избранное (подсветка + ❤️, без отдельного блока) ---
const LSK = 'ttFavoritesV1';
const loadFavs = () => { try { return JSON.parse(localStorage.getItem(LSK) || '{}'); } catch { return {}; } };
const saveFavs = (obj) => localStorage.setItem(LSK, JSON.stringify(obj));
let favs = loadFavs();
applyFavClasses();
table.addEventListener('click', (e) => {
const td = e.target.closest('td');
if (!td || !td.dataset.key) return;
toggleFav(td);
});
function makeKey(time, track){ return `${time}__${track}`; }
function toggleFav(td){
const key = td.dataset.key;
if (!key) return;
if (favs[key]) {
delete favs[key];
saveFavs(favs);
td.classList.remove('tt-fav');
} else {
const item = {
key,
time: td.dataset.time || '',
track: td.dataset.track || '',
talk: (td.querySelector('.tt-talk')?.textContent || '').trim(),
speaker: (td.querySelector('.tt-speaker')?.textContent || '').trim()
};
favs[key] = item;
saveFavs(favs);
td.classList.add('tt-fav','tt-flash');
setTimeout(()=>td.classList.remove('tt-flash'), 1200);
}
}
function applyFavClasses(){
document.querySelectorAll('.tt-table td[data-key]').forEach(td => {
const key = td.dataset.key;
if (key && favs[key]) td.classList.add('tt-fav');
else td.classList.remove('tt-fav');
});
}
// --- Тогглы секций (колонок) с сохранением состояния ---
const LSF = 'ttHiddenTracksV1';
const loadHidden = () => {
try { return new Set(JSON.parse(localStorage.getItem(LSF) || '[]')); } catch { return new Set(); }
};
const saveHidden = (set) => localStorage.setItem(LSF, JSON.stringify([...set]));
const hidden = loadHidden();
// построить чекбоксы
tracks.forEach((t, idx) => {
const id = `tt_ch_${idx}`;
const label = document.createElement('label');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.id = id;
const initiallyVisible = !hidden.has(t.name);
cb.checked = initiallyVisible;
cb.addEventListener('change', () => {
setColumnVisible(idx, cb.checked);
if (cb.checked) hidden.delete(t.name);
else hidden.add(t.name);
saveHidden(hidden);
});
const span = document.createElement('span');
span.textContent = t.name;
label.appendChild(cb);
label.appendChild(span);
filters.appendChild(label);
});
// применить сохранённое состояние
tracks.forEach((t, idx) => {
const visible = !hidden.has(t.name);
setColumnVisible(idx, visible);
const cb = document.getElementById(`tt_ch_${idx}`);
if (cb) cb.checked = visible;
});
function setColumnVisible(trackIndex, visible){
const dsp = visible ? '' : 'none';
const headRow = thead.querySelector('tr');
// +1 потому что 0-й столбец — «Время»
const hCell = headRow?.children[trackIndex + 1];
if (hCell) hCell.style.display = dsp;
tbody.querySelectorAll('tr').forEach(tr => {
const cell = tr.children[trackIndex + 1];
if (cell) cell.style.display = dsp;
});
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment