tinypin/client/addpin.html

362 lines
13 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>tinypin</title>
<meta charset="utf-8">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="apple-touch-icon" sizes="180x180" href="pub/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="pub/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="pub/icons/favicon-16x16.png">
<link rel="manifest" href="pub/icons/site.webmanifest">
<link rel="mask-icon" href="pub/icons/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="pub/icons/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="pub/icons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="pub/bulma-custom.css" />
<link rel="stylesheet" href="client.css" />
</head>
<body>
<div id="ilen"></div>
<div id="app"></div>
<script src="reef.min.js"></script>
<script src="reef-bootstrap.js"></script>
<script src="reef-databind.js"></script>
<script>
document.getElementById("ilen").innerHTML = window.location.hash.length;
Reef.debug(true);
app.addSetter("render", (data) => {
appComponent.render();
});
app.addSetter("loader.show", (data) => {
data.loading++;
});
app.addSetter("loader.hide", (data) => {
data.loading--;
});
app.addSetter("hash.parse", (data) => {
let qs = window.location.hash.substr(1);
let obj = {};
let parts = qs.split("&");
for ( let i = 0; i < parts.length; ++i ){
let kv = parts[i].split("=");
if ( kv.length == 2 ){
let key = decodeURIComponent(kv[0]);
let value = decodeURIComponent(kv[1]);
obj[key] = value;
}
}
data.hash = obj;
if ( data.hash.i ){
data.addPinModal.imageUrl = data.hash.i;
data.addPinModal.previewImageUrl = data.hash.i;
data.addPinModal.siteUrl = data.hash.s;
}
});
app.addSetter("load.boards", async (data) => {
store.do("loader.show");
let res = await fetch("/api/boards");
data.boards = await res.json();
data.initialized = true;
if ( window.localStorage.addPinLastBoardId ){
data.addPinModal.boardId = window.localStorage.addPinLastBoardId;
} else if ( data.boards && data.boards.length > 0 ){
data.addPinModal.boardId = data.boards[0].id;
} else {
data.addPinModal.boardId = "new";
}
store.do("loader.hide");
});
app.addSetter('addPinModal.updatePreview', (data) => {
if ( data.addPinModal.imageUrl.startsWith("http") ){
( async() => {
let res = await fetch(data.addPinModal.imageUrl, {
mode: 'no-cors',
method: "HEAD"
});
if ( res.status = 200 ){
data.addPinModal.previewImageUrl = data.addPinModal.imageUrl;
}
store.do("render");
})();
} else {
data.addPinModal.previewImageUrl = null;
}
});
app.addGetter('addPinModal.isValid', (data) => {
if ( data.addPinModal.boardId == "new"){
if ( !data.addPinModal.newBoardName ){
return false;
} else if ( data.addPinModal.newBoardName.trim().length < 1 ){
return false;
}
}
if ( !data.addPinModal.previewImageUrl ){
return false;
}
return true;
});
app.addSetter('addPinModal.save', async (data) => {
data.addPinModal.saveInProgress = true;
let boardId = data.addPinModal.boardId;
let newBoard = null;
if ( boardId == "new" ){
let res = await fetch('api/boards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"name": data.addPinModal.newBoardName
})
});
if ( res.status == 200 ){
newBoard = await res.json();
boardId = newBoard.id;
data.boards.push(newBoard);
}
}
let postData = {
boardId: boardId,
imageUrl: data.addPinModal.imageUrl,
siteUrl: data.addPinModal.siteUrl,
description: data.addPinModal.description
};
let res = await fetch('api/pins', {
method: 'POST',
headers: {
'Content-Type': "application/json"
},
body: JSON.stringify(postData)
});
if ( res.status == 200 ){
window.localStorage.addPinLastBoardId = boardId;
data.done = true;
window.close();
}
store.do("render");
});
const store = new Reef.Store({
data: {
loading: 0,
hash: null,
boards: [],
addPinModal: {
pinId: null,
active: false,
boardId: "",
newBoardName: null,
imageUrl: "",
previewImageUrl: null,
siteUrl: "",
description: "",
saveInProgress: false
},
initialized: false,
done: false
},
getters: app.getGetters(),
setters: app.getSetters()
});
app.freeze();
const appComponent = new Reef("#app", {
store: store,
template: (data) => {
if ( !data.initialized ){
return `<div style="margin-top: 20px; margin-left: auto; margin-right: auto;">
<div id="loader" class="button is-text is-loading"></div>
</div>`;
}
//
let imagePlaceholder = 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22300%22%20height%3D%22300%22%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%22300%22%20height%3D%22300%22%20style%3D%22fill%3A%23dedede%3B%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20font-size%3D%2218%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20font-family%3D%22monospace%2C%20sans-serif%22%20fill%3D%22%23555555%22%3Eimage%3C%2Ftext%3E%3C%2Fsvg%3E';
let options = "";
for ( let i = 0; i < data.boards.length; ++i ){
options += `<option value="${data.boards[i].id}">${data.boards[i].name}</option>`;
}
let newBoardField = '';
if ( data.addPinModal.boardId == "new" ){
newBoardField = /*html*/`
<div class="field">
<label class="label">Board Name</label>
<div class="control">
<input class="input" type="text" data-bind="addPinModal.newBoardName" />
</div>
</div>
`;
}
return /*html*/`
<div id="doneModal">
<div class="modal ${data.done ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box has-text-centered" style="max-width: 90%; margin-left: auto; margin-right: auto;">
<!-- https://thenounproject.com/search/?q=done&i=587164 -->
<img style="width: 80px; height: 80px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjNGU4ODJiIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PGc+PHBhdGggZmlsbD0iIzRlODgyYiIgZD0iTTY5LjQzNyw0My4zMDNsLTI0LjEyLDI1Ljc1M2MtMC44MTcsMC44NzEtMS44NTEsMS4zMDctMi45OTUsMS4zMDdzLTIuMjMyLTAuNDM2LTIuOTk1LTEuMzA3ICAgIEwyNy4yNDEsNTYuMTUyYy0xLjUyNC0xLjYzMy0xLjQ3LTQuMTkyLDAuMjE4LTUuNzcxYzEuNjMzLTEuNTI1LDQuMTkyLTEuNDE2LDUuNzcxLDAuMjE4bDkuMDkzLDkuNjkxbDIxLjE4LTIyLjU5NSAgICBjMS41MjQtMS42MzMsNC4xMzgtMS43NDIsNS43NzEtMC4xNjNDNzAuOTA3LDM5LjA1Niw3MS4wMTYsNDEuNjE1LDY5LjQzNyw0My4zMDN6Ij48L3BhdGg+PHBhdGggZmlsbD0iIzRlODgyYiIgZD0iTTUuMDgyLDUwQzUuMDgyLDI1LjIyNywyNS4xNzIsNS4xMzYsNTAsNS4wODJDNzQuNzczLDUuMTM2LDk0LjkxOCwyNS4yMjcsOTQuOTE4LDUwICAgIGMwLDI0LjgyOC0yMC4xNDUsNDQuOTE4LTQ0LjkxOCw0NC45MThDMjUuMTcyLDk0LjkxOCw1LjA4Miw3NC44MjgsNS4wODIsNTB6IE0yMy45NzUsNzYuMDI1ICAgIEMzMC42NzIsODIuNjY4LDM5LjgxOSw4Ni43NTEsNTAsODYuNzUxYzEwLjEyNywwLDE5LjMyOC00LjA4MywyNS45NzEtMTAuNzI2YzYuNjQyLTYuNjk3LDEwLjc4LTE1Ljg0NCwxMC43OC0yNi4wMjUgICAgYzAtMTAuMTI3LTQuMTM4LTE5LjI3NC0xMC43OC0yNS45NzFDNjkuMzI4LDE3LjM4Nyw2MC4xMjcsMTMuMjQ5LDUwLDEzLjI0OWMtMTAuMTgxLDAtMTkuMzI4LDQuMTM4LTI2LjAyNSwxMC43OCAgICBDMTcuMzMyLDMwLjcyNiwxMy4yNDksMzkuODczLDEzLjI0OSw1MEMxMy4yNDksNjAuMTgxLDE3LjMzMiw2OS4zMjgsMjMuOTc1LDc2LjAyNXoiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPg==" />
</div>
</div>
</div>
</div>
</div>
<div id="addPinModal">
<div class="modal ${!data.done ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Pin</p>
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
</header>
<section class="modal-card-body">
<div class="add-pin-flex">
<div class="add-pin-flex-left">
<img id="add-pin-modal-img" src="${data.addPinModal.previewImageUrl ? data.addPinModal.previewImageUrl : imagePlaceholder}" />
</div>
<div class="add-pin-flex-right">
<form>
<div class="field">
<label class="label">Board</label>
<div class="select">
<select data-bind="addPinModal.boardId">
${options}
<option value="new">Create New Board</option>
</select>
</div>
</div>
${newBoardField}
<div class="field">
<label class="label">Image Url</label>
<div class="control">
<input class="input" type="text" data-bind="addPinModal.imageUrl" data-onblur="addPinModal.updatePreview" ${data.hash.i ? 'disabled' : ''}/>
</div>
</div>
<div class="field">
<label class="label">Website Url</label>
<div class="control">
<input class="input" type="text" data-bind="addPinModal.siteUrl" />
</div>
</div>
<div class="field">
<label class="label">Description</label>
<div class="control">
<textarea class="textarea" data-bind="addPinModal.description"></textarea>
</div>
</div>
</form>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success ${data.addPinModal.saveInProgress ? 'is-loading' : ''}" ${!store.get('addPinModal.isValid') || data.addPinModal.saveInProgress ? 'disabled' : ''} data-onclick="addPinModal.save">Add Pin</button>
<button class="button is-light" ${data.addPinModal.saveInProgress ? 'disabled' : '' } onclick="window.close()">Cancel</button>
</footer>
</div>
</div></div>`;
}
});
// attach all the child components
for (const [name, f] of Object.entries(app.getComponents())) {
let c = f(store);
if ( !c ){
throw(new Error(`component ${name} did not return a Reef component`));
} else {
appComponent.attach(c);
}
}
document.addEventListener('click', (el) => {
let target = el.target.closest('[data-onclick]');
if (target) {
let action = target.getAttribute('data-onclick');
if (action) {
try{
store.do(action, target);
} catch (err){
console.error(`Error invoking ${action}:`, err);
}
}
}
});
// focusout bubbles while 'blur' does not.
document.addEventListener('focusout', (el) => {
let target = el.target.closest('[data-onblur]');
if ( target ){
let method = target.getAttribute('data-onblur');
if ( method ) {
store.do(method, target);
}
}
});
store.do('hash.parse');
store.do('load.boards');
Reef.databind(appComponent);
appComponent.render();
</script>
</body>
</html>