sortable tables, apprentice/location filters
This commit is contained in:
parent
ff9452ec3d
commit
0ac6178228
@ -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)
|
||||
},),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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
164
web/static/saga.js
Normal 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));
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) + '%'
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user