diff --git a/web/server.js b/web/server.js index 6b6beb8..cdb4e22 100644 --- a/web/server.js +++ b/web/server.js @@ -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) + },), }); }); diff --git a/web/static/7th-saga.css b/web/static/7th-saga.css index 175ca56..cd9f41e 100644 --- a/web/static/7th-saga.css +++ b/web/static/7th-saga.css @@ -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; +} diff --git a/web/static/saga.js b/web/static/saga.js new file mode 100644 index 0000000..fa3a5c3 --- /dev/null +++ b/web/static/saga.js @@ -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)); diff --git a/web/views/accessories.pug b/web/views/accessories.pug index 1dbc1b7..331827b 100644 --- a/web/views/accessories.pug +++ b/web/views/accessories.pug @@ -3,42 +3,42 @@ 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') - th.align-middle Users - th.align-middle Locations - tbody: each item in accessories - tr( - data-name=item.name - data-defense=item.defense - data-cost=item.cost - data-res-fire=item.resistance.fire - data-res-ice=item.resistance.ice - data-res-thunder=item.resistance.thunder - data-res-vacuum=item.resistance.vacuum - data-res-debuff=item.resistance.debuff - ) - td: strong= item.name - td.text-right: code= item.defense - td.text-right: code= item.cost.toLocaleString() - td.text-right: code= item.resistance.fire - td.text-right: code= item.resistance.ice - td.text-right: code= item.resistance.thunder - td.text-right: code= item.resistance.vacuum - td.text-right: code= item.resistance.debuff - td: ul.list-horizontal - each char in item.users - li= char - td: ul.list-horizontal - each location in item.locations - li= location + 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.data: each item in accessories + tr( + data-name=item.name + data-defense=item.defense + data-cost=item.cost + data-res-fire=item.resistance.fire + data-res-ice=item.resistance.ice + 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 + td.text-right: code= item.cost.toLocaleString() + td.text-right: code= item.resistance.fire + td.text-right: code= item.resistance.ice + td.text-right: code= item.resistance.thunder + td.text-right: code= item.resistance.vacuum + td.text-right: code= item.resistance.debuff + td: ul.list-horizontal + each char in item.users + li= char + td: ul.list-horizontal + each location in item.locations + li= location diff --git a/web/views/armor.pug b/web/views/armor.pug index dc97807..20843fd 100644 --- a/web/views/armor.pug +++ b/web/views/armor.pug @@ -1,42 +1,44 @@ 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') - th.align-middle Users - th.align-middle Locations - tbody: each item in armor - tr( - data-name=item.name - data-defense=item.defense - data-cost=item.cost - data-res-fire=item.resistance.fire - data-res-ice=item.resistance.ice - data-res-thunder=item.resistance.thunder - data-res-vacuum=item.resistance.vacuum - data-res-debuff=item.resistance.debuff - ) - td: strong= item.name - td.text-right: code= item.defense - td.text-right: code= item.cost.toLocaleString() - td.text-right: code= item.resistance.fire - td.text-right: code= item.resistance.ice - td.text-right: code= item.resistance.thunder - td.text-right: code= item.resistance.vacuum - td.text-right: code= item.resistance.debuff - td: ul.list-horizontal - each char in item.users - li= char - td: ul.list-horizontal - each location in item.locations - li= location + +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.data: each item in armor + tr( + data-name=item.name + data-defense=item.defense + data-cost=item.cost + data-res-fire=item.resistance.fire + data-res-ice=item.resistance.ice + 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 + td.text-right: code= item.cost.toLocaleString() + td.text-right: code= item.resistance.fire + td.text-right: code= item.resistance.ice + td.text-right: code= item.resistance.thunder + td.text-right: code= item.resistance.vacuum + td.text-right: code= item.resistance.debuff + td: ul.list-horizontal + each char in item.users + li= char + td: ul.list-horizontal + each location in item.locations + li= location diff --git a/web/views/enemies.pug b/web/views/enemies.pug index 3ccf0a1..0416c29 100644 --- a/web/views/enemies.pug +++ b/web/views/enemies.pug @@ -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 diff --git a/web/views/exp.pug b/web/views/exp.pug index b00ba6d..2e761de 100644 --- a/web/views/exp.pug +++ b/web/views/exp.pug @@ -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) + '%' diff --git a/web/views/items.pug b/web/views/items.pug index 3d6639a..36242bc 100644 --- a/web/views/items.pug +++ b/web/views/items.pug @@ -1,22 +1,23 @@ 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') - th.align-middle Effect - th.align-middle Locations - tbody: each item in items - tr( - data-name=item.name - data-cost=item.cost - ) - td: strong= item.name - td.text-right: code= item.cost.toLocaleString() - td: em= item.effect - td: ul.list-horizontal - each location in item.locations - li= location + +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.data: each item in items + tr( + data-name=item.name + data-cost=item.cost + data-locations=item.locations.join(',') + ) + td: strong.text-nowrap= item.name + td.text-right: code= item.cost.toLocaleString() + td: em= item.effect + td: ul.list-horizontal + each location in item.locations + li= location diff --git a/web/views/master.pug b/web/views/master.pug index 8edda2b..19f5492 100644 --- a/web/views/master.pug +++ b/web/views/master.pug @@ -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") diff --git a/web/views/spells.pug b/web/views/spells.pug index 3c1a60a..cf23f4c 100644 --- a/web/views/spells.pug +++ b/web/views/spells.pug @@ -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 diff --git a/web/views/weapons.pug b/web/views/weapons.pug index da8a937..74ad77c 100644 --- a/web/views/weapons.pug +++ b/web/views/weapons.pug @@ -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