Setting up view css and utilities
This commit is contained in:
@@ -73,6 +73,28 @@
|
||||
</classpathDependencyExcludes>
|
||||
</configuration>
|
||||
</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>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
|
@@ -5,8 +5,30 @@
|
||||
<title>Pairgoth</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
<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)
|
||||
</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>
|
||||
</html>
|
||||
|
||||
|
@@ -31,7 +31,9 @@ class DispatchingFilter : Filter {
|
||||
val uri = req.requestURI
|
||||
when {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
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