Setting up view css and utilities
This commit is contained in:
@@ -73,6 +73,28 @@
|
|||||||
</classpathDependencyExcludes>
|
</classpathDependencyExcludes>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<!-- sass to css -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.gitlab.haynes</groupId>
|
||||||
|
<artifactId>libsass-maven-plugin</artifactId>
|
||||||
|
<version>0.2.29</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>generate-resources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<inputPath>${basedir}/src/main/sass/</inputPath>
|
||||||
|
<inputSyntax>scss</inputSyntax>
|
||||||
|
<outputPath>${project.build.directory}/${project.build.finalName}/css</outputPath>
|
||||||
|
<generateSourceMap>true</generateSourceMap>
|
||||||
|
<sourceMapOutputPath>${project.build.directory}/${project.build.finalName}/css</sourceMapOutputPath>
|
||||||
|
<!-- <includePath>${basedir}/src/main/sass/plugins/</includePath> -->
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
@@ -5,8 +5,30 @@
|
|||||||
<title>Pairgoth</title>
|
<title>Pairgoth</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Pairgoth Go Paring Engine">
|
<meta name="description" content="Pairgoth Go Paring Engine">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/fonts/fork-awesome.min.css">
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="header">
|
||||||
|
<div id="logo">Pairgoth v0.1</div>
|
||||||
|
<div id="menu">
|
||||||
|
<button>New tournament</button>
|
||||||
|
<button>Players</button>
|
||||||
|
<button>Scales</button>
|
||||||
|
<button>Pairing</button>
|
||||||
|
<button>Results</button>
|
||||||
|
<button>Standings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="center">
|
||||||
#translate($page)
|
#translate($page)
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript" src="/js/formproxy.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/api.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@@ -31,7 +31,9 @@ class DispatchingFilter : Filter {
|
|||||||
val uri = req.requestURI
|
val uri = req.requestURI
|
||||||
when {
|
when {
|
||||||
uri.endsWith('/') -> response.sendRedirect("${uri}index")
|
uri.endsWith('/') -> response.sendRedirect("${uri}index")
|
||||||
uri.contains('.') -> defaultRequestDispatcher.forward(request, response)
|
uri.contains('.') ->
|
||||||
|
if (uri.endsWith(".html")) resp.sendError(404)
|
||||||
|
else defaultRequestDispatcher.forward(request, response)
|
||||||
else -> chain.doFilter(request, response)
|
else -> chain.doFilter(request, response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
view-webapp/src/main/sass/main.scss
Normal file
28
view-webapp/src/main/sass/main.scss
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
body {
|
||||||
|
font-size : clamp(2rem, 10vw, 5rem);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
justify-items: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
height: 1.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#center {
|
||||||
|
max-width: clamp(800px, 80vw, 100vw);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
12
view-webapp/src/main/webapp/fonts/fork-awesome.min.css
vendored
Normal file
12
view-webapp/src/main/webapp/fonts/fork-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.eot
Normal file
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.eot
Normal file
Binary file not shown.
3232
view-webapp/src/main/webapp/fonts/forkawesome-webfont.svg
Normal file
3232
view-webapp/src/main/webapp/fonts/forkawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 547 KiB |
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.ttf
Normal file
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.woff
Normal file
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.woff
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.woff2
Normal file
BIN
view-webapp/src/main/webapp/fonts/forkawesome-webfont.woff2
Normal file
Binary file not shown.
@@ -1 +1 @@
|
|||||||
Hello World!
|
|
||||||
|
70
view-webapp/src/main/webapp/js/api.js
Normal file
70
view-webapp/src/main/webapp/js/api.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// API
|
||||||
|
|
||||||
|
const apiVersion = '1.0';
|
||||||
|
|
||||||
|
// Usage :
|
||||||
|
// api.put('user', {user_id: 12, first_name: 'Toto', ... })
|
||||||
|
// .then(ret => ret.json())
|
||||||
|
// .then(json => { ... })
|
||||||
|
// .catch(err => { ... });
|
||||||
|
|
||||||
|
const base = '/api/';
|
||||||
|
let headers = function() {
|
||||||
|
let ret = {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Accept-Version": apiVersion,
|
||||||
|
"Accept": "application/json",
|
||||||
|
"X-Browser-Key": store('browserKey')
|
||||||
|
};
|
||||||
|
let accessToken = store('accessToken');
|
||||||
|
if (accessToken) {
|
||||||
|
ret['Authorization'] = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
let api = {
|
||||||
|
get: (path) => fetch(base + path, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: headers()
|
||||||
|
}),
|
||||||
|
post: (path, body) => fetch(base + path, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: headers()
|
||||||
|
}),
|
||||||
|
put: (path, body) => fetch(base + path, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: headers()
|
||||||
|
}),
|
||||||
|
delete: (path, body) => fetch(base + path, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: headers()
|
||||||
|
}),
|
||||||
|
|
||||||
|
/* then, some helpers */
|
||||||
|
|
||||||
|
getJson: (path) => api.get(path)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.ok) return resp.json();
|
||||||
|
else throw resp.statusText;
|
||||||
|
}),
|
||||||
|
|
||||||
|
postJson: (path, body) => api.post(path, body)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.ok) return resp.json();
|
||||||
|
else throw resp.statusText;
|
||||||
|
}),
|
||||||
|
|
||||||
|
putJson: (path, body) => api.put(path, body)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.ok) return resp.json();
|
||||||
|
else throw resp.statusText;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
163
view-webapp/src/main/webapp/js/formproxy.js
Normal file
163
view-webapp/src/main/webapp/js/formproxy.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// hack to replace .34 into 0.34 (CB TODO - upstream patch to inputmask)
|
||||||
|
const fixNumber = (value) => isNaN(value) || !`${value}`.startsWith('.') ? value : `0.${value}`;
|
||||||
|
|
||||||
|
class FormProxy {
|
||||||
|
constructor(formSelector, config) {
|
||||||
|
this.formSelector = formSelector;
|
||||||
|
this.config = config;
|
||||||
|
this.config.properties = () => Object.keys(this.config).filter(k => typeof(this.config[k]) !== 'function');
|
||||||
|
this.promises = [];
|
||||||
|
this.dirty = false;
|
||||||
|
|
||||||
|
this.config.import = function(obj) {
|
||||||
|
for (let key in obj) {
|
||||||
|
if (key in this.config) {
|
||||||
|
this.proxy[key] = obj[key];
|
||||||
|
} else {
|
||||||
|
console.warn(`ignoring property ${key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.config.dirty(false);
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
this.config.export = function() {
|
||||||
|
let ret = {}
|
||||||
|
this.config.properties().forEach(prop => {
|
||||||
|
ret[prop] = this.proxy[prop];
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
this.config.dirty = function(value) {
|
||||||
|
if (typeof(value) === 'undefined') return thisProxy.dirty;
|
||||||
|
else thisProxy.dirty = Boolean(value);
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
this.config.valid = function() {
|
||||||
|
return $(`${thisProxy.formSelector} [required]:invalid`).length === 0
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
this.config.reset = function() {
|
||||||
|
this.initialize();
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
// CB TODO - needs a function to wait for promises coming from dependencies
|
||||||
|
|
||||||
|
this.setState('loading');
|
||||||
|
$(() => this.configure.bind(this)());
|
||||||
|
|
||||||
|
let thisProxy = this;
|
||||||
|
return this.proxy = new Proxy(config, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (typeof(target[prop]) === 'function') {
|
||||||
|
return target[prop];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let elem = config[prop];
|
||||||
|
if (typeof(elem) === 'undefined') throw `invalid property: ${prop}`
|
||||||
|
return elem.getter();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(target, prop, value) {
|
||||||
|
let def = config[prop];
|
||||||
|
if (typeof(def) === 'undefined') throw `invalid property: ${prop}`
|
||||||
|
let depends = [].concat(def.depends ? def.depends : []);
|
||||||
|
let proms = depends.flatMap(field => config[field].promise).filter(prom => prom);
|
||||||
|
let operation = () => {
|
||||||
|
def.setter(value);
|
||||||
|
if (typeof(def.change) === 'function') {
|
||||||
|
let rst = def.change(value, def.elem);
|
||||||
|
if (typeof(rst?.then) === 'function') {
|
||||||
|
def.promise = rst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (proms.length) Promise.all(proms).then(() => operation());
|
||||||
|
else operation();
|
||||||
|
config.dirty(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
configure() {
|
||||||
|
this.form = $(this.formSelector);
|
||||||
|
if (!this.form.length) throw `Form not found: ${this.formSelector}`;
|
||||||
|
this.form.on('submit', e => { e.preventDefault(); return false; });
|
||||||
|
let controls = this.form.find('input[name],select[name],textarea[name]');
|
||||||
|
controls.on('input change keyup', e => {
|
||||||
|
this.setState('editing');
|
||||||
|
this.config.dirty(true);
|
||||||
|
});
|
||||||
|
controls.each((i,e) => {
|
||||||
|
let name = $(e).attr('name');
|
||||||
|
if (!(name in this.config)) this.config[name] = {};
|
||||||
|
});
|
||||||
|
this.config.properties().forEach(key => {
|
||||||
|
let def = this.config[key];
|
||||||
|
if (!def) def = this.config[key] = {};
|
||||||
|
else if (typeof(def) === 'function') return true; // continue foreach
|
||||||
|
if (!def.getter) {
|
||||||
|
let elem = def.elem;
|
||||||
|
if (!elem || !elem.length) elem = $(`${this.formSelector} [name="${key}"]`);
|
||||||
|
if (!elem || !elem.length) elem = $(`#${key}`);
|
||||||
|
if (!elem || !elem.length) throw `element not found: ${key}`;
|
||||||
|
def.elem = elem;
|
||||||
|
def.getter = elem.is('input,select,textarea')
|
||||||
|
? elem.attr('type') === 'radio'
|
||||||
|
? (() => elem.filter(':checked').val())
|
||||||
|
: (() => elem.data('default')
|
||||||
|
? elem.val()
|
||||||
|
? elem.is('.number')
|
||||||
|
? elem.val().replace(/ /g, '')
|
||||||
|
: elem.val()
|
||||||
|
: elem.data('default')
|
||||||
|
: elem.is('.number')
|
||||||
|
? elem.val() ? elem.val().replace(/ /g, '') : elem.val()
|
||||||
|
: elem.val())
|
||||||
|
: (() => elem.text());
|
||||||
|
def.setter = elem.is('input,select,textarea')
|
||||||
|
? elem.attr('type') === 'radio'
|
||||||
|
? (value => elem.filter(`[value="${value}"]`).prop('checked', true))
|
||||||
|
: elem.is('input.number') ? (value => elem.val(fixNumber(value))) : (value => elem.val(value))
|
||||||
|
: (value => elem.text(value));
|
||||||
|
if (typeof(def.change) === 'function') {
|
||||||
|
elem.on('change', () => def.change(def.getter(), elem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let loading = def?.loading;
|
||||||
|
switch (typeof(loading)) {
|
||||||
|
case 'function':
|
||||||
|
let rst = loading(def.elem);
|
||||||
|
if (typeof(rst?.then) === 'function') {
|
||||||
|
this.promises.push(rst);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
Promise.all(this.promises).then(() => { this.promises = []; this.initialize(); });
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.config.properties().forEach(key => {
|
||||||
|
let def = this.config[key];
|
||||||
|
if (typeof(def.initial) === 'undefined') {
|
||||||
|
this.proxy[key] = '';
|
||||||
|
} else {
|
||||||
|
if (typeof(def.initial) === 'function') {
|
||||||
|
def.initial(def.elem)
|
||||||
|
} else if (def.initial != null) {
|
||||||
|
this.proxy[key] = def.initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.config.dirty(false);
|
||||||
|
this.setState('initial');
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state) {
|
||||||
|
if (this.form && this.form.length) this.form[0].dispatchEvent(new Event(state));
|
||||||
|
}
|
||||||
|
}
|
98
view-webapp/src/main/webapp/js/main.js
Normal file
98
view-webapp/src/main/webapp/js/main.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Utilities
|
||||||
|
|
||||||
|
const characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
function randomString(length) {
|
||||||
|
let result = '';
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
for ( let i = 0; i < length; i++ ) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeObject tweak to allow '.' to nest keys, and '-' in keys
|
||||||
|
$.extend(FormSerializer.patterns, {
|
||||||
|
fixed: /^\d+$/,
|
||||||
|
validate: /^[a-z][a-z0-9_-]*(?:\.[a-z0-9_-]+|\[[0-9]+\])*(?:\[\])?$/i,
|
||||||
|
key: /[a-z0-9_-]+|(?=\[\])/gi,
|
||||||
|
named: /^[a-z0-9_-]+$/i
|
||||||
|
});
|
||||||
|
|
||||||
|
// deserializeObject
|
||||||
|
jQuery.fn.populate = function (data) {
|
||||||
|
if (!this.is('form')) throw "Error: ${this} is not a form";
|
||||||
|
populate(this[0], data);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toasts
|
||||||
|
|
||||||
|
function clearNotif() {
|
||||||
|
let infoBox = $('#information');
|
||||||
|
infoBox.removeClass('alert-info alert-success alert-warning alert-danger show');
|
||||||
|
infoBox.addClass('hide');
|
||||||
|
infoBox.find('.toast-text').text('');
|
||||||
|
store.remove('notif');
|
||||||
|
}
|
||||||
|
|
||||||
|
function notif(type, msg) {
|
||||||
|
console.log(type)
|
||||||
|
let infoBox = $('#information');
|
||||||
|
clearNotif();
|
||||||
|
infoBox.removeClass('hide');
|
||||||
|
infoBox.addClass(`alert-${type} show`);
|
||||||
|
infoBox.find('.toast-text').text(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// crypto
|
||||||
|
|
||||||
|
async function digestMessage(message) {
|
||||||
|
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// number formats
|
||||||
|
|
||||||
|
function setDecimals() {
|
||||||
|
// due to a W3C decision, "number" inputs do not expose their selection, breaking inputmask library
|
||||||
|
$('input[data-decimals="0"]').inputmask({ alias: 'integer', placeholder: '0', groupSeparator: ' ' });
|
||||||
|
$('input[data-decimals="1"]').inputmask({ alias: 'numeric', placeholder: '0', groupSeparator: ' ', digits: 1 });
|
||||||
|
$('input[data-decimals="2"]').inputmask({ alias: 'numeric', placeholder: '0', groupSeparator: ' ', digits: 2 });
|
||||||
|
$('input[data-decimals="3"]').inputmask({ alias: 'numeric', placeholder: '0', groupSeparator: ' ', digits: 3 });
|
||||||
|
$('input[data-decimals="4"]').inputmask({ alias: 'numeric', placeholder: '0', groupSeparator: ' ', digits: 4 });
|
||||||
|
$('input.number:not([data-decimals]):not([data-digits])').inputmask({ alias: 'numeric', placeholder: '', groupSeparator: ' '});
|
||||||
|
$('input[data-digits="2"]').inputmask({ alias: 'currency', placeholder: '0', groupSeparator: ' ', digits: 2, digitsOptional: false });
|
||||||
|
$('input[data-digits="4"]').inputmask({ alias: 'currency', placeholder: '0', groupSeparator: ' ', digits: 4, digitsOptional: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
setDecimals();
|
||||||
|
});
|
||||||
|
|
||||||
|
function populateSelect(select, list, empty = false) {
|
||||||
|
select.empty();
|
||||||
|
if (empty) select.append('<option></option>');
|
||||||
|
list.forEach(option => select.append(`<option value="${option.key}">${option.value}</option>`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function spinner(show) {
|
||||||
|
if (show) $('#backdrop').addClass('active');
|
||||||
|
else $('#backdrop').removeClass('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCSV(filename, content) {
|
||||||
|
let body = content.map(s => [].concat(s).join(';')).join('\n');
|
||||||
|
let blob = new Blob(['\uFEFF', body], {type: 'text/csv;charset=utf-8'});
|
||||||
|
let link = document.createElement("a");
|
||||||
|
let url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", filename);
|
||||||
|
//link.setAttribute("target", "_blank")
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
Reference in New Issue
Block a user