sortable tables, apprentice/location filters

This commit is contained in:
tmont 2021-02-20 19:28:33 -08:00
parent ff9452ec3d
commit 0ac6178228
11 changed files with 365 additions and 150 deletions

View File

@ -25,7 +25,7 @@ app.get([ '/', '/enemies' ], (req, res) => {
app.get('/spells', (req, res) => {
res.render('spells', {
context: 'spells',
spells: spells.spells,
spells: spells.spells.sort((a, b) => a.name.localeCompare(b.name)),
});
});
@ -39,14 +39,24 @@ app.get('/exp', (req, res) => {
app.get('/weapons', (req, res) => {
res.render('weapons', {
context: 'weapons',
weapons: items.weapons,
weapons: items.weapons.sort((a, b) => {
if (a.attack === b.attack) {
return a.name.localeCompare(b.name);
}
return a.attack === b.attack ? 0 : (a.attack < b.attack ? -1 : 1 );
})
});
});
app.get('/armor', (req, res) => {
res.render('armor', {
context: 'armor',
armor: items.armor,
armor: items.armor.sort((a, b) => {
if (a.defense === b.defense) {
return a.name.localeCompare(b.name);
}
return a.defense === b.defense ? 0 : (a.defense < b.defense ? -1 : 1)
},),
});
});

View File

@ -42,3 +42,19 @@
content: ",";
margin-right: 4px;
}
table.sticky-header {
position: relative;
}
table.sticky-header tr.header th {
position: sticky;
top: 37px;
background-color: #fcfac8;
z-index: 2;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
}
th.sorted, td.sorted {
background-color: #dae5f6 !important;
}

164
web/static/saga.js Normal file
View File

@ -0,0 +1,164 @@
(function(window) {
const $apprenticeFilterForm = $('.apprentice-filter-form');
const $locationFilterForm = $('.location-filter-form');
const $table = $('#main-table');
window.saga = {
sortData: () => {
const qs = new URLSearchParams(window.location.search);
const col = qs.get('col');
let dir = qs.get('dir');
if (!col) {
return;
}
if (!$table.length) {
return;
}
if (dir !== 'asc' && dir !== 'desc') {
dir = 'asc';
}
const rows = $table.find('tbody.data tr').toArray();
const firstRow = rows[0];
if (!firstRow) {
return;
}
const coefficient = dir === 'desc' ? -1 : 1;
rows.sort((a, b) => {
let aVal = a.getAttribute('data-' + col);
let bVal = b.getAttribute('data-' + col);
const isNumber = !isNaN(Number(aVal));
if (isNumber && aVal) {
aVal = Number(aVal);
bVal = Number(bVal);
}
if (aVal === bVal) {
if (col === 'name') {
return 0;
}
const aName = a.getAttribute('data-name');
const bName = b.getAttribute('data-name');
if (typeof(aName) === 'string' && typeof(bName) === 'string') {
return aName.localeCompare(bName) * coefficient;
}
return 0;
}
if (typeof(aVal) === 'number' && typeof(bVal) === 'number') {
return (aVal < bVal ? -1 : 1) * coefficient;
}
if (typeof(aVal) === 'string' && typeof(bVal) === 'string') {
return aVal.localeCompare(bVal) * coefficient;
}
return 0;
});
let nextChild = null;
const headers = $table.find('tr.header th').removeClass('sorted').toArray();
const highlightedIndex = headers.findIndex(cell => cell.getAttribute('data-col') === col);
if (highlightedIndex === -1) {
throw new Error('could not find header column');
}
headers[highlightedIndex].classList.add('sorted');
for (let i = rows.length - 1; i >= 0; i--) {
const row = rows[i];
if (row === nextChild) {
continue;
}
$(row).find('td.sorted').removeClass('sorted');
$(row).find('td').eq(highlightedIndex).addClass('sorted');
row.parentNode.insertBefore(row, nextChild);
nextChild = row;
}
},
filterApprentices: () => {
const apprentices = [];
$apprenticeFilterForm.find('input[type="checkbox"]').toArray().map((input) => {
if (input.checked) {
apprentices.push(input.name);
}
});
$table
.find('tbody.data tr')
.hide()
.filter((i, row) => {
if (!apprentices.length) {
return true;
}
const users = ($(row).attr('data-users') || '').split(',');
if (!users.length) {
return true;
}
return apprentices.some((name) => users.includes(name));
})
.show();
},
filterLocations: () => {
const checked = [];
console.log('filter locations');
$locationFilterForm.find('input[type="checkbox"]').toArray().map((input) => {
if (input.checked) {
console.log('input checked');
checked.push(input.name);
}
});
$table
.find('tbody.data tr')
.hide()
.filter((i, row) => {
if (!checked.length) {
return true;
}
const data = $(row).attr('data-locations').split(',');
if (!data.length) {
return true;
}
return data.some((name) => checked.includes(name));
})
.show();
},
};
window.saga.sortData();
window.saga.filterApprentices();
window.saga.filterLocations();
window.addEventListener('popstate', () => {
window.saga.sortData();
});
$('.sortable-links a').click(function(e) {
e.preventDefault();
window.history.replaceState(null, '', $(this).attr('href'));
window.saga.sortData();
});
$apprenticeFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterApprentices);
$locationFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterLocations);
}(window));

View File

@ -3,20 +3,19 @@ extends master.pug
block tab-content
+apprenticeFilterForm()
div.table-responsive
table.table.table-sm.table-borderless.table-hover.table-striped
thead: tr
+sortHeader('Name')
+sortHeader('Defense')
+sortHeader('Cost')
+sortHeader('Fire')
+sortHeader('Ice')
+sortHeader('Thunder')
+sortHeader('Vacuum')
+sortHeader('Debuff')
table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header
thead: tr.header
+sortHeader('Name', 'name')
+sortHeader('Defense', 'defense')
+sortHeader('Cost', 'cost')
+sortHeader('Fire', 'res-fire')
+sortHeader('Ice', 'res-ice')
+sortHeader('Thunder', 'res-thunder')
+sortHeader('Vacuum', 'res-vacuum')
+sortHeader('Debuff', 'res-debuff')
th.align-middle Users
th.align-middle Locations
tbody: each item in accessories
tbody.data: each item in accessories
tr(
data-name=item.name
data-defense=item.defense
@ -26,6 +25,7 @@ block tab-content
data-res-thunder=item.resistance.thunder
data-res-vacuum=item.resistance.vacuum
data-res-debuff=item.resistance.debuff
data-users=item.users.join(',')
)
td: strong= item.name
td.text-right: code= item.defense

View File

@ -1,20 +1,21 @@
extends master.pug
block tab-content
div.table-responsive
table.table.table-sm.table-borderless.table-hover.table-striped
thead: tr
+sortHeader('Name')
+sortHeader('Defense')
+sortHeader('Cost')
+sortHeader('Fire')
+sortHeader('Ice')
+sortHeader('Thunder')
+sortHeader('Vacuum')
+sortHeader('Debuff')
+apprenticeFilterForm()
table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header
thead: tr.header
+sortHeader('Name', 'name')
+sortHeader('Defense', 'defense')
+sortHeader('Cost', 'cost')
+sortHeader('Fire', 'res-fire')
+sortHeader('Ice', 'res-ice')
+sortHeader('Thunder', 'res-thunder')
+sortHeader('Vacuum', 'res-vacuum')
+sortHeader('Debuff', 'res-debuff')
th.align-middle Users
th.align-middle Locations
tbody: each item in armor
tbody.data: each item in armor
tr(
data-name=item.name
data-defense=item.defense
@ -24,6 +25,7 @@ block tab-content
data-res-thunder=item.resistance.thunder
data-res-vacuum=item.resistance.vacuum
data-res-debuff=item.resistance.debuff
data-users=item.users.join(',')
)
td: strong= item.name
td.text-right: code= item.defense

View File

@ -1,7 +1,7 @@
extends master.pug
block tab-content
div.table-responsive: table.table.table-sm.table-borderless.table-striped.table-hover
table#main-table.table.table-sm.table-borderless.table-striped.table-hover.sticky-header
tr.header-above
th(colspan="2")
th(colspan="3").text-center.bg-info.text-light Reward
@ -9,23 +9,23 @@ block tab-content
th(colspan="5").text-center.bg-dark.text-light Resistance
tr.header
th.align-middle Img
+sortHeader('Name')
+sortHeader('Gold')
+sortHeader('Exp')
+sortHeader('Name', 'name')
+sortHeader('Gold', 'gold')
+sortHeader('Exp', 'exp')
th.align-middle Drops
+sortHeader('HP')
+sortHeader('MP')
+sortHeader('Power')
+sortHeader('Guard')
+sortHeader('Magic')
+sortHeader('Speed')
+sortHeader('HP', 'hp')
+sortHeader('MP', 'mp')
+sortHeader('Power', 'power')
+sortHeader('Guard', 'guard')
+sortHeader('Magic', 'magic')
+sortHeader('Speed', 'speed')
th.align-middle Spells
+sortHeader('Fire')
+sortHeader('Ice')
+sortHeader('Thunder')
+sortHeader('Vacuum')
+sortHeader('Debuff')
tbody: each enemy in enemies
+sortHeader('Fire', 'res-fire')
+sortHeader('Ice', 'res-ice')
+sortHeader('Thunder', 'res-thunder')
+sortHeader('Vacuum', 'res-vacuum')
+sortHeader('Debuff', 'res-debuff')
tbody.data: each enemy in enemies
tr(
data-name=enemy.name
data-gold=enemy.gold

View File

@ -1,25 +1,27 @@
extends master.pug
block tab-content
div.row: div.col-12.col-md-6: div.table-responsive
table.table.table-sm.table-borderless.table-hover
thead: tr
+sortHeader('Level')
div.row: div.col-12.col-md-6
table#main-table.table.table-sm.table-borderless.table-hover.sticky-header
thead: tr.header
+sortHeader('Level', 'level')
th Experience
+sortHeader('+ increase')
+sortHeader('% increase')
tbody: each value, i in exp
+sortHeader('+ increase', 'increase')
+sortHeader('% increase', 'increase-per')
tbody.data: each value, i in exp
- const prev = exp[i - 1];
tr
- const increaseAmount = i > 0 ? value - prev : 0;
- const increasePercent = i > 0 && prev > 0 ? increaseAmount / prev : 0;
tr(
data-level=(i + 1)
data-increase=increaseAmount
data-increase-per=increasePercent
)
td: strong= i + 1
td: code= value
td
+na(i > 0)
- const increaseAmount = value - prev;
code= increaseAmount
td
+na(i > 0 && prev > 0)
-
const increaseAmount = value - prev;
const increasePercent = increaseAmount / prev;
code= (increasePercent * 100).toFixed(2) + '%'

View File

@ -1,19 +1,20 @@
extends master.pug
block tab-content
div.row: div.col-12: div.table-responsive
table.table.table-sm.table-borderless.table-hover.table-striped
thead: tr
+sortHeader('Name')
+sortHeader('Cost')
+locationFilterForm()
table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header
thead: tr.header
+sortHeader('Name', 'name')
+sortHeader('Cost', 'cost')
th.align-middle Effect
th.align-middle Locations
tbody: each item in items
tbody.data: each item in items
tr(
data-name=item.name
data-cost=item.cost
data-locations=item.locations.join(',')
)
td: strong= item.name
td: strong.text-nowrap= item.name
td.text-right: code= item.cost.toLocaleString()
td: em= item.effect
td: ul.list-horizontal

View File

@ -7,11 +7,11 @@ html
link(rel="stylesheet" href="/static/bootstrap-icons.css")
link(rel="stylesheet" href="/static/7th-saga.css")
body
mixin sortHeader(label)
th: div.sortable
mixin sortHeader(label, col)
th(data-col=col): div.sortable
div.sortable-links
a(href="#") ▲
a(href="#") ▼
a(href="?col=" + col + "&dir=asc") ▲
a(href="?col=" + col + "&dir=desc") ▼
div.col-label.ml-1= label
mixin na(bool, text)
@ -21,15 +21,23 @@ html
small: em.text-muted= text || 'n/a'
mixin apprenticeFilterForm()
form.apprentice-filter-form: div.d-flex.justify-content-center
div.d-flex.justify-content-center.apprentice-filter-form.my-2
each apprentice in [ 'Esuna', 'Kamil', 'Olvan', 'Lejes', 'Lux', 'Valsu', 'Wilme' ]
div.form-check-inline
- const id = 'apprentice-' + apprentice;
input.form-check-input(type="checkbox" name=apprentice id=id)
label.form-check-label(for=id)= apprentice
mixin locationFilterForm()
div.d-flex.justify-content-center.location-filter-form.my-2.flex-wrap
each location in [ 'Lemele', 'Rablesk', 'Bonro', 'Zellis', 'Pell', 'Patrof', 'Bone', 'Dowaine', 'Belaine', 'Telaine', 'Pang', 'Padal', 'Polasu', 'Tiffana', 'Bilthem', 'Brush', 'Valenca', 'Bugask', 'Guanta', 'Pharano', 'Pasanda', 'Ligena', 'Palsu', 'Melenam', 'Airship' ]
div.form-check-inline
- const id = 'location-' + location;
input.form-check-input(type="checkbox" name=location id=id)
label.form-check-label(for=id)= location
div.container-fluid
ul.nav.nav-tabs.mt-4.px-4.position-sticky(style="top: 0; background-color: white;")
ul.nav.nav-tabs.mt-4.px-4.position-sticky(style="top: 0; background-color: white; z-index: 1")
li.nav-item: a.nav-link(href="/enemies" class=(context === 'enemies' ? 'active' : '')) Enemies
li.nav-item: a.nav-link(href="/spells" class=(context === 'spells' ? 'active' : '')) Spells
li.nav-item: a.nav-link(href="/items" class=(context === 'items' ? 'active' : '')) Items
@ -45,3 +53,4 @@ html
script(src="/static/jquery.js")
script(src="/static/popper.js")
script(src="/static/bootstrap.js")
script(src="/static/saga.js")

View File

@ -1,18 +1,26 @@
extends master.pug
block tab-content
div.table-responsive: table.table.table-sm.table-borderless.table-striped.table-hover
thead: tr
+sortHeader('Name')
+sortHeader('Type')
+sortHeader('MP')
+sortHeader('Power')
+sortHeader('Heals')
+sortHeader('Element')
+sortHeader('Targets')
th Effect
tbody: each spell in spells
tr
table#main-table.table.table-sm.table-borderless.table-striped.table-hover.sticky-header
thead: tr.header
+sortHeader('Name', 'name')
+sortHeader('Type', 'type')
+sortHeader('MP', 'mp')
+sortHeader('Power', 'power')
+sortHeader('Heals', 'heals')
+sortHeader('Element', 'element')
+sortHeader('Targets', 'targets')
th.align-middle Effect
tbody.data: each spell in spells
tr(
data-name=spell.name
data-type=spell.type
data-mp=spell.mp
data-power=(spell.power || 0)
data-heals=(spell.healingPower === 'max' ? 999 : spell.healingPower || 0)
data-element=(spell.element || '')
data-targets=spell.targets
)
td: strong= spell.name
td= spell.type
td= spell.mp

View File

@ -1,19 +1,22 @@
extends master.pug
block tab-content
div.row: div.col-12.col-md-6: div.table-responsive
table.table.table-sm.table-borderless.table-hover.table-striped
thead: tr
+sortHeader('Name')
+sortHeader('Power')
+sortHeader('Cost')
div.row.d-flex.justify-content-center: div.col-12.col-md-8
+apprenticeFilterForm()
table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header
thead: tr.header
+sortHeader('Name', 'name')
+sortHeader('Power', 'power')
+sortHeader('Cost', 'cost')
th.align-middle Users
th.align-middle Locations
tbody: each weapon in weapons
tbody.data: each weapon in weapons
tr(
data-name=weapon.name
data-power=weapon.attack
data-cost=weapon.cost
data-users=weapon.users.join(',')
)
td: strong= weapon.name
td.text-right: code= weapon.attack