mirror of
https://github.com/slynn1324/tinypin.git
synced 2026-01-23 18:35:59 +00:00
382 lines
13 KiB
HTML
382 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.user", async (data) => {
|
|
store.do("loader.show");
|
|
|
|
let res = await fetch("/api/whoami");
|
|
|
|
if ( res.status == 200 ){
|
|
data.user = await res.json();
|
|
|
|
window.csrfToken = data.user.csrf;
|
|
} else {
|
|
console.log("error getting user");
|
|
}
|
|
|
|
|
|
store.do("loader.hide");
|
|
});
|
|
|
|
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', 'x-csrf-token': window.csrfToken },
|
|
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",
|
|
'x-csrf-token': window.csrfToken
|
|
},
|
|
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.user");
|
|
|
|
store.do('load.boards');
|
|
|
|
Reef.databind(appComponent);
|
|
appComponent.render();
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
</html>
|