Initial commit
This commit is contained in:
237
static/apiWrapper.js
Normal file
237
static/apiWrapper.js
Normal file
@ -0,0 +1,237 @@
|
||||
_wrapperVersion = '1.0.0';
|
||||
_minApiVersion = '1.0.0';
|
||||
_maxApiVersion = '1.0.0';
|
||||
|
||||
_defaultTTL = 60000;
|
||||
|
||||
_apiConfig = {
|
||||
basePath: '/api/v1/'
|
||||
};
|
||||
|
||||
if (!window.localStorage) {
|
||||
console.warn('Local Storage is not available, some features may not work');
|
||||
}
|
||||
|
||||
// Generic driver functions
|
||||
let _api = {
|
||||
get: async function (path) {
|
||||
const options = {
|
||||
headers: new Headers({ 'content-type': 'application/json' })
|
||||
};
|
||||
const response = await fetch(_apiConfig.basePath + path, options);
|
||||
// Handle the response
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch:', response.statusText);
|
||||
_testPageFail(response.statusText);
|
||||
return;
|
||||
}
|
||||
const result = await response.json();
|
||||
// Handle the result, was json valid?
|
||||
if (!result) {
|
||||
// Is it a number instead?
|
||||
if (typeof result === 'number') {
|
||||
return result;
|
||||
}
|
||||
console.error('Invalid JSON response');
|
||||
_testPageFail('Invalid JSON response');
|
||||
return;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
post: async function (path, data) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
const response = await fetch(_apiConfig.basePath + path, options);
|
||||
// Handle the response
|
||||
if (!response.ok) {
|
||||
_testPageFail(response.statusText);
|
||||
return;
|
||||
}
|
||||
const result = await response.json();
|
||||
// Handle the result, was json valid?
|
||||
if (!result) {
|
||||
_testPageFail('Invalid JSON response');
|
||||
return;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
delete: async function (path, data) {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
const response = await fetch(_apiConfig.basePath + path, options);
|
||||
// Handle the response
|
||||
if (!response.ok) {
|
||||
_testPageFail(response.statusText);
|
||||
return;
|
||||
}
|
||||
const result = await response.json();
|
||||
// Handle the result, was json valid?
|
||||
if (!result) {
|
||||
_testPageFail('Invalid JSON response');
|
||||
return;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
patch: async function (path, data) {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
const response = await fetch(_apiConfig.basePath + path, options);
|
||||
// Handle the response
|
||||
if (!response.ok) {
|
||||
_testPageFail(response.statusText);
|
||||
return;
|
||||
}
|
||||
const result = await response.json();
|
||||
// Handle the result, was json valid?
|
||||
if (!result) {
|
||||
_testPageFail('Invalid JSON response');
|
||||
return;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function updateRow(tableName, id, data) {
|
||||
invalidateCache(tableName);
|
||||
return _api.patch(`${tableName}`, { id: id, ...data });
|
||||
}
|
||||
|
||||
function deleteRow(tableName, id) {
|
||||
invalidateCache(tableName);
|
||||
return _api.delete(`${tableName}`, { id: id });
|
||||
}
|
||||
|
||||
function getApiDescriptionByTable(tableName) {
|
||||
const keyDesc = `desc:${tableName}`;
|
||||
const keyTime = `${keyDesc}:time`;
|
||||
const keyTTL = `${keyDesc}:ttl`;
|
||||
|
||||
// Retrieve cached data
|
||||
const description = JSON.parse(localStorage.getItem(keyDesc));
|
||||
const timeCreated = parseInt(localStorage.getItem(keyTime));
|
||||
const ttl = parseInt(localStorage.getItem(keyTTL));
|
||||
|
||||
// Check if valid cached data exists
|
||||
if (description && timeCreated && ttl) {
|
||||
const currentTime = Date.now();
|
||||
const age = currentTime - parseInt(timeCreated, 10);
|
||||
if (age < parseInt(ttl, 10)) {
|
||||
// Return cached data immediately
|
||||
return Promise.resolve(description);
|
||||
} else {
|
||||
console.warn('Cached description expired; fetching new data');
|
||||
// Fetch new data, update cache, and return it
|
||||
return fetchAndUpdateCache(tableName);
|
||||
}
|
||||
} else {
|
||||
console.warn('No cached description; fetching from server');
|
||||
// Fetch data, update cache, and return it
|
||||
return fetchAndUpdateCache(tableName);
|
||||
}
|
||||
|
||||
function fetchAndUpdateCache(tableName) {
|
||||
return _api
|
||||
.get(`${tableName}/describe`)
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
// Update local storage with new data
|
||||
localStorage.setItem(keyDesc, JSON.stringify(data));
|
||||
localStorage.setItem(keyTime, Date.now().toString());
|
||||
localStorage.setItem(keyTTL, '60000'); // 60 seconds TTL
|
||||
}
|
||||
return data; // Return the fetched data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch description:', error);
|
||||
// Fallback to cached data if available (even if expired)
|
||||
return description || null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function returnTableDataByTableName(tableName, search="", orderBy="asc", sort="", take=-1, skip=0) {
|
||||
var orderBy = orderBy.toLowerCase();
|
||||
if(orderBy == "") {
|
||||
orderBy = "asc";
|
||||
}
|
||||
var baseString = tableName + "?order=" + orderBy;
|
||||
if(sort && sort.length > 0) {
|
||||
baseString += "&sort=" + sort;
|
||||
}
|
||||
if(take > 0) {
|
||||
baseString += "&take=" + take;
|
||||
}
|
||||
if(skip > 0) {
|
||||
baseString += "&skip=" + skip;
|
||||
}
|
||||
|
||||
if (search && search.length > 0) {
|
||||
return _api.get(baseString + '&search=' + search);
|
||||
} else {
|
||||
return _api.get(baseString);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCountByTable(tableName, search="") {
|
||||
let baseString = tableName + '?count=true';
|
||||
if (search && search.length > 0) {
|
||||
baseString += '&search=' + search;
|
||||
}
|
||||
// Stored in `data:count:${tableName}`
|
||||
let result = await _api.get(baseString);
|
||||
console.debug('Count result:', result);
|
||||
if (typeof result !== 'number') {
|
||||
_testPageWarn('Count was not a number, was: ' + result);
|
||||
console.warn('Count was not a number, was: ' + result);
|
||||
return -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function _testPageFail(reason) {
|
||||
document.getElementById('heroStatus').classList.remove('is-success');
|
||||
document.getElementById('heroStatus').classList.add('is-danger');
|
||||
|
||||
document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Failed, reason: ' + reason;
|
||||
}
|
||||
|
||||
function _testPageWarn(reason) {
|
||||
document.getElementById('heroStatus').classList.remove('is-success');
|
||||
document.getElementById('heroStatus').classList.add('is-warning');
|
||||
|
||||
document.getElementById('heroExplainer').innerHTML = 'API Wrapper Test Warning, reason: ' + reason;
|
||||
}
|
||||
|
||||
function getServerVersion() {
|
||||
return _api.get('version');
|
||||
}
|
||||
|
||||
function createEntry(tableName, data) {
|
||||
invalidateCache(tableName);
|
||||
return _api.post(tableName, data);
|
||||
}
|
||||
|
||||
function invalidateCache(tableName) {
|
||||
const keyDesc = `desc:${tableName}`;
|
||||
const keyTime = `${keyDesc}:time`;
|
||||
const keyTTL = `${keyDesc}:ttl`;
|
||||
|
||||
localStorage.removeItem(keyDesc);
|
||||
localStorage.removeItem(keyTime);
|
||||
localStorage.removeItem(keyTTL);
|
||||
}
|
31
static/css/lockscreen.css
Normal file
31
static/css/lockscreen.css
Normal file
@ -0,0 +1,31 @@
|
||||
#clock {
|
||||
font-size: 120px;
|
||||
font-weight: 100;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
bottom: 10%;
|
||||
right: 5%;
|
||||
z-index: 900010;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#time {
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 0px;
|
||||
text-align: center;
|
||||
width: 95%;
|
||||
vertical-align: middle;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#date {
|
||||
font-size: 50px;
|
||||
margin-top: -40px;
|
||||
}
|
3
static/favicon.svg
Normal file
3
static/favicon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg id="favicon" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="138.654" height="146.519" viewBox="0 0 36.685 38.767">
|
||||
<path d="M18.775 0A24.388 24.388 0 0 0 6.82 3.115C3.15 5.165-1.91 9.252.736 13.985c.37.66.9 1.221 1.47 1.713 1.532 1.322 2.98.222 4.554-.457.975-.42 1.95-.842 2.922-1.27.434-.19 1.01-.33 1.328-.698.858-.99.494-2.994.05-4.095a27.25 27.25 0 0 1 3.65-1.24v30.828h7.215V7.671c1.05.184 2.438.432 3.266 1.041.387.284.113.908.076 1.297-.08.827-.027 1.817.344 2.581.308.632 1.16.784 1.765 1.008l4.564 1.704c.628.232 1.33.643 1.979.297 2.822-1.507 3.574-5.39 1.843-8.023-1.165-1.77-3.255-3.13-5.035-4.216C27.037 1.107 22.906.014 18.775 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 678 B |
144
static/js/lockscreenBgHandler.js
Normal file
144
static/js/lockscreenBgHandler.js
Normal file
@ -0,0 +1,144 @@
|
||||
// Image Handler
|
||||
const baseUrl = "https://api.unsplash.com/photos/random?client_id=[KEY]&orientation=landscape&topics=nature";
|
||||
const apiKey = "tYOt7Jo94U7dunVcP5gt-kDKDMjWFOGQNsHuhLDLV8k"; // Take from config
|
||||
const fullUrl = baseUrl.replace("[KEY]", apiKey);
|
||||
|
||||
const showModeImage = "/static/media/showModeLockscreen.jpg"
|
||||
|
||||
let credits = document.getElementById("credits");
|
||||
|
||||
let currentImageHandle;
|
||||
|
||||
// Lock screen or show mode
|
||||
let screenState = "lock";
|
||||
|
||||
function handleImage() {
|
||||
if(screenState === "lock") {
|
||||
fetch("https://staging.thegreydiamond.de/projects/photoPortfolio/api/getRand.php?uuid=01919dec-b2cd-7adc-8ca2-a071d1169cbc&unsplash=true")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// data = {
|
||||
// urls: {
|
||||
// regular: "https://imageproxy.thegreydiamond.de/ra5iqxlyve6HpjNvC1tzG50a14oIOgiWP95CxIvbBC8/sm:1/kcr:1/aHR0cHM6Ly9zdGFn/aW5nLnRoZWdyZXlk/aWFtb25kLmRlL3By/b2plY3RzL3Bob3Rv/UG9ydGZvbGlvL2Rl/bW9IaVJlcy9QMTE5/MDgzMC1zY2hpbGQu/anBn.webp"
|
||||
// },
|
||||
// user: {
|
||||
// name: "Sören Oesterwind",
|
||||
// links: {
|
||||
// html: "https://thegreydiamond.de"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if(!currentImageHandle) {
|
||||
// Create a page filling div which contains the image
|
||||
currentImageHandle = document.createElement("div");
|
||||
currentImageHandle.style.position = "absolute";
|
||||
currentImageHandle.style.top = "0";
|
||||
currentImageHandle.style.left = "0";
|
||||
currentImageHandle.style.width = "100%";
|
||||
currentImageHandle.style.height = "100%";
|
||||
currentImageHandle.style.backgroundImage = `url(${data.urls.regular})`;
|
||||
currentImageHandle.style.backgroundSize = "cover";
|
||||
currentImageHandle.style.opacity = 1;
|
||||
} else {
|
||||
// Create a new div behind the current one and delete the old one when the new one is loaded
|
||||
let newImageHandle = document.createElement("div");
|
||||
newImageHandle.style.position = "absolute";
|
||||
newImageHandle.style.top = "0";
|
||||
newImageHandle.style.left = "0";
|
||||
newImageHandle.style.width = "100%";
|
||||
newImageHandle.style.height = "100%";
|
||||
newImageHandle.style.backgroundImage = `url(${data.urls.regular})`;
|
||||
newImageHandle.style.backgroundSize = "cover";
|
||||
newImageHandle.style.opacity = 1;
|
||||
newImageHandle.style.transition = "1s";
|
||||
newImageHandle.style.zIndex = 19999;
|
||||
document.body.appendChild(newImageHandle);
|
||||
|
||||
currentImageHandle.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
currentImageHandle.remove();
|
||||
newImageHandle.style.zIndex = 200000;
|
||||
currentImageHandle = newImageHandle;
|
||||
}, 1000);
|
||||
|
||||
|
||||
|
||||
// Set the credits
|
||||
credits.innerHTML = `Photo by <a href="${data.user.links.html}" target="_blank">${data.user.name}</a> on <a href="https://unsplash.com" target="_blank">Unsplash</a>`;
|
||||
credits.style.zIndex = 300000;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching image: ", error);
|
||||
});
|
||||
} else {
|
||||
if(currentImageHandle) {
|
||||
// Check if the image is already loaded
|
||||
if(currentImageHandle.style.backgroundImage === `url("${showModeImage}")`) {
|
||||
return;
|
||||
}
|
||||
// Create a new div behind the current one and delete the old one when the new one is loaded
|
||||
let newImageHandle = document.createElement("div");
|
||||
newImageHandle.style.position = "absolute";
|
||||
newImageHandle.style.top = "0";
|
||||
newImageHandle.style.left = "0";
|
||||
newImageHandle.style.width = "100%";
|
||||
newImageHandle.style.height = "100%";
|
||||
newImageHandle.style.backgroundImage = `url(${showModeImage})`;
|
||||
newImageHandle.style.backgroundSize = "cover";
|
||||
newImageHandle.style.opacity = 1;
|
||||
newImageHandle.style.transition = "1s";
|
||||
document.body.appendChild(newImageHandle);
|
||||
|
||||
setTimeout(() => {
|
||||
currentImageHandle.remove();
|
||||
currentImageHandle = newImageHandle;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimeAndDate() {
|
||||
let time = new Date();
|
||||
let hours = time.getHours();
|
||||
let minutes = time.getMinutes();
|
||||
let day = time.getDate();
|
||||
let month = time.getMonth();
|
||||
month += 1;
|
||||
let year = time.getFullYear();
|
||||
|
||||
let timeHandle = document.getElementById("time");
|
||||
let dateHandle = document.getElementById("date");
|
||||
|
||||
timeHandle.innerHTML = `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${time.getSeconds() < 10 ? "0" + time.getSeconds() : time.getSeconds()}`;
|
||||
// Datum in format Montag, 22.12.2024
|
||||
dateHandle.innerHTML = `${getDay(time.getDay())}, ${day < 10 ? "0" + day : day}.${month < 10 ? "0" + month : month}.${year}`;
|
||||
}
|
||||
|
||||
function getDay(day) {
|
||||
switch(day) {
|
||||
case 0:
|
||||
return "Sonntag";
|
||||
case 1:
|
||||
return "Montag";
|
||||
case 2:
|
||||
return "Dienstag";
|
||||
case 3:
|
||||
return "Mittwoch";
|
||||
case 4:
|
||||
return "Donnerstag";
|
||||
case 5:
|
||||
return "Freitag";
|
||||
case 6:
|
||||
return "Samstag";
|
||||
}
|
||||
}
|
||||
|
||||
// Set the image handler to run every 10 minutes
|
||||
setInterval(handleImage, 60 * 1000 * 10);
|
||||
handleImage();
|
||||
handleImage()
|
||||
|
||||
// Set the time and date handler to run every minute
|
||||
setInterval(handleTimeAndDate, 500);
|
||||
handleTimeAndDate();
|
3
static/logo.svg
Normal file
3
static/logo.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393">
|
||||
<path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
3
static/main.css
Normal file
3
static/main.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
BIN
static/media/showModeLockscreen.jpg
Normal file
BIN
static/media/showModeLockscreen.jpg
Normal file
Binary file not shown.
620
static/pageDriver.js
Normal file
620
static/pageDriver.js
Normal file
@ -0,0 +1,620 @@
|
||||
_pageDriverVersion = '1.0.1';
|
||||
|
||||
// Handle color for icon svg with id="logo" based on the current theme
|
||||
const logo = document.getElementById('logo');
|
||||
if (logo) {
|
||||
logo.style.fill = getComputedStyle(document.documentElement).getPropertyValue('--bulma-text');
|
||||
}
|
||||
|
||||
if (_wrapperVersion === undefined) {
|
||||
console.error('API Wrapper not found; Please include the API Wrapper before including the Page Driver');
|
||||
exit();
|
||||
} else {
|
||||
console.log('API Wrapper found; Page Driver is ready to use');
|
||||
}
|
||||
|
||||
// Find all tables on the page which have data-dataSource attribute
|
||||
var tables = document.querySelectorAll('table[data-dataSource]');
|
||||
//var tables = []
|
||||
|
||||
// Get all single values with data-dataSource, data-dataCol and data-dataAction
|
||||
var singleValues = document.querySelectorAll('span[data-dataSource]');
|
||||
|
||||
// Find all search fields with data-searchTargetId
|
||||
var searchFields = document.querySelectorAll('input[data-searchTargetId]');
|
||||
|
||||
// Find all modalForms
|
||||
var modalForms = document.querySelectorAll('form[data-targetTable]');
|
||||
|
||||
console.info('Processing single values');
|
||||
console.info(singleValues);
|
||||
|
||||
// Iterate over all single values
|
||||
singleValues.forEach(async (singleValue) => {
|
||||
writeSingelton(singleValue);
|
||||
});
|
||||
|
||||
// Iterate over all tables
|
||||
tables.forEach(async (table) => {
|
||||
// Get THs and attach onClick event to sort
|
||||
const ths = table.querySelectorAll('th');
|
||||
ths.forEach((th) => {
|
||||
if(th.getAttribute('fnc') == "actions") {
|
||||
return;
|
||||
}
|
||||
th.style.cursor = 'pointer';
|
||||
th.style.userSelect = 'none';
|
||||
th.addEventListener('click', async function () {
|
||||
const table = th.closest('table');
|
||||
const order = th.getAttribute('data-order');
|
||||
// Clear all other order attributes
|
||||
ths.forEach((th) => {
|
||||
if (th != this) {
|
||||
th.removeAttribute('data-order');
|
||||
th.classList.remove("bi-caret-up-fill")
|
||||
th.classList.remove("bi-caret-down-fill")
|
||||
}
|
||||
});
|
||||
if (order == 'ASC') {
|
||||
th.setAttribute('data-order', 'DESC');
|
||||
th.classList.add("bi-caret-down-fill")
|
||||
th.classList.remove("bi-caret-up-fill")
|
||||
} else {
|
||||
th.setAttribute('data-order', 'ASC');
|
||||
th.classList.add("bi-caret-up-fill")
|
||||
th.classList.remove("bi-caret-down-fill")
|
||||
}
|
||||
refreshTable(table);
|
||||
});
|
||||
});
|
||||
refreshTable(table);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
async function writeSingelton(element) {
|
||||
const table = element.getAttribute('data-dataSource');
|
||||
console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element);
|
||||
switch (element.getAttribute('data-dataAction')) {
|
||||
case 'COUNT': {
|
||||
console.log('Count action found');
|
||||
element.innerHTML = await getCountByTable(table);
|
||||
break;
|
||||
}
|
||||
case 'SPECIAL': {
|
||||
if (table == 'version') {
|
||||
element.innerHTML = (await getServerVersion())['version'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
console.error('Unknown action found: ', element.getAttribute('data-dataAction'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
element.classList.remove('is-skeleton');
|
||||
}
|
||||
|
||||
// Attach listeners to search fields
|
||||
searchFields.forEach((searchField) => {
|
||||
// Apply restrictions to search field (min, max, chars, etc)
|
||||
|
||||
getApiDescriptionByTable(document.getElementById(searchField.getAttribute('data-searchTargetId')).getAttribute('data-dataSource')).then((desc) => {
|
||||
desc = desc['GET']['keys']['search'];
|
||||
var rules = desc['rules'];
|
||||
rules.forEach((rule) => {
|
||||
switch (rule['name']) {
|
||||
case 'min': {
|
||||
searchField.setAttribute('minlength', rule['args']['limit']);
|
||||
break;
|
||||
}
|
||||
case 'max': {
|
||||
searchField.setAttribute('maxlength', rule['args']['limit']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
searchField.addEventListener('input', async function () {
|
||||
console.log('Search field changed: ', searchField);
|
||||
if (searchField.checkValidity() == false) {
|
||||
console.log('Invalid input');
|
||||
searchField.classList.add('is-danger');
|
||||
return;
|
||||
} else {
|
||||
searchField.classList.remove('is-danger');
|
||||
const targetId = searchField.getAttribute('data-searchTargetId');
|
||||
const target = document.getElementById(targetId);
|
||||
const table = target.getAttribute('data-dataSource');
|
||||
const column = target.getAttribute('data-dataCol');
|
||||
const value = searchField.value;
|
||||
console.log('Searching for ', value, ' in ', table, ' column ', column);
|
||||
refreshTableByName(table);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Attach listeners to modal forms
|
||||
modalForms.forEach((modalForm) => {
|
||||
// Add validation to form by using API description (everything is assumed POST for now)
|
||||
modalForm.addEventListener('input', async function (event) {
|
||||
if (event.target.checkValidity() == false) {
|
||||
modalForm.querySelector("input[type='submit']").setAttribute('disabled', true);
|
||||
event.target.classList.add('is-danger');
|
||||
return;
|
||||
} else {
|
||||
modalForm.querySelector("input[type='submit']").removeAttribute('disabled');
|
||||
event.target.classList.remove('is-danger');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
getApiDescriptionByTable(modalForm.getAttribute('data-targetTable')).then((desc) => {
|
||||
console.log('Description: ', desc);
|
||||
const keys = desc['POST']['keys'];
|
||||
// Apply resitrictions and types to form fields
|
||||
for (key in keys) {
|
||||
const field = modalForm.querySelector("input[name='" + key + "']");
|
||||
if (field) {
|
||||
const rules = keys[key]['rules'];
|
||||
const flags = keys[key]['flags'];
|
||||
console.log('Field: ', field, ' Rules: ', rules, ' Flags: ', flags);
|
||||
rules.forEach((rule) => {
|
||||
switch (rule['name']) {
|
||||
case 'min': {
|
||||
field.setAttribute('minlength', rule['args']['limit']);
|
||||
break;
|
||||
}
|
||||
case 'max': {
|
||||
field.setAttribute('maxlength', rule['args']['limit']);
|
||||
break;
|
||||
}
|
||||
case 'pattern': {
|
||||
field.setAttribute('pattern', rule['args']['regex'].substring(1, rule['args']['regex'].length - 1));
|
||||
//field.setAttribute("pattern", "^[\\+]?[\\(]?[0-9]{3}[\\)]?[\\-\\s\\.]?[0-9]{3}[\\-\\s\\.]?[0-9]{4,9}$");
|
||||
break;
|
||||
}
|
||||
case 'type': {
|
||||
//field.setAttribute("type", rule["args"]["type"]);
|
||||
console.log('Type: ', rule['args']['type']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (flags) {
|
||||
flags['presence'] == 'required' ? field.setAttribute('required', true) : field.removeAttribute('required');
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('Keys: ', keys);
|
||||
});
|
||||
|
||||
modalForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
// Check what button submitted the form and if it has data-actionBtn = save
|
||||
// If not, close modal
|
||||
const pressedBtn = event.submitter;
|
||||
if (pressedBtn.getAttribute('data-actionBtn') != 'save') {
|
||||
modalForm.closest('.modal').classList.remove('is-active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find .entryPhase and hide it
|
||||
const entryPhase = modalForm.querySelector('.entryPhase');
|
||||
const loadPhase = modalForm.querySelector('.loadPhase');
|
||||
if (entryPhase) {
|
||||
entryPhase.classList.add('is-hidden');
|
||||
}
|
||||
if (loadPhase) {
|
||||
loadPhase.classList.remove('is-hidden');
|
||||
}
|
||||
console.log('Form submitted: ', modalForm);
|
||||
const table = modalForm.getAttribute('data-targetTable');
|
||||
const data = new FormData(modalForm);
|
||||
// Convert to JSON object
|
||||
let jsonData = {};
|
||||
data.forEach((value, key) => {
|
||||
jsonData[key] = value;
|
||||
});
|
||||
console.log('JSON Data: ', jsonData);
|
||||
let resp = {};
|
||||
if(modalForm.getAttribute('data-action') == 'edit') {
|
||||
Rid = modalForm.getAttribute('data-rid');
|
||||
resp = await updateRow(table, Rid,jsonData);
|
||||
modalForm.setAttribute('data-action', 'create');
|
||||
} else {
|
||||
resp = await createEntry(table, jsonData);
|
||||
}
|
||||
|
||||
console.log('Response: ', resp);
|
||||
if (resp['status'] == 'CREATED' || resp['status'] == 'UPDATED') {
|
||||
console.log('Entry created successfully');
|
||||
modalForm.closest('.modal').classList.remove('is-active');
|
||||
modalForm.reset();
|
||||
// Hide loadPhase
|
||||
if (loadPhase) {
|
||||
loadPhase.classList.add('is-hidden');
|
||||
}
|
||||
// Show entryPhase
|
||||
if (entryPhase) {
|
||||
entryPhase.classList.remove('is-hidden');
|
||||
}
|
||||
} else {
|
||||
// Hide loadPhase
|
||||
if (loadPhase) {
|
||||
loadPhase.classList.add('is-hidden');
|
||||
}
|
||||
// Show entryPhase
|
||||
if (entryPhase) {
|
||||
entryPhase.classList.remove('is-hidden');
|
||||
}
|
||||
// TODO: Show error message
|
||||
}
|
||||
|
||||
// Find all tables with data-searchTargetId set to table
|
||||
setTimeout(() => {
|
||||
refreshTableByName(table);
|
||||
updateSingeltonsByTableName(table);
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper
|
||||
async function refreshTable(table) {
|
||||
// Refresh a table while keeping (optionally set) search value
|
||||
const searchField = document.querySelector("input[data-searchTargetId='" + table.id + "']");
|
||||
// Get state of order and sort
|
||||
const ths = table.querySelectorAll('th');
|
||||
const columnIndices = [];
|
||||
ths.forEach((th, index) => {
|
||||
columnIndices[th.getAttribute('data-dataCol')] = index;
|
||||
});
|
||||
let order = '';
|
||||
let column = '';
|
||||
ths.forEach((th) => {
|
||||
if (th.hasAttribute('data-order')) {
|
||||
order = th.getAttribute('data-order');
|
||||
column = th.getAttribute('data-dataCol');
|
||||
}
|
||||
});
|
||||
console.log('Order: ', order, ' Column: ', column);
|
||||
|
||||
const maxLinesPerPage = table.getAttribute('data-pageSize');
|
||||
let currentPage = table.getAttribute('data-currentPage');
|
||||
if(currentPage == null) {
|
||||
table.setAttribute('data-currentPage', 1);
|
||||
currentPage = 1;
|
||||
}
|
||||
|
||||
const start = (currentPage - 1) * maxLinesPerPage;
|
||||
const end = start + maxLinesPerPage;
|
||||
|
||||
let paginationPassOnPre = {
|
||||
'start': start,
|
||||
'end': end,
|
||||
'currentPage': currentPage,
|
||||
'maxLinesPerPage': maxLinesPerPage
|
||||
};
|
||||
|
||||
if (searchField) {
|
||||
const value = searchField.value;
|
||||
const dbTable = table.getAttribute('data-dataSource');
|
||||
const result = await returnTableDataByTableName(dbTable, value, order, column, take= maxLinesPerPage, skip= start);
|
||||
const totalResultCount = await getCountByTable(dbTable, value);
|
||||
paginationPassOnPre['dataLength'] = totalResultCount;
|
||||
var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
|
||||
var data = magMiddl[0];
|
||||
var paginationPassOn = magMiddl[1];
|
||||
clearTable(table);
|
||||
writeDataToTable(table, data, paginationPassOn);
|
||||
} else {
|
||||
const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'), undefined, order, column, take= maxLinesPerPage, skip= start);
|
||||
const resultCount = await getCountByTable(table.getAttribute('data-dataSource'));
|
||||
paginationPassOnPre['dataLength'] = resultCount;
|
||||
var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
|
||||
var data = magMiddl[0];
|
||||
var paginationPassOn = magMiddl[1];
|
||||
clearTable(table);
|
||||
writeDataToTable(table, data, paginationPassOn);
|
||||
}
|
||||
}
|
||||
|
||||
function managePaginationMiddleware(data, paginationPassOnPre) {
|
||||
const maxLinesPerPage = paginationPassOnPre['maxLinesPerPage'];
|
||||
|
||||
const dataLength = paginationPassOnPre['dataLength'];
|
||||
const maxPages = Math.ceil(dataLength / maxLinesPerPage);
|
||||
paginationPassOn = paginationPassOnPre;
|
||||
paginationPassOn['maxPages'] = maxPages;
|
||||
// paginationPassOn['dataLength'] = dataLength;
|
||||
return [data, paginationPassOn];
|
||||
}
|
||||
|
||||
async function refreshTableByName(name) {
|
||||
const dirtyTables = document.querySelectorAll("table[data-dataSource='" + name + "']");
|
||||
for (dirty of dirtyTables) {
|
||||
refreshTable(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSingeltonsByTableName(name) {
|
||||
const dirtySingles = document.querySelectorAll("span[data-dataSource='" + name + "']");
|
||||
for (dirty of dirtySingles) {
|
||||
writeSingelton(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
function clearTable(table) {
|
||||
const tbody = table.querySelector('tbody');
|
||||
tbody.innerHTML = '';
|
||||
}
|
||||
|
||||
function writeDataToTable(table, data, paginationPassOn) {
|
||||
if(data == undefined || data == null || data.length == 0) {
|
||||
return;
|
||||
}
|
||||
console.log('Writing data to table: ', table, data);
|
||||
// Get THEAD and TBODY elements
|
||||
const thead = table.querySelector('thead');
|
||||
const tbody = table.querySelector('tbody');
|
||||
|
||||
// get index per column
|
||||
const columns = thead.querySelectorAll('th');
|
||||
const columnIndices = [];
|
||||
columns.forEach((column, index) => {
|
||||
columnIndices[column.getAttribute('data-dataCol')] = index;
|
||||
});
|
||||
|
||||
// All required cols
|
||||
let requiredCols = [];
|
||||
let actionFields = [];
|
||||
columns.forEach((column) => {
|
||||
// console.log('Column: ', column, ' FNC: ', column.getAttribute('data-fnc'), column.attributes);
|
||||
if(column.getAttribute('data-fnc') == "actions") {
|
||||
console.log('!!! Found actions column !!!');
|
||||
actionFields.push(column);
|
||||
return;
|
||||
}
|
||||
requiredCols.push(column.getAttribute('data-dataCol'));
|
||||
});
|
||||
|
||||
|
||||
// Get paginationPassOn
|
||||
const start = paginationPassOn['start'];
|
||||
const end = paginationPassOn['end'];
|
||||
const currentPage = paginationPassOn['currentPage'];
|
||||
const maxLinesPerPage = paginationPassOn['maxLinesPerPage'];
|
||||
const maxPages = paginationPassOn['maxPages'];
|
||||
const dataLength = paginationPassOn['dataLength'];
|
||||
|
||||
|
||||
// Find nav with class pagination and data-targetTable="table.id"
|
||||
const paginationElement = document.querySelector("nav.pagination[data-targetTable='" + table.id + "']");
|
||||
const paginationList = paginationElement.querySelector('ul.pagination-list');
|
||||
console.log('Data length: ', dataLength, ' Max pages: ', maxPages);
|
||||
|
||||
if(maxPages > 1) {
|
||||
// Clear pagination list
|
||||
paginationList.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= maxPages; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = '<a class="pagination-link" aria-label="Goto page ' + i + '" data-page="' + i + '">' + i + '</a>';
|
||||
if(i == currentPage) {
|
||||
li.querySelector('a').classList.add('is-current');
|
||||
}
|
||||
paginationList.appendChild(li);
|
||||
}
|
||||
|
||||
|
||||
// Remove unused pages, only leave first, last, current and 2 neighbors
|
||||
let pages = paginationList.querySelectorAll('li');
|
||||
let friends = []
|
||||
// Always add first and last
|
||||
friends.push(0);
|
||||
friends.push(pages.length - 1);
|
||||
friends.push(currentPage-1);
|
||||
|
||||
// Add direct neighbors
|
||||
// friends.push(currentPage - 2);
|
||||
friends.push(currentPage);
|
||||
friends.push(currentPage - 2);
|
||||
|
||||
|
||||
// Deduplicate friends
|
||||
friends = [...new Set(friends)];
|
||||
// Sort friends
|
||||
friends.sort((a, b) => a - b);
|
||||
// Parse friends (string to int)
|
||||
friends = friends.map((x) => parseInt(x));
|
||||
|
||||
console.log('Friends: ', friends, ' Pages: ', pages.length, ' Current: ', currentPage);
|
||||
|
||||
|
||||
// Remove everyone who is not a friend
|
||||
for(let i = 0; i < pages.length; i++) {
|
||||
if(friends.includes(i)) {
|
||||
continue;
|
||||
}
|
||||
pages[i].remove();
|
||||
}
|
||||
|
||||
// Find all gaps (step size bigger then 1) and add an ellipsis in between the two numbers
|
||||
let last = 0;
|
||||
for(let i = 0; i < friends.length; i++) {
|
||||
if(friends[i] - last > 1) {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = '<span class="pagination-ellipsis">…</span>';
|
||||
paginationList.insertBefore(li, pages[friends[i]]);
|
||||
}
|
||||
last = friends[i];
|
||||
}
|
||||
|
||||
|
||||
// Append on click event to all pagination links
|
||||
paginationList.querySelectorAll('a').forEach((link) => {
|
||||
link.addEventListener('click', async function() {
|
||||
const page = link.getAttribute('data-page');
|
||||
table.setAttribute('data-currentPage', page);
|
||||
refreshTable(table);
|
||||
});
|
||||
});
|
||||
|
||||
paginationElement.classList.remove('is-hidden');
|
||||
} else {
|
||||
paginationElement.classList.add('is-hidden');
|
||||
}
|
||||
|
||||
|
||||
for (resultIndex in data) {
|
||||
const row = data[resultIndex];
|
||||
const tr = document.createElement('tr');
|
||||
requiredCols.forEach((column) => {
|
||||
const td = document.createElement('td');
|
||||
td.innerText = row[column];
|
||||
tr.appendChild(td);
|
||||
});
|
||||
|
||||
// Add action fields
|
||||
actionFields.forEach((actionField) => {
|
||||
const td = document.createElement('td');
|
||||
const actions = actionField.getAttribute('data-actions').split(',');
|
||||
actions.forEach((action) => {
|
||||
const button = document.createElement('button');
|
||||
let icon = '';
|
||||
let color = 'is-primary';
|
||||
switch(action) {
|
||||
case 'edit': {
|
||||
icon = '<i class="bi bi-pencil"></i>';
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
icon = '<i class="bi bi-trash"></i>';
|
||||
color = 'is-danger';
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Add classes
|
||||
button.classList.add('button');
|
||||
button.classList.add('is-small');
|
||||
button.classList.add(color);
|
||||
button.classList.add('is-outlined');
|
||||
button.innerHTML = ` <span class="icon is-small">${icon}</span> `;
|
||||
button.style.marginRight = '5px';
|
||||
|
||||
// Add data-action and data-id
|
||||
button.setAttribute('data-action', action);
|
||||
button.setAttribute("data-id", row["id"]);
|
||||
|
||||
// Add event listener
|
||||
button.addEventListener('click', async function() {
|
||||
const table = actionField.closest('table');
|
||||
const row = button.closest('tr');
|
||||
const columns = table.querySelectorAll('th');
|
||||
const columnIndices = [];
|
||||
columns.forEach((column, index) => {
|
||||
columnIndices[column.getAttribute('data-dataCol')] = index;
|
||||
});
|
||||
const data = [];
|
||||
columns.forEach((column) => {
|
||||
data[column.getAttribute('data-dataCol')] = row.children[columnIndices[column.getAttribute('data-dataCol')]].innerText;
|
||||
});
|
||||
console.log('Data: ', data);
|
||||
switch(action) {
|
||||
case 'edit': {
|
||||
// Open modal with form
|
||||
const form = document.querySelector("form[data-targetTable='" + table.getAttribute('data-dataSource') + "']");
|
||||
const formTitle = form.querySelector('.title');
|
||||
const entryPhase = form.querySelector('.entryPhase');
|
||||
const loadPhase = form.querySelector('.loadPhase');
|
||||
const fields = form.querySelectorAll('input');
|
||||
// Set modal to edit mode
|
||||
form.setAttribute('data-action', 'edit');
|
||||
form.setAttribute('data-rid', button.getAttribute('data-id'));
|
||||
formTitle.innerText = 'Edit entry';
|
||||
fields.forEach((field) => {
|
||||
// Skip for submit button
|
||||
if(field.getAttribute('type') == 'submit') {
|
||||
return;
|
||||
}
|
||||
field.value = data[field.getAttribute('name')];
|
||||
});
|
||||
form.closest('.modal').classList.add('is-active');
|
||||
// TBD
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
// confirm
|
||||
const confirm = window.confirm('Do you really want to delete this entry?');
|
||||
// Delete entry
|
||||
if(confirm) {
|
||||
const table = actionField.closest('table');
|
||||
const id = button.getAttribute('data-id');
|
||||
const resp = await deleteRow(table.getAttribute('data-dataSource'), id);
|
||||
if(resp['status'] == 'DELETED') {
|
||||
refreshTable(table);
|
||||
updateSingeltonsByTableName(table.getAttribute('data-dataSource'));
|
||||
} else {
|
||||
// Show error message
|
||||
// TODO: Show error message
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
td.appendChild(button);
|
||||
});
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle modal
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Functions to open and close a modal
|
||||
function openModal($el) {
|
||||
$el.classList.add('is-active');
|
||||
}
|
||||
|
||||
function closeModal($el) {
|
||||
$el.classList.remove('is-active');
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
|
||||
closeModal($modal);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a click event on buttons to open a specific modal
|
||||
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
|
||||
const modal = $trigger.dataset.target;
|
||||
const $target = document.getElementById(modal);
|
||||
|
||||
$trigger.addEventListener('click', () => {
|
||||
openModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a click event on various child elements to close the parent modal
|
||||
(document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
|
||||
const $target = $close.closest('.modal');
|
||||
|
||||
$close.addEventListener('click', () => {
|
||||
closeModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a keyboard event to close all modals
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
});
|
1
static/placeholder.json
Normal file
1
static/placeholder.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
1
static/test.js
Normal file
1
static/test.js
Normal file
@ -0,0 +1 @@
|
||||
console.log('test.js');
|
Reference in New Issue
Block a user