Setting up view css and utilities

This commit is contained in:
Claude Brisson
2023-06-09 20:41:42 +02:00
parent 97cc9d91f0
commit 669f2cfc99
14 changed files with 3652 additions and 3 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
} }
} }

View 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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 547 KiB

View File

@@ -1 +1 @@
Hello World!

View 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;
})
};

View 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));
}
}

View 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);
}