diff --git a/layouts/shortcodes/titlesTable.html b/layouts/shortcodes/titlesTable.html index ebc1532..981c368 100644 --- a/layouts/shortcodes/titlesTable.html +++ b/layouts/shortcodes/titlesTable.html @@ -1,10 +1,12 @@ +{{/* add script for dynamic interaction */}} + {{/* fetch JSON from file */}} {{ $titles := getJSON "https://liberation.rpg-librarium.de/v1/titles?recursive=true" }} {{/* generate HTML */}} -
+
- + @@ -22,4 +24,11 @@ {{ end }}
TitleTitel System
+
+ Stand: + +
+
diff --git a/static/css/librarium.css b/static/css/librarium.css index 376fa65..8b6d4fc 100644 --- a/static/css/librarium.css +++ b/static/css/librarium.css @@ -149,18 +149,103 @@ header.page-header { } /* content elements */ + +.titlesTable .styleSelect { + display: flex; + justify-content: flex-start; + align-items: baseline; + margin-bottom: .5em; +} +.titlesTable .styleSelect > * { + margin: .2em 1em .2em 0; +} +.titlesTable .styleSelect input[type=radio] { + display: none; +} +.titlesTable .styleSelect label[for] { + display: inline-block; + cursor: pointer; + user-select: none; + padding: .2em .5em; + border: .1em solid var(--color-cd-dark-ish); + color: var(--color-cd-light-green); +} +.titlesTable .styleSelect label[for]:hover, +.titlesTable .styleSelect label[for]:focus { + border-color: var(--color-cd-dark-green); + color: var(--color-cd-dark-green); +} +.titlesTable .styleSelect input[type=radio]:checked + label[for] { + background: var(--color-cd-dark-green); + border-color: var(--color-cd-dark-green); + color: var(--color-cd-light-ish); + cursor: default; +} + +.titles.full-list .title { + text-align: left; +} +.titles.full-list .system { + text-align: right; +} +.titles.full-list tbody .system > th { + padding-top: 1.42em; + text-align: center; +} + +.titlesTable .updatedAt { + margin-top: .5em; + font-size: 0.9em; +} +.titlesTable .updatedAt .updating { + display: inline-block; + color: var(--color-cd-brown-ish); + font-size: .7em; + margin-left: 1em; +} +.titlesTable .updatedAt .updating.error { + color: #ff0077; +} +.loadingTitles .titlesTable .updatedAt .updating::before { + content: 'aktualisiere ...'; +} +.loadingTitles .titlesTable .updatedAt .updating::after { + content: ''; + display: inline-block; + box-sizing: border-box; + margin-left: .5em; + width: 1.2em; + height: 1.2em; + border-radius: .6em; + border: .15em solid; + border-color: var(--color-cd-light-green) transparent transparent transparent; + animation: 2s infinite linear spinningLoadingCircle; +} +@keyframes spinningLoadingCircle { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.titlesTable, .multiversumTable { font-size: .85em; } + +table.titles, .multiversumTable table { border: 0 none; border-collapse: collapse; width: 100%; } + + +table.titles th, +table.titles td, .multiversumTable table th, .multiversumTable table td { padding: .21em .5em; } +table.titles thead th, .multiversumTable table thead th { border-bottom: .2em solid var(--color-cd-dark-ish); } @@ -168,6 +253,7 @@ header.page-header { min-width: 10em; text-align: left; } +table.titles tbody td, .multiversumTable table tbody td { border: solid var(--color-cd-dark-ish); border-width: .1em 0; diff --git a/static/js/titleList.js b/static/js/titleList.js new file mode 100644 index 0000000..62310a0 --- /dev/null +++ b/static/js/titleList.js @@ -0,0 +1,250 @@ +'use strict'; + +const LIBERATION_BASE = 'https://liberation.rpg-librarium.de/v1'; +const _DEBUG_PRINT = text => thing => (console.debug(text, thing), thing); +const _FORMAT_DATE = date => date.toLocaleString('de-DE', { + hourCycle: 'h23', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +const _ROOT = document.documentElement; +_ROOT.classList.add('loadingTitles'); + +// load as early as posible +let titlesPromise = fetch(`${LIBERATION_BASE}/titles?recursive=true`) + .then(response => response.json()) + .then(_DEBUG_PRINT('titles json')) + .then(data => processTitleData(data)) + .then(_DEBUG_PRINT('titles processed')) + +function processTitleData(titlesData) { + let systemsMap = {}; + let titlesMap = {}; + let titles = titlesData.sort((a,b) => a.name.localeCompare(b.name)); + titles.forEach(t => { + titlesMap[t.id] = t; + let sysId = t.rpg_system.id; + if (!systemsMap[sysId]) { + systemsMap[sysId] = {...t.rpg_system}; + } + let sys = systemsMap[sysId]; + if (!sys.titles) sys.titles = []; + sys.titles.push(t.id); + }); + let systems = Object.values(systemsMap) + .sort((a,b) => a.name.localeCompare(b.name)); + + return { + now: new Date(), + systems, + systemsMap, + titles, + titlesMap, + }; +} + +function doTheMagic(data) { + const _LIST_STYLES = [ + { + id: 'static', + renderFn: renderStaticLikeList, + name: 'Einfach Alles', + description: 'eine einfache Liste mit allen Titeln', + },{ + id: 'grouped', + renderFn: renderFullList, + name: 'Gruppiert', + description: 'eine etwas weniger einfache Liste, gruppiert nach Systemen', + }, + ]; + const _C = (tag, opts={}) => { + let el = document.createElement(tag); + if (opts.classes) opts.classes.forEach(c => el.classList.add(c)); + if (opts.id) el.id = opts.id; + if (opts.children) opts.children.forEach(c => el.appendChild(c)); + if (opts.text) el.innerText = opts.text; + if (opts.attrs) Object.keys(opts.attrs).forEach(k=>el.setAttribute(k, opts.attrs[k])); + return el; + }; + const titlesRoot = document.querySelector('.titlesTable'); + let activeStyle = _LIST_STYLES[0].id; // default style + let activeStyleNode = null; + const styleSelector = renderStyleSelector(); + let container = _C('div', {id:'titleListContainer'}); + changeStyle(activeStyle); + titlesRoot.querySelector('table.titles').replaceWith(container); + titlesRoot.prepend(styleSelector); + + function changeStyle(newStyleId) { + // retrieve style info + let filtered = _LIST_STYLES.filter(s => s.id === newStyleId); + if (filtered.length !== 1) { + console.debug('could not find list style ', newStyleId); + changeStyle(activeStyle); + return false; + } + let newStyle = filtered[0]; + let previousStyle = activeStyle; + activeStyle = newStyle; + + // update selector + styleSelector + .querySelector(`input[type=radio][value=${newStyle.id}]`) + .checked = true; + + // apply style, if not already active + let prevNode = container.querySelector('.titles'); + if (previousStyle.id === newStyle.id && prevNode) { + return true; + } + let newNode = newStyle.renderFn(); + if (prevNode) { + prevNode.replaceWith(newNode); + } else { + container.appendChild(newNode); + } + } + + function renderStyleSelector() { + let element = _C('div', { + classes:['styleSelect'], + children: [ + _C('span', {text: 'Ansicht:'}), + ... _LIST_STYLES.flatMap(style => { + let inputId = `_listStyleSelect_${style.id}`; + let input = _C('input', { + id: inputId, + attrs: { + type: 'radio', + name: '_listStyleSelect', + value: style.id, + }, + }); + let label = _C('label', { + attrs: { + for: inputId, + title: style.description, + tabindex: 0, + role: 'button', + }, + text: style.name, + }); + let labelListener = evt => { + let target = evt.target; + if (evt.type === 'click' + || (evt.type === 'keydown' + && [' ', 'Enter'].includes(evt.key))) { + evt.preventDefault(); + input.checked = true; + changeStyle(style.id); + } + }; + label.addEventListener('click', labelListener); + label.addEventListener('keydown', labelListener); + input.addEventListener('change', evt => { + if (input.checked) { + changeStyle(style.id); + } + }); + return [input, label]; + }) + ], + }); + return element; + } + + function renderFullList() { + let table = _C('table', { + classes:['titles', 'full-list'], + children: [ + _C('thead', {children:[ + _C('tr', {children:[ + _C('th', {classes:['title'], text:'Titel'}), + _C('th', {classes:['system'], text:'System'}), + ]}) + ]}), + ] + }); + let tbody = _C('tbody'); + table.appendChild(tbody); + data.systems.forEach(sys => { + let displayName = sys.shortname ? + `[${sys.shortname}] ${sys.name}` : + sys.name; + tbody.appendChild(_C('tr', { + classes: ['system'], + children:[_C('th', { + text: displayName, + attrs: {colspan: 2}, + })] + })); + sys.titles.forEach(tId => { + let title = data.titlesMap[tId]; + tbody.appendChild(_C('tr', { + classes: ['title'], + children:[_C('td', { + text: title.name, + attrs: {colspan: 2}, + })] + })); + }) + }) + + return table; + } + + function renderStaticLikeList() { + let table = _C('table', { + classes:['titles', 'static-like-list'], + children: [ + _C('thead', {children:[ + _C('tr', {children:[ + _C('th', {classes:['title'], text:'Titel'}), + _C('th', {classes:['system'], text:'System'}), + ]}) + ]}), + ] + }); + let tbody = _C('tbody'); + table.appendChild(tbody); + data.titles + .sort((a,b) => a.name.localeCompare(b.name)) + .forEach(title => { + let sys = title.rpg_system; + tbody.appendChild(_C('tr', {children:[ + _C('td', { + classes: ['title'], + text: title.name, + }), + _C('td', { + classes: ['system'], + text: sys.name, + }), + ]})); + }); + + return table; + } +} + +document.addEventListener('DOMContentLoaded', evt => { + titlesPromise + .then(data => { + doTheMagic(data); + let updateTimeNode = document.querySelector('.titles+.updatedAt time, #titleListContainer+.updatedAt time'); + updateTimeNode.datetime = data.now.toISOString(); + updateTimeNode.innerText = _FORMAT_DATE(data.now); + _ROOT.classList.remove('loadingTitles'); + }) + .catch(e => { + _ROOT.classList.remove('loadingTitles'); + let statusNode = document.querySelector('.titles+.updatedAt .updating'); + statusNode.classList.add('error'); + statusNode.innerText = `Aktualisierung fehlgeschlagen. (${e})`; + }); +});