Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions layouts/shortcodes/titlesTable.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{{/* add script for dynamic interaction */}}
<script src="/js/titleList.js" async="true"></script>
{{/* fetch JSON from file */}}
{{ $titles := getJSON "https://liberation.rpg-librarium.de/v1/titles?recursive=true" }}
{{/* generate HTML */}}
<div class="multiversumTable">
<div class="titlesTable">
<table class="titles">
<thead>
<th class="title">Title</th>
<th class="title">Titel</th>
<th class="system">System</th>
</thead>
<tbody>
Expand All @@ -22,4 +24,11 @@
{{ end }}
</tbody>
</table>
<div class="updatedAt">
Stand:
<time datetime="{{ .Site.LastChange.Format "2006-01-02" }}">
{{ .Site.LastChange.Format "02.01.2006" }}
</time>
<div class="updating"></div>
</div>
</div>
86 changes: 86 additions & 0 deletions static/css/librarium.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,25 +149,111 @@ 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);
}
.multiversumTable table thead th:nth-of-type(1) {
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;
Expand Down
250 changes: 250 additions & 0 deletions static/js/titleList.js
Original file line number Diff line number Diff line change
@@ -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})`;
});
});