Application improvements

* Change favicon
* Display account banned status on admin search
* Pull in application version from Webpack
* Add "Feedback" button in footer and github references
* Fix faction icons to make them the same size
* PaginatedList component now supports back
* Tabbed navs support back
* Hide Pagination when only one page
* Improve admin table style and size
This commit is contained in:
Chord 2019-12-31 09:46:34 -05:00
parent 817796e181
commit fa6e168ccc
23 changed files with 244 additions and 158 deletions

View file

@ -350,7 +350,7 @@ export async function search(term, pagination) {
const values = ['%'+term.toUpperCase()+'%', start_id, pagination.items_per_page]; const values = ['%'+term.toUpperCase()+'%', start_id, pagination.items_per_page];
try { try {
const accounts = await pool.query('SELECT id, username, gm FROM accounts ' + const accounts = await pool.query('SELECT id, username, gm, inactive FROM accounts ' +
'WHERE upper(username) LIKE $1 '+ 'WHERE upper(username) LIKE $1 '+
` ORDER BY username OFFSET $2 LIMIT $3`, values); ` ORDER BY username OFFSET $2 LIMIT $3`, values);
const characters = await pool.query('SELECT id, name, account_id, faction_id FROM characters ' + const characters = await pool.query('SELECT id, name, account_id, faction_id FROM characters ' +

View file

@ -27,6 +27,9 @@ import UserList from './views/UserList.svelte';
import AdminPanel from './views/AdminPanel.svelte'; import AdminPanel from './views/AdminPanel.svelte';
import CharacterList from './views/CharacterList.svelte'; import CharacterList from './views/CharacterList.svelte';
// Defined by webpack
let APP_VERSION = __VERSION__;
// prevent view pop-in // prevent view pop-in
let initialized = false; let initialized = false;
@ -117,7 +120,9 @@ page("*", setRoute(BadRoute));
<footer class="footer"> <footer class="footer">
<div class="container text-center"> <div class="container text-center">
<span class="text-muted"> <span class="text-muted">PSFPortal {APP_VERSION} (<a href="https://github.com/psforever/PSFPortal">GitHub</a>) -
<a data-toggle="modal" data-target="#reportIssueModal" href="#feedback">Feedback</a></span>
<span class="text-muted"><br/><br/>
&copy;2019, PSForever.net, All Rights Reserved.<br/> &copy;2019, PSForever.net, All Rights Reserved.<br/>
PlanetSide is a registered trademark of Daybreak Game Company, LLC. PSForever claims no such trademarks.<br/> PlanetSide is a registered trademark of Daybreak Game Company, LLC. PSForever claims no such trademarks.<br/>
All other trademarks or tradenames are properties of their respective owners. All other trademarks or tradenames are properties of their respective owners.
@ -127,3 +132,25 @@ All other trademarks or tradenames are properties of their respective owners.
{/if} {/if}
<div class="modal fade" id="reportIssueModal" tabindex="-1" role="dialog" aria-labelledby="reportIssueLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="reportIssueLabel">How to report an issue</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul>
<li>If you are having trouble using the web application, you can contact one of the admins on Discord for support.</li>
<li>If you have found a reproducable bug in the application, please <a href="https://github.com/psforever/PSFPortal/issues/new">open a github issue</a>.</li>
<li>If have some ideas or code to improve the app, <a href="https://github.com/psforever/PSFPortal">set up the project</a>, and open a pull request!</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal" aria-label="Close">OK</button>
</div>
</div>
</div>
</div>

View file

@ -3,14 +3,19 @@
export let character; export let character;
</script> </script>
<style>
</style>
<span class="character-link"> <span class="character-link">
<span class="faction-icon">
{#if character.faction_id == 1} {#if character.faction_id == 1}
<img height=24 src="/img/nc_icon.png" alt="NC" /> <img height=32 src="/img/nc_icon.png" alt="NC" />
{:else if character.faction_id == 0} {:else if character.faction_id == 0}
<img height=32 src="/img/tr_icon.png" alt="TR" /> <img height=32 src="/img/tr_icon.png" alt="TR" />
{:else if character.faction_id == 2} {:else if character.faction_id == 2}
<img height=32 src="/img/vs_icon.png" alt="VS" /> <img height=32 src="/img/vs_icon.png" alt="VS" />
{/if} {/if}
</span>
{#if $isAdmin} {#if $isAdmin}
<a href="/user/{character.account_id}">{character.name}</a> <a href="/user/{character.account_id}">{character.name}</a>

View file

@ -22,10 +22,8 @@
<PaginatedList {fetch} let:data={logins} let:pagination={pagination}> <PaginatedList {fetch} let:data={logins} let:pagination={pagination}>
<p slot="header"> <p slot="header">
{#if pagination.item_count} {#if !pagination.item_count}
Login data No logins yet.
{:else}
No logins yet
{/if} {/if}
</p> </p>
<table slot="body" class="table table-dark table-responsive"> <table slot="body" class="table table-dark table-responsive">

View file

@ -4,6 +4,7 @@
import Pagination from '../components/Pagination' import Pagination from '../components/Pagination'
export let setURLParam = false; export let setURLParam = false;
export let URLSearchName = 'page';
export let fetch; export let fetch;
let data; let data;
@ -18,8 +19,9 @@
const url = new URL(window.location.href) const url = new URL(window.location.href)
let initialPage = 1; let initialPage = 1;
console.log(setURLParam, URLSearchName)
if (setURLParam) { if (setURLParam) {
let param = parseInt(url.searchParams.get('page')) let param = parseInt(url.searchParams.get(URLSearchName))
if (param != NaN) if (param != NaN)
initialPage = param; initialPage = param;
@ -32,6 +34,13 @@
if (pagination.page == page || fetching) if (pagination.page == page || fetching)
return return
if (setURLParam) {
const url = new URL(window.location.href);
url.searchParams.set(URLSearchName, page);
history.replaceState(null, null,
url.pathname + url.search + url.hash)
}
await list_fetch(page); await list_fetch(page);
} }
@ -56,9 +65,15 @@
{#if data} {#if data}
<slot name="header" data={data} pagination={pagination}></slot> <slot name="header" data={data} pagination={pagination}></slot>
{#if pagination.item_count > 0} {#if pagination.item_count > 0}
<Pagination {pagination} {pageChange} {setURLParam} /> {#if pagination.page_count > 1}
<slot name="body" data={data} pagination={pagination}></slot> <Pagination {pagination} {pageChange} {setURLParam} {URLSearchName} />
<Pagination {pagination} {pageChange} {setURLParam} /> {/if}
<slot name="body" data={data} pagination={pagination}></slot>
{#if pagination.page_count > 1}
<Pagination {pagination} {pageChange} {setURLParam} {URLSearchName} />
{/if}
{/if} {/if}
<slot name="footer" data={data} pagination={pagination}></slot> <slot name="footer" data={data} pagination={pagination}></slot>
{/if} {/if}

View file

@ -1,17 +1,17 @@
<script> <script>
import page from 'page'
export let pagination; export let pagination;
export let pageChange; export let pageChange;
export let setURLParam = false;
export let URLSearchName = 'page';
export let displayPages = 10; export let displayPages = 10;
let pages = [] let pages = []
function pageClick(event) { function pageClick(event) {
const page = event.target.getAttribute('data-page'); const target_page = parseInt(event.target.getAttribute('data-page'));
pageChange(parseInt(page)) event.preventDefault()
pageChange(target_page)
if (!setURLParam)
event.preventDefault()
} }
$ : { $ : {
@ -66,8 +66,8 @@
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination pagination-sm"> <ul class="pagination pagination-sm">
<li class="page-item" class:disabled={pagination.page<=1}> <li class="page-item" class:disabled={pagination.page<=1}>
<a class="page-link" href={"?page="+(pagination.page-1)} <a class="page-link" href="?{URLSearchName}={pagination.page-1}"
on:click={pageClick} on:click|preventDefault={pageClick}
data-page={pagination.page-1} data-page={pagination.page-1}
aria-label="Previous"> aria-label="Previous">
&laquo; &laquo;
@ -78,14 +78,14 @@
<li class="page-item page-last-separator disabled"><span class="page-link">...</span></li> <li class="page-item page-last-separator disabled"><span class="page-link">...</span></li>
{:else} {:else}
<li class="page-item" class:active={page==pagination.page}> <li class="page-item" class:active={page==pagination.page}>
<a on:click={pageClick} href={"?page="+page} data-page={page} class="page-link">{page}</a> <a on:click|preventDefault={pageClick} href="?{URLSearchName}={page}" data-page={page} class="page-link">{page}</a>
</li> </li>
{/if} {/if}
{/each} {/each}
<li class="page-item" class:disabled={pagination.page>=pagination.page_count}> <li class="page-item" class:disabled={pagination.page>=pagination.page_count}>
<a class="page-link" href={"?page="+(pagination.page+1)} <a class="page-link" href="?{URLSearchName}={pagination.page+1}"
data-page={pagination.page+1} data-page={pagination.page+1}
on:click={pageClick} on:click|preventDefault={pageClick}
aria-label="Next"> aria-label="Next">
&raquo; &raquo;
</a> </a>

19
app/util/navigation.js Normal file
View file

@ -0,0 +1,19 @@
import jq from 'jquery'
export function monitor_tabs(callback) {
jq('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var hash = jq(e.target).attr('href');
if (callback)
callback(hash);
if (history.pushState) {
history.replaceState(null, null, hash);
} else {
location.hash = hash;
}
});
var hash = window.location.hash;
if (hash) {
jq('.nav-link[href="' + hash + '"]').tab('show');
}
}

View file

@ -1,8 +1,10 @@
<script> <script>
import { onMount } from 'svelte'
import UserList from '../views/UserList' import UserList from '../views/UserList'
import CharacterList from '../views/CharacterList' import CharacterList from '../views/CharacterList'
import CharacterLink from '../components/CharacterLink' import CharacterLink from '../components/CharacterLink'
import AccountLink from '../components/AccountLink' import AccountLink from '../components/AccountLink'
import { monitor_tabs } from '../util/navigation'
import axios from 'axios' import axios from 'axios'
export let appAlert; export let appAlert;
@ -17,6 +19,10 @@
appAlert.message(e.message) appAlert.message(e.message)
} }
} }
onMount(() => monitor_tabs((tab) => {
console.log(tab);
}));
</script> </script>
<svelte:head> <svelte:head>
@ -40,13 +46,11 @@
</ul> </ul>
<div class="tab-content" id="tabs-tabContent"> <div class="tab-content" id="tabs-tabContent">
<div class="tab-pane fade show active" id="search" role="tabpanel" aria-labelledby="search-tab"> <div class="tab-pane show active" id="search" role="tabpanel" aria-labelledby="search-tab">
<form name="search" class="form-inline" on:submit|preventDefault={submitSearch}> <form name="search" class="form-inline" on:submit|preventDefault={submitSearch}>
<div class="form-group mx-sm-3 mb-2"> <div class="form-group mx-sm-3">
<label for="inputSearch" class="sr-only">Search</label> <input type="text" class="form-control" id="inputSearch" name="search" placeholder="Username/Character Name" minlength=3 required>
<input type="text" class="form-control" id="inputSearch" name="search" placeholder="Search" minlength=3 required>
</div> </div>
<button type="submit" class="btn btn-primary mb-2">Search</button> <button type="submit" class="btn btn-primary mb-2">Search</button>
</form> </form>
{#if results} {#if results}
@ -66,10 +70,10 @@
</ol> </ol>
{/if} {/if}
</div> </div>
<div class="tab-pane fade" id="users" role="tabpanel" aria-labelledby="users-tab"> <div class="tab-pane" id="users" role="tabpanel" aria-labelledby="users-tab">
<UserList {appAlert} /> <UserList setURLParam={true} {appAlert} />
</div> </div>
<div class="tab-pane fade" id="characters" role="tabpanel" aria-labelledby="characters-tab"> <div class="tab-pane" id="characters" role="tabpanel" aria-labelledby="characters-tab">
<CharacterList {appAlert} /> <CharacterList setURLParam={true} {appAlert} />
</div> </div>
</div> </div>

View file

@ -4,6 +4,7 @@
import CharacterLink from '../components/CharacterLink' import CharacterLink from '../components/CharacterLink'
import PaginatedList from '../components/PaginatedList' import PaginatedList from '../components/PaginatedList'
import moment from 'moment' import moment from 'moment'
export let setURLParam = true;
export let appAlert export let appAlert
@ -19,17 +20,17 @@
} }
</script> </script>
<PaginatedList bind:fetch={fetch} let:data={characters} let:pagination={pagination}> <PaginatedList {setURLParam} URLSearchName='page_char' bind:fetch={fetch} let:data={characters} let:pagination={pagination}>
<div slot="header"> <div slot="header">
<p>{pagination.item_count.toLocaleString()} characters in the database</p> <p>{pagination.item_count.toLocaleString()} characters in the database</p>
</div> </div>
<table slot="body" class="table table-dark table-responsive"> <table slot="body" class="table table-sm table-dark table-responsive-md table-striped table-hover">
<thead> <thead class="thead-light">
<td>ID</td> <th>ID</th>
<td>Name</td> <th>Name</th>
<td>Last Played</td> <th>Last Played</th>
<td>Created</td> <th>Created</th>
</thead> </thead>
<tbody> <tbody>
{#each characters as char, i} {#each characters as char, i}

View file

@ -33,7 +33,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>PSForever</title> <title>PSForever Portal</title>
</svelte:head> </svelte:head>
<Alert bind:this={alert} /> <Alert bind:this={alert} />

View file

@ -6,6 +6,7 @@
import ActionModal from '../components/ActionModal.svelte' import ActionModal from '../components/ActionModal.svelte'
import moment from 'moment' import moment from 'moment'
export let setURLParam = false;
export let appAlert export let appAlert
let userList; let userList;
@ -21,23 +22,23 @@
} }
</script> </script>
<PaginatedList bind:this={userList} bind:fetch={fetch} let:data={users} let:pagination={pagination}> <PaginatedList {setURLParam} bind:this={userList} bind:fetch={fetch} let:data={users} let:pagination={pagination}>
<div slot="header"> <div slot="header">
<p>{pagination.item_count.toLocaleString()} users in the database</p> <p>{pagination.item_count.toLocaleString()} users in the database</p>
</div> </div>
<table slot="body" class="table table-dark table-responsive"> <table slot="body" class="table table-sm table-dark table-responsive-md table-striped table-hover">
<thead> <thead class="thead-light">
<td>ID</td> <th>ID</th>
<td>Username</td> <th>Username</th>
<td>User Created</td> <th>User Created</th>
<td>Last Login</td> <th>Last Login</th>
<td>Actions</td> <th>Actions</th>
</thead> </thead>
<tbody> <tbody>
{#each users as user, i} {#each users as user, i}
<tr> <tr>
<td>#{user.id}</td> <th>#{user.id}</th>
<td><AccountLink account={user} /></td> <td><AccountLink account={user} /></td>
<td>{moment(user.created).fromNow()}</td> <td>{moment(user.created).fromNow()}</td>
<td>{#if user.last_login.time} <td>{#if user.last_login.time}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -4,9 +4,10 @@
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'> <meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Play PSForever</title> <title>PSForever Portal</title>
<link rel='icon' type='image/png' href='/favicon.png'> <link rel='icon' href='/favicon.ico'>
<link href="https://fonts.googleapis.com/css?family=Poppins" rel="stylesheet">
<link rel='stylesheet' href='/bundle.css'> <link rel='stylesheet' href='/bundle.css'>
<script defer src='/bundle.js'></script> <script defer src='/bundle.js'></script>

81
scss/base.scss Normal file
View file

@ -0,0 +1,81 @@
html, body {
position: relative;
width: 100%;
height: 100%;
background: $bg-color;
}
body {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
// Sticky footer code
main {
min-height: calc(100vh - 160px);
margin: 20px 0;
color: white;
.modal {
color: black;
}
}
.footer {
height: 60px;
font-size: 0.75em;
}
a {
color: rgb(0,150,250);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/*a:visited {
color: rgb(0,160,190);
}*/
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
background-color: #f4f4f4;
outline: none;
}
button:active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
.form-control-plaintext {
color: white;
}

View file

@ -0,0 +1 @@
// TODO

View file

@ -1,111 +1,3 @@
$bg-color: #171d3a;
$faction-vs: #440E62;
$faction-nc: #004B80;
$faction-tr: #9E0B0F;
.faction-vs {
color: $faction-vs;
}
.faction-nc {
color: $faction-nc;
}
.faction-tr {
color: $faction-tr;
}
.faction-vs-bg {
background: $faction-vs;
}
.faction-nc-bg {
background: $faction-nc;
}
.faction-tr-bg {
background: $faction-tr;
}
html, body {
position: relative;
width: 100%;
height: 100%;
background: $bg-color;
}
body {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
// Sticky footer code
main {
min-height: calc(100vh - 160px);
margin: 20px 0;
color: white;
.modal {
color: black;
}
}
.footer {
height: 60px;
font-size: 0.75em;
}
a {
color: rgb(0,150,250);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/*a:visited {
color: rgb(0,160,190);
}*/
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
background-color: #f4f4f4;
outline: none;
}
button:active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
.form-control-plaintext {
color: white;
}
.notification-shake { .notification-shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);

View file

@ -1,4 +1,9 @@
// bootstrap style overrides // bootstrap style overrides
@import "custom"; @import "custom";
@import "~bootstrap/scss/bootstrap"; @import "~bootstrap/scss/bootstrap";
$bg-color: #171d3a;
@import "defaults"; @import "defaults";
@import "base";
@import "planetside";

32
scss/planetside.scss Normal file
View file

@ -0,0 +1,32 @@
$faction-vs: #440E62;
$faction-nc: #004B80;
$faction-tr: #9E0B0F;
.faction-vs {
color: $faction-vs;
}
.faction-nc {
color: $faction-nc;
}
.faction-tr {
color: $faction-tr;
}
.faction-vs-bg {
background: $faction-vs;
}
.faction-nc-bg {
background: $faction-nc;
}
.faction-tr-bg {
background: $faction-tr;
}
.faction-icon {
width: 32px;
display: inline-block;
}

View file

@ -1,3 +1,4 @@
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path'); const path = require('path');
@ -69,6 +70,9 @@ module.exports = {
plugins: [ plugins: [
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: '[name].css' filename: '[name].css'
}),
new webpack.DefinePlugin({
__VERSION__: JSON.stringify(require("./package.json").version)
}) })
], ],
devServer: { devServer: {