initial commit

This commit is contained in:
jomo 2022-08-04 05:41:51 +02:00
commit 0961d0e47d
15 changed files with 564 additions and 0 deletions

44
static/form.html Normal file
View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>in?</title>
<link rel="stylesheet" href="style.css">
<script src="form.js"></script>
</head>
<body>
<h1><a href="../">in?</a></h1>
<form id="form">
<label for="id">ID</label>
<input id="id" type="text" placeholder="loading...">
<label for="is_in">Is in</label>
<select id="is_in">
<option value=""></option>
</select>
<label for="coords_bl">Bottom left coordinates</label>
<input id="coords_bl" type="text">
<label for="coords_tr">Top right coordinates</label>
<input id="coords_tr" type="text">
<label for="type">Type</label>
<input id="type" type="text">
<label for="name">Name / Label</label>
<input id="name" type="text">
<label for="content">Content</label>
<textarea id="content"></textarea>
<label for="note">Note</label>
<textarea id="note"></textarea>
<label for="hidden">Hide by default</label>
<input type="checkbox" id="hidden">
</form>
<button id="delete" class="btn red" autocomplete="off">Delete</button>
<button id="save" class="btn green" autocomplete="off">Save</button>
</body>
</html>

103
static/form.js Normal file
View file

@ -0,0 +1,103 @@
let items = {};
const id = new URLSearchParams(window.location.search).get('id');
document.onreadystatechange = function() {
if (document.readyState === 'interactive') {
fetch('/api/items').then(r => r.json()).then((data) => {
items = data;
fillForm();
});
}
}
function fillForm() {
const item = items[id];
const is_in = document.getElementById('is_in');
for (const [_, item] of Object.entries(items)) {
const option = document.createElement('option');
option.value = item.id;
option.textContent = `${item.id} ${item.name && `(${item.name})` || ''}`
is_in.appendChild(option);
};
document.getElementById('id').placeholder = '';
if (id && item) {
document.getElementById('id').value = item.id;
document.getElementById('is_in').value = item.is_in;
document.getElementById('coords_bl').value = item.coords_bl;
document.getElementById('coords_tr').value = item.coords_tr;
document.getElementById('type').value = item.type;
document.getElementById('name').value = item.name;
document.getElementById('content').value = item.content;
document.getElementById('note').value = item.note;
document.getElementById('hidden').checked = item.hidden;
}
const saveBtn = document.getElementById('save');
const delBtn = document.getElementById('delete');
saveBtn.onclick = function(e) {
e.preventDefault();
save();
};
delBtn.onclick = function(e) {
e.preventDefault();
del();
}
}
function save() {
item = {
id: document.getElementById('id').value || null,
is_in: document.getElementById('is_in').value || null,
coords_bl: document.getElementById('coords_bl').value || null,
coords_tr: document.getElementById('coords_tr').value || null,
type: document.getElementById('type').value || null,
name: document.getElementById('name').value || null,
content: document.getElementById('content').value || null,
note: document.getElementById('note').value || null,
hidden: document.getElementById('hidden').checked,
}
if (items[item.id]) {
if (!id || (id && id != item.id)) {
alert(`ID ${item.id} exists. Please chose a different ID or edit the existing ${item.id} item!`);
return;
}
}
const original_id = id || item.id;
fetch(`/api/items/${original_id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(item),
})
.then(r => {
if (r.ok) {
window.location.href = '../';
} else {
r.text().then(t => alert(`Error:\n${t}`));
}
}).catch(error => {
alert(`Error:\n${error}`);
});
}
function del() {
if (id && confirm('Are you sure?')) {
fetch(`/api/items/${id}`, {method: 'DELETE'}).then(r => {
if (r.ok) {
window.location.href = '../';
} else {
r.text().then(t => alert(`Error:\n${t}`));
}
}).catch(error => {
alert(`Error:\n${error}`);
});
}
}

30
static/index.html Normal file
View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>in?</title>
<link rel="stylesheet" href="style.css">
<script src="index.js"></script>
</head>
<body>
<h1>in?</h1>
<input type="text" id="search" value="" autocomplete="off" placeholder="RegEx search" tabindex="1" autofocus>
<label>Show hidden items</label> <input type="checkbox" id="showhidden">
<div id="results">
<span id="loading">loading…</span>
<template id="item">
<div id="" class="result" tabindex="0">
<h2>
<span class="id"></span>
<span class="name"></span>
<a title="edit"></a>
<span class="loc"></span>
</h2>
<div class="type"></div>
<div class="note"></div>
<div class="content"></div>
</div>
</template>
</div>
<a href="form.html" class="btn green">+</a>
</body>
</html>

92
static/index.js Normal file
View file

@ -0,0 +1,92 @@
let items = {};
document.onreadystatechange = function() {
if (document.readyState === 'interactive') {
document.getElementById('search').oninput = search;
const checkbox = document.getElementById('showhidden')
checkbox.oninput = showhidden;
fetch('/api/items').then((response) => response.json()).then((data) => {
items = data;
renderItems();
showhidden({target: checkbox});
});
}
}
function renderItems() {
const container = document.getElementById('results');
const template = document.getElementById('item');
const loading = document.getElementById('loading');
for (const [id, item] of Object.entries(items)) {
const clone = template.content.cloneNode(true);
const [loc, longloc] = getLocString(items, item);
clone.querySelector(".loc").textContent = loc;
clone.querySelector(".loc").title = longloc;
clone.querySelector(".type").textContent = item.type;
clone.querySelector(".id").textContent = item.id;
clone.querySelector(".name").textContent = item.name;
clone.querySelector(".content").textContent = item.content;
clone.querySelector(".note").textContent = item.note;
clone.querySelector(".result").id = id;
clone.querySelector("a").href = `form.html?id=${id}`;
if (item.hidden) {
clone.querySelector(".result").classList.add('hidden')
};
container.appendChild(clone);
};
loading.remove();
}
function getLocString(items, item) {
if (item.is_in == item.id) {
return ['in ↻', `${item.id} is in itself`];
}
let ancestors = [];
let next = item;
while (next) {
ancestors.unshift(next);
next = items[next.is_in];
}
ancestors.pop();
const loc = ancestors.map(i => i.id).join(' ➜ ') || '⬚';
let longloc = ancestors.map(i => `${i.type || ''} ${i.id} ${i.name && `(${i.name})` || ''}`).join(' ➜ ') || 'universe';
return [loc, `${item.id} is in ${longloc}`];
}
function search(e) {
const searchAttrs = ['id', 'name', 'type', 'note', 'content'];
const query = e.target.value;
const regex = new RegExp(query, 'i')
for (const elem of document.getElementsByClassName('result')) {
const item = items[elem.id];
let found = false;
if (query) {
for (const a in searchAttrs) {
const attr = item[searchAttrs[a]];
if (attr && attr.match(regex)) {
found = true;
break;
}
}
} else {
found = true;
}
if (found) {
elem.classList.remove('filtered');
} else {
elem.classList.add('filtered');
}
}
}
function showhidden(e){
const container = document.getElementById('results');
if (e.target.checked) {
results.classList.add('showhidden');
} else {
results.classList.remove('showhidden');
}
}

136
static/style.css Normal file
View file

@ -0,0 +1,136 @@
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
max-width: 600px;
margin: auto;
padding-bottom: 1em;
background: #fefefe;
}
h1, #search {
text-align: center;
width: 100%;
font-weight: bold;
}
h1 a {
text-decoration: none;
color: initial;
}
#results {
margin-top: 1em;
}
.result {
background: #eee;
padding: 1em;
margin: 1em 0;
}
.result a {
text-decoration: none;
color: #03d;
}
.result.hidden {
display: none;
}
#results.showhidden .result.hidden {
display: block;
}
.result.filtered {
display: none;
}
.loc {
float: right;
font-weight: normal;
font-size: 0.8em;
}
.type {
font-size: 0.8em;
}
h2 {
margin: 0;
font-size: 20px;
}
.name {
font-weight: normal;
}
.content {
margin-top: 1em;
}
.note:empty {
display: none;
}
.note {
background: #aef;
padding: 1em;
margin-top: 1em;
}
.btn {
display: block;
width: 100%;
border: 1px solid;
padding: 0.5em;
text-decoration: initial;
color: initial;
font-weight: bold;
text-align: center;
font-size: 1.5em;
margin-bottom: 0.5em;
cursor: pointer;
}
.btn.green {
background: #0a0;
color: white;
}
.btn.red {
background: #a00;
color: white;
}
form label {
display: block;
width: 100%;
font-weight: bold;
margin-bottom: 0.3em;
}
input, textarea, select {
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
margin: 0 0 1em 0;
padding: 0.3em;
font-family: monospace;
font-size: 1.4em;
background: initial;
border: 1px solid gray;
}
input[type=checkbox] {
width: initial;
display: initial;
min-width: initial;
}
textarea {
min-height: 8em;
}