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
|
|
@ -350,7 +350,7 @@ export async function search(term, pagination) {
|
|||
const values = ['%'+term.toUpperCase()+'%', start_id, pagination.items_per_page];
|
||||
|
||||
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 '+
|
||||
` ORDER BY username OFFSET $2 LIMIT $3`, values);
|
||||
const characters = await pool.query('SELECT id, name, account_id, faction_id FROM characters ' +
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import UserList from './views/UserList.svelte';
|
|||
import AdminPanel from './views/AdminPanel.svelte';
|
||||
import CharacterList from './views/CharacterList.svelte';
|
||||
|
||||
// Defined by webpack
|
||||
let APP_VERSION = __VERSION__;
|
||||
|
||||
// prevent view pop-in
|
||||
let initialized = false;
|
||||
|
||||
|
|
@ -117,7 +120,9 @@ page("*", setRoute(BadRoute));
|
|||
|
||||
<footer class="footer">
|
||||
<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/>
|
||||
©2019, PSForever.net, All Rights Reserved.<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.
|
||||
|
|
@ -127,3 +132,25 @@ All other trademarks or tradenames are properties of their respective owners.
|
|||
|
||||
{/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">×</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>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,19 @@
|
|||
export let character;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<span class="character-link">
|
||||
<span class="faction-icon">
|
||||
{#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}
|
||||
<img height=32 src="/img/tr_icon.png" alt="TR" />
|
||||
{:else if character.faction_id == 2}
|
||||
<img height=32 src="/img/vs_icon.png" alt="VS" />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if $isAdmin}
|
||||
<a href="/user/{character.account_id}">{character.name}</a>
|
||||
|
|
|
|||
|
|
@ -22,10 +22,8 @@
|
|||
|
||||
<PaginatedList {fetch} let:data={logins} let:pagination={pagination}>
|
||||
<p slot="header">
|
||||
{#if pagination.item_count}
|
||||
Login data
|
||||
{:else}
|
||||
No logins yet
|
||||
{#if !pagination.item_count}
|
||||
No logins yet.
|
||||
{/if}
|
||||
</p>
|
||||
<table slot="body" class="table table-dark table-responsive">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import Pagination from '../components/Pagination'
|
||||
|
||||
export let setURLParam = false;
|
||||
export let URLSearchName = 'page';
|
||||
export let fetch;
|
||||
|
||||
let data;
|
||||
|
|
@ -18,8 +19,9 @@
|
|||
const url = new URL(window.location.href)
|
||||
let initialPage = 1;
|
||||
|
||||
console.log(setURLParam, URLSearchName)
|
||||
if (setURLParam) {
|
||||
let param = parseInt(url.searchParams.get('page'))
|
||||
let param = parseInt(url.searchParams.get(URLSearchName))
|
||||
|
||||
if (param != NaN)
|
||||
initialPage = param;
|
||||
|
|
@ -32,6 +34,13 @@
|
|||
if (pagination.page == page || fetching)
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -56,9 +65,15 @@
|
|||
{#if data}
|
||||
<slot name="header" data={data} pagination={pagination}></slot>
|
||||
{#if pagination.item_count > 0}
|
||||
<Pagination {pagination} {pageChange} {setURLParam} />
|
||||
{#if pagination.page_count > 1}
|
||||
<Pagination {pagination} {pageChange} {setURLParam} {URLSearchName} />
|
||||
{/if}
|
||||
|
||||
<slot name="body" data={data} pagination={pagination}></slot>
|
||||
<Pagination {pagination} {pageChange} {setURLParam} />
|
||||
|
||||
{#if pagination.page_count > 1}
|
||||
<Pagination {pagination} {pageChange} {setURLParam} {URLSearchName} />
|
||||
{/if}
|
||||
{/if}
|
||||
<slot name="footer" data={data} pagination={pagination}></slot>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<script>
|
||||
import page from 'page'
|
||||
export let pagination;
|
||||
export let pageChange;
|
||||
export let setURLParam = false;
|
||||
|
||||
export let URLSearchName = 'page';
|
||||
export let displayPages = 10;
|
||||
|
||||
let pages = []
|
||||
|
||||
function pageClick(event) {
|
||||
const page = event.target.getAttribute('data-page');
|
||||
pageChange(parseInt(page))
|
||||
|
||||
if (!setURLParam)
|
||||
const target_page = parseInt(event.target.getAttribute('data-page'));
|
||||
event.preventDefault()
|
||||
pageChange(target_page)
|
||||
}
|
||||
|
||||
$ : {
|
||||
|
|
@ -66,8 +66,8 @@
|
|||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm">
|
||||
<li class="page-item" class:disabled={pagination.page<=1}>
|
||||
<a class="page-link" href={"?page="+(pagination.page-1)}
|
||||
on:click={pageClick}
|
||||
<a class="page-link" href="?{URLSearchName}={pagination.page-1}"
|
||||
on:click|preventDefault={pageClick}
|
||||
data-page={pagination.page-1}
|
||||
aria-label="Previous">
|
||||
«
|
||||
|
|
@ -78,14 +78,14 @@
|
|||
<li class="page-item page-last-separator disabled"><span class="page-link">...</span></li>
|
||||
{:else}
|
||||
<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>
|
||||
{/if}
|
||||
{/each}
|
||||
<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}
|
||||
on:click={pageClick}
|
||||
on:click|preventDefault={pageClick}
|
||||
aria-label="Next">
|
||||
»
|
||||
</a>
|
||||
|
|
|
|||
19
app/util/navigation.js
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import UserList from '../views/UserList'
|
||||
import CharacterList from '../views/CharacterList'
|
||||
import CharacterLink from '../components/CharacterLink'
|
||||
import AccountLink from '../components/AccountLink'
|
||||
import { monitor_tabs } from '../util/navigation'
|
||||
import axios from 'axios'
|
||||
export let appAlert;
|
||||
|
||||
|
|
@ -17,6 +19,10 @@
|
|||
appAlert.message(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => monitor_tabs((tab) => {
|
||||
console.log(tab);
|
||||
}));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -40,13 +46,11 @@
|
|||
</ul>
|
||||
|
||||
<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}>
|
||||
<div class="form-group mx-sm-3 mb-2">
|
||||
<label for="inputSearch" class="sr-only">Search</label>
|
||||
<input type="text" class="form-control" id="inputSearch" name="search" placeholder="Search" minlength=3 required>
|
||||
<div class="form-group mx-sm-3">
|
||||
<input type="text" class="form-control" id="inputSearch" name="search" placeholder="Username/Character Name" minlength=3 required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2">Search</button>
|
||||
</form>
|
||||
{#if results}
|
||||
|
|
@ -66,10 +70,10 @@
|
|||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="users" role="tabpanel" aria-labelledby="users-tab">
|
||||
<UserList {appAlert} />
|
||||
<div class="tab-pane" id="users" role="tabpanel" aria-labelledby="users-tab">
|
||||
<UserList setURLParam={true} {appAlert} />
|
||||
</div>
|
||||
<div class="tab-pane fade" id="characters" role="tabpanel" aria-labelledby="characters-tab">
|
||||
<CharacterList {appAlert} />
|
||||
<div class="tab-pane" id="characters" role="tabpanel" aria-labelledby="characters-tab">
|
||||
<CharacterList setURLParam={true} {appAlert} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import CharacterLink from '../components/CharacterLink'
|
||||
import PaginatedList from '../components/PaginatedList'
|
||||
import moment from 'moment'
|
||||
export let setURLParam = true;
|
||||
|
||||
export let appAlert
|
||||
|
||||
|
|
@ -19,17 +20,17 @@
|
|||
}
|
||||
</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">
|
||||
<p>{pagination.item_count.toLocaleString()} characters in the database</p>
|
||||
</div>
|
||||
|
||||
<table slot="body" class="table table-dark table-responsive">
|
||||
<thead>
|
||||
<td>ID</td>
|
||||
<td>Name</td>
|
||||
<td>Last Played</td>
|
||||
<td>Created</td>
|
||||
<table slot="body" class="table table-sm table-dark table-responsive-md table-striped table-hover">
|
||||
<thead class="thead-light">
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Last Played</th>
|
||||
<th>Created</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each characters as char, i}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>PSForever</title>
|
||||
<title>PSForever Portal</title>
|
||||
</svelte:head>
|
||||
|
||||
<Alert bind:this={alert} />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import ActionModal from '../components/ActionModal.svelte'
|
||||
import moment from 'moment'
|
||||
|
||||
export let setURLParam = false;
|
||||
export let appAlert
|
||||
let userList;
|
||||
|
||||
|
|
@ -21,23 +22,23 @@
|
|||
}
|
||||
</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">
|
||||
<p>{pagination.item_count.toLocaleString()} users in the database</p>
|
||||
</div>
|
||||
|
||||
<table slot="body" class="table table-dark table-responsive">
|
||||
<thead>
|
||||
<td>ID</td>
|
||||
<td>Username</td>
|
||||
<td>User Created</td>
|
||||
<td>Last Login</td>
|
||||
<td>Actions</td>
|
||||
<table slot="body" class="table table-sm table-dark table-responsive-md table-striped table-hover">
|
||||
<thead class="thead-light">
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>User Created</th>
|
||||
<th>Last Login</th>
|
||||
<th>Actions</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user, i}
|
||||
<tr>
|
||||
<td>#{user.id}</td>
|
||||
<th>#{user.id}</th>
|
||||
<td><AccountLink account={user} /></td>
|
||||
<td>{moment(user.created).fromNow()}</td>
|
||||
<td>{#if user.last_login.time}
|
||||
|
|
|
|||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
|
@ -4,9 +4,10 @@
|
|||
<meta charset='utf-8'>
|
||||
<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'>
|
||||
|
||||
<script defer src='/bundle.js'></script>
|
||||
|
|
|
|||
81
scss/base.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
// TODO
|
||||
|
|
@ -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 {
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
// bootstrap style overrides
|
||||
@import "custom";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
$bg-color: #171d3a;
|
||||
|
||||
@import "defaults";
|
||||
@import "base";
|
||||
@import "planetside";
|
||||
|
|
|
|||
32
scss/planetside.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
const webpack = require('webpack');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const path = require('path');
|
||||
|
||||
|
|
@ -69,6 +70,9 @@ module.exports = {
|
|||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css'
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
__VERSION__: JSON.stringify(require("./package.json").version)
|
||||
})
|
||||
],
|
||||
devServer: {
|
||||
|
|
|
|||