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) => { app.get('/spells', (req, res) => {
res.render('spells', { res.render('spells', {
context: '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) => { app.get('/weapons', (req, res) => {
res.render('weapons', { res.render('weapons', {
context: '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) => { app.get('/armor', (req, res) => {
res.render('armor', { res.render('armor', {
context: '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: ","; content: ",";
margin-right: 4px; 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 block tab-content
+apprenticeFilterForm() +apprenticeFilterForm()
div.table-responsive table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header
table.table.table-sm.table-borderless.table-hover.table-striped thead: tr.header
thead: tr +sortHeader('Name', 'name')
+sortHeader('Name') +sortHeader('Defense', 'defense')
+sortHeader('Defense') +sortHeader('Cost', 'cost')
+sortHeader('Cost') +sortHeader('Fire', 'res-fire')
+sortHeader('Fire') +sortHeader('Ice', 'res-ice')
+sortHeader('Ice') +sortHeader('Thunder', 'res-thunder')
+sortHeader('Thunder') +sortHeader('Vacuum', 'res-vacuum')
+sortHeader('Vacuum') +sortHeader('Debuff', 'res-debuff')
+sortHeader('Debuff')
th.align-middle Users th.align-middle Users
th.align-middle Locations th.align-middle Locations
tbody: each item in accessories tbody.data: each item in accessories
tr( tr(
data-name=item.name data-name=item.name
data-defense=item.defense data-defense=item.defense
@ -26,6 +25,7 @@ block tab-content
data-res-thunder=item.resistance.thunder data-res-thunder=item.resistance.thunder
data-res-vacuum=item.resistance.vacuum data-res-vacuum=item.resistance.vacuum
data-res-debuff=item.resistance.debuff data-res-debuff=item.resistance.debuff
data-users=item.users.join(',')
) )
td: strong= item.name td: strong= item.name
td.text-right: code= item.defense td.text-right: code= item.defense

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,11 @@ html
link(rel="stylesheet" href="/static/bootstrap-icons.css") link(rel="stylesheet" href="/static/bootstrap-icons.css")
link(rel="stylesheet" href="/static/7th-saga.css") link(rel="stylesheet" href="/static/7th-saga.css")
body body
mixin sortHeader(label) mixin sortHeader(label, col)
th: div.sortable th(data-col=col): div.sortable
div.sortable-links div.sortable-links
a(href="#") ▲ a(href="?col=" + col + "&dir=asc") ▲
a(href="#") ▼ a(href="?col=" + col + "&dir=desc") ▼
div.col-label.ml-1= label div.col-label.ml-1= label
mixin na(bool, text) mixin na(bool, text)
@ -21,15 +21,23 @@ html
small: em.text-muted= text || 'n/a' small: em.text-muted= text || 'n/a'
mixin apprenticeFilterForm() 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' ] each apprentice in [ 'Esuna', 'Kamil', 'Olvan', 'Lejes', 'Lux', 'Valsu', 'Wilme' ]
div.form-check-inline div.form-check-inline
- const id = 'apprentice-' + apprentice; - const id = 'apprentice-' + apprentice;
input.form-check-input(type="checkbox" name=apprentice id=id) input.form-check-input(type="checkbox" name=apprentice id=id)
label.form-check-label(for=id)= apprentice 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 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="/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="/spells" class=(context === 'spells' ? 'active' : '')) Spells
li.nav-item: a.nav-link(href="/items" class=(context === 'items' ? 'active' : '')) Items 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/jquery.js")
script(src="/static/popper.js") script(src="/static/popper.js")
script(src="/static/bootstrap.js") script(src="/static/bootstrap.js")
script(src="/static/saga.js")

View File

@ -1,18 +1,26 @@
extends master.pug extends master.pug
block tab-content 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
thead: tr thead: tr.header
+sortHeader('Name') +sortHeader('Name', 'name')
+sortHeader('Type') +sortHeader('Type', 'type')
+sortHeader('MP') +sortHeader('MP', 'mp')
+sortHeader('Power') +sortHeader('Power', 'power')
+sortHeader('Heals') +sortHeader('Heals', 'heals')
+sortHeader('Element') +sortHeader('Element', 'element')
+sortHeader('Targets') +sortHeader('Targets', 'targets')
th Effect th.align-middle Effect
tbody: each spell in spells tbody.data: each spell in spells
tr 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: strong= spell.name
td= spell.type td= spell.type
td= spell.mp td= spell.mp

View File

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