breakout into components, bootstrapping

This commit is contained in:
slynn1324 2021-01-22 15:38:06 -06:00
parent 39ef8c9742
commit 94a562fdf5
14 changed files with 1214 additions and 799 deletions

View file

@ -26,8 +26,9 @@ const SERVER_ERROR = {status: "error", error: "server error"};
initDb();
// list boards
app.get("/api/boards", (req, res) => {
app.get("/api/boards", async (req, res) => {
try{
await sleep(1000);
let boards = db.prepare("SELECT * FROM boards").all();
for( let i = 0; i < boards.length; ++i ){

192
static/app.js Normal file
View file

@ -0,0 +1,192 @@
Reef.debug(true);
// force a re-render
app.addSetter("render", (data) => {
appComponent.render();
});
app.addSetter("loader.show", (data) => {
data.loading++;
});
app.addSetter("loader.hide", (data) => {
data.loading--;
});
app.addSetter("load.boards", async (data) => {
store.do("loader.show");
let res = await fetch("/api/boards");
data.boards = await res.json();
store.do("loader.hide");
});
app.addSetter('load.board', async (data) => {
store.do("loader.show");
if ( !data.board || data.board.id != data.hash.board ){
let res = await fetch("/api/boards/" + data.hash.board);
data.board = await res.json();
}
store.do("loader.hide");
});
app.addSetter("hash.update", (data) => {
console.log("hash update");
data.hash = parseQueryString(window.location.hash.substr(1));
if ( data.hash.board ){
store.do('load.board');
} else {
data.board = null;
data.pinZoomModal.active = false;
data.addPinModal.active = false;
data.aboutModal.active = false;
}
});
let store = new Reef.Store({
data: {
hash: {
board: null
},
loading: 0,
boards: [],
board: null,
addPinModal: {
active: false,
boardId: "",
newBoardName: null,
imageUrl: "",
previewImageUrl: null,
siteUrl: "",
description: "",
saveInProgress: false
},
pinZoomModal: {
active: false,
pin: null,
fullDescriptionOpen: false
},
aboutModal: {
active: false
},
editBoardModal: {
active: false,
name: ""
}
},
getters: app.getGetters(),
setters: app.getSetters()
});
app.freeze();
// init the app component
const appComponent = new Reef("#app", {
store: store,
template: (data) => {
return /*html*/`
<div id="navbar"></div>
<section class="section">
<div class="container" id="brickwall-container">
<div id="brickwall" class="brickwall"></div>
</div>
</section>
<footer class="footer" id="footer">
<div class="content has-text-right">
<a data-onclick="aboutModal.open">about tinypin</a>
</div>
</footer>
<div id="addPinModal"></div>
<div id="pinZoomModal"></div>
<div id="editBoardModal"></div>
<div id="aboutModal"></div>
`
//<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></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);
}
}
});
document.addEventListener('keyup', (el) => {
if ( store.data.pinZoomModal.active ){
if ( el.key == "Escape" ){
store.do('pinZoomModal.close');
} else if ( el.key == "ArrowLeft" ){
store.do('pinZoomModal.moveLeft');
} else if ( el.key == "ArrowRight" ){
store.do('pinZoomModal.moveRight');
}
}
if ( store.data.addPinModal.active ){
if ( el.key == "Escape" ){
store.do('addPinModal.close');
}
}
if ( store.data.aboutModal.active ){
if ( el.key == "Escape" ){
store.do('aboutModal.close');
}
}
});
window.addEventListener("hashchange", () => {
store.do("hash.update");
});
window.addEventListener('resize', (evt) => {
store.do("render");
});
Reef.databind(appComponent);
store.do('load.boards');
store.do('hash.update');
appComponent.render();

View file

@ -1,8 +1,8 @@
.brick-wall {
.brickwall {
display: flex;
}
.brick-wall-column {
.brickwall-column {
flex: 1;
width: 100%;
}
@ -19,7 +19,7 @@
background-color: #ccc;
}
#brick-wall-container {
#brickwall-container {
max-width: 95%;
}
@ -45,18 +45,18 @@
object-fit: cover;
}
#pin-zoom-modal .modal-content {
#pinZoomModal .modal-content {
/* height: 90%; */
height: calc(100% - 120px);
min-height: 50px;
width: 90%;
}
#pin-zoom-modal .modal-content > p {
#pinZoomModal .modal-content > p {
height: 100%;
}
#pin-zoom-modal .modal-content > p > img {
#pinZoomModal .modal-content > p > img {
height: 100%;
width: 100%;
margin-left: auto;
@ -64,29 +64,29 @@
object-fit: contain;
}
#add-pin-modal-board-name {
#addPinModal-boardName {
font-weight: bold;
}
#add-pin-modal .add-pin-flex {
#addPinModal .add-pin-flex {
display: flex;
}
#add-pin-modal .add-pin-flex-left {
#addPinModal .add-pin-flex-left {
flex: 1;
margin: 10px;
margin-top: 40px;
}
#add-pin-modal .add-pin-flex-left img{
#addPinModal .add-pin-flex-left img{
border-radius: 12px;
width: 100%;
height: auto;
display: block;
}
#add-pin-modal .add-pin-flex-right {
#addPinModal .add-pin-flex-right {
flex: 2;
margin: 10px;
}
@ -106,49 +106,7 @@
}
/*
* loader - https://loading.io/css/
*/
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.pin-zoom-modal-delete {
.pinZoomModal-delete {
position: fixed;
width: 24px;
height: 24px;
@ -160,11 +118,11 @@
border: none;
opacity: 0.8;
}
.pin-zoom-modal-delete:hover{
.pinZoomModal-delete:hover{
opacity: 1;
}
.pin-zoom-modal-site-link {
.pinZoomModal-site-link {
position: fixed;
width: 24px;
height: 24px;
@ -177,11 +135,11 @@
opacity: 0.8;
}
.pin-zoom-modal-site-link:hover{
.pinZoomModal-site-link:hover{
opacity: 1;
}
.pin-zoom-modal-edit {
.pinZoomModal-edit {
position: fixed;
width: 24px;
height: 24px;
@ -194,16 +152,13 @@
opacity: 0.8;
}
.pin-zoom-modal-edit {
.pinZoomModal-edit {
opacity: 1;
}
#loader:after {
border-left-color: #3273dc;
border-bottom-color: #3273dc;
}
#pin-zoom-modal .pin-zoom-modal-description {
#pinZoomModal .pinZoomModal-description {
position: fixed;
height: 24px;
left: 20px;
@ -215,15 +170,15 @@
text-overflow: ellipsis;
}
#pin-zoom-modal .pin-zoom-modal-description:hover {
#pinZoomModal .pinZoomModal-description:hover {
cursor: pointer;
}
#pin-zoom-modal .pin-zoom-modal-full-description {
#pinZoomModal .pinZoomModal-full-description {
display: none;
}
#pin-zoom-modal .pin-zoom-modal-hide-full-description {
#pinZoomModal .pinZoomModal-hide-full-description {
display: block;
height: 16px;
width: 16px;
@ -234,11 +189,37 @@
margin-bottom: 8px;
}
#pin-zoom-modal .pin-zoom-modal-full-description.pin-zoom-modal-full-description-open {
#pinZoomModal .pinZoomModal-full-description.pinZoomModal-full-description-open {
display: block;
position: fixed;
left: 0px;
bottom: 0px;
background-color: #eeeeee;
padding: 20px;
}
#loader:after {
border-left-color: #3273dc;
border-bottom-color: #3273dc;
}
#loader-mobile {
display: none;
}
@media (max-width: 1023px) {
#loader-desktop {
display: none;
}
#burger-mobile {
margin-left: 0;
}
#loader-mobile {
display: block;
position: relative;
margin-left: auto;
}
}

View file

@ -24,7 +24,7 @@ const store = new Reef.Store({
pin: null,
fullDescriptionOpen: false
},
about: {
aboutModal: {
active: false
},
editBoard: {
@ -47,22 +47,55 @@ const store = new Reef.Store({
return false;
}
return true;
},
isEditBoardValid: (data) => {
if (!data.editBoard.name){
return false;
}
if ( data.editBoard.name.trim().length < 1 ){
return false;
}
return true;
}
}
});
// since we can't dynamically set setters/getters in Reef,
// we'll create our own outside 'store'
const actions = new Proxy(new function(){
const _actions = {};
this.add = (actionName, f) => {
if ( _actions[actionName] ){
console.error(`action ${actionName} is already defined.`);
} else {
console.log(`Added action ${actionName}`);
_actions[actionName] = f;
}
};
this.do = (actionName, target) => {
console.log(_actions);
if (!_actions[actionName]){
console.error(`action ${actionName} is not defined.`);
} else {
console.log(`running action ${actionName}`);
_actions[actionName](store.data, target);
}
}
set = () => {
console.error("Use actions.do(name, function).");
}
}, {
get(target, name, receiver){
console.log("target");
console.log(target);
console.log("name");
console.log(name);
console.log("receiver");
console.log(receiver);
return Reflect.get(target, name, receiver);
},
set(target, name, receiver){
console.error("Direct modification of actions is not allowed. Use actions.do(name, function) instead.");
}
});
function getBoardIndexById(id){
let idx = -1;
for ( let i = 0; i < store.data.boards.length; ++i ){
@ -91,243 +124,39 @@ function getPinById(id){
return store.data.board.pins[getPinIndexById(id)];
}
const actions = {
openAddPinModal: () => {
if ( store.data.board ){
store.data.addPin.boardId = store.data.board.id;
} else if ( store.data.boards && store.data.boards.length > 0 ){
store.data.addPin.boardId = store.data.boards[0].id;
} else {
store.data.addPin.boardId = "new";
}
// const actions = {
// deletePin: async () => {
// if ( confirm("Are you sure you want to delete this pin?") ){
store.data.addPin.active = true;
},
closeAddPinModal: () => {
store.data.addPin.active = false;
store.data.addPin.imageUrl = "";
store.data.addPin.previewImageUrl = "";
store.data.addPin.siteUrl = "";
store.data.addPin.description = "";
store.data.addPin.newBoardName = "";
store.data.addPin.saveInProgress = false;
},
saveAddPin: async () => {
// store.data.loading++;
store.data.addPin.saveInProgress = true;
// let pinId = store.data.pinZoom.pin.id;
let boardId = store.data.addPin.boardId;
// let idx = getPinIndexById(pin.id);
// if ( idx >= 0 ){
// store.data.board.pins.splice(idx,1);
// }
let newBoard = null;
// actions.closePinZoomModal();
if ( boardId == "new" ){
let res = await fetch('api/boards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"name": store.data.addPin.newBoardName
})
});
// let res = await fetch(`/api/pins/${pinId}`, {
// method: "DELETE"
// });
if ( res.status == 200 ){
newBoard = await res.json();
boardId = newBoard.id;
store.data.boards.push(newBoard);
}
}
// if ( res.status == 200 ){
// console.log(`deleted pin#${pinId}`);
// } else {
// console.error(`error deleting pin#${pinId}`);
// }
let postData = {
boardId: boardId,
imageUrl: store.data.addPin.imageUrl,
siteUrl: store.data.addPin.siteUrl,
description: store.data.addPin.description
};
let res = await fetch('api/pins', {
method: 'POST',
headers: {
'Content-Type': "application/json"
},
body: JSON.stringify(postData)
});
if ( res.status == 200 ){
let body = await res.json();
if ( store.data.board && store.data.board.id == boardId ){
store.data.board.pins.push(body);
}
if ( newBoard ){
newBoard.titlePinId = body.id;
}
actions.closeAddPinModal();
}
},
updateAddPinPreview: () => {
if ( store.data.addPin.imageUrl.startsWith("http") ){
( async() => {
let res = await fetch(store.data.addPin.imageUrl, {
mode: 'no-cors',
method: "HEAD"
});
if ( res.status = 200 ){
store.data.addPin.previewImageUrl = store.data.addPin.imageUrl;
}
})();
} else {
store.data.addPin.previewImageUrl = null;
}
},
openPinZoomModal: (el) => {
let pinId = el.getAttribute("data-pinid");
if( pinId ){
store.data.pinZoom.pin = getPinById(pinId);
store.data.pinZoom.active = true;
}
},
closePinZoomModal: () => {
store.data.pinZoom.active = false;
store.data.pinZoom.pinId = null;
store.data.pinZoom.fullDescriptionOpen = false;
},
movePinZoomModalLeft: () => {
let idx = getPinIndexById(store.data.pinZoom.pin.id);
if ( idx > 0 ){
store.data.pinZoom.pin = store.data.board.pins[idx-1];
}
},
movePinZoomModalRight: () => {
let idx = getPinIndexById(store.data.pinZoom.pin.id);
if ( idx >= 0 && (idx < store.data.board.pins.length-1) ){
store.data.pinZoom.pin = store.data.board.pins[idx+1];
}
},
deletePin: async () => {
if ( confirm("Are you sure you want to delete this pin?") ){
store.data.loading++;
let pinId = store.data.pinZoom.pin.id;
let idx = getPinIndexById(pin.id);
if ( idx >= 0 ){
store.data.board.pins.splice(idx,1);
}
actions.closePinZoomModal();
let res = await fetch(`/api/pins/${pinId}`, {
method: "DELETE"
});
if ( res.status == 200 ){
console.log(`deleted pin#${pinId}`);
} else {
console.error(`error deleting pin#${pinId}`);
}
store.data.loading--;
// store.data.loading--;
}
},
showAboutModal: () => {
store.data.about.active = true;
},
closeAboutModal: () => {
store.data.about.active = false;
},
openEditBoardModal: () => {
store.data.editBoard.name = store.data.board.name;
store.data.editBoard.active = true;
},
closeEditBoardModal: () => {
store.data.editBoard.name = "";
store.data.editBoard.active = false;
},
saveEditBoard: async () => {
// }
// },
store.data.loading++
let boardId = store.data.board.id;
let name = store.data.editBoard.name;
let idx = getBoardIndexById(boardId);
console.log("idx=" + idx);
if ( idx >= 0 ){
store.data.boards[idx].name = name;
store.data.board.name = name;
}
let res = await fetch(`/api/boards/${boardId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name
})
});
if ( res.status == 200 ){
console.log(`updated board#${boardId}`);
store.data.editBoard.active = false;
} else {
console.error(`error updating board#${boardId}`);
}
store.data.loading--;
},
editBoardDelete: async () => {
if ( !confirm("Are you sure you want to delete this board and all pins on it?") ){
return;
}
store.data.loading++;
let boardId = store.data.board.id;
let idx = getBoardIndexById(boardId);
if ( idx >= 0 ){
store.data.boards.splice(idx, 1);
}
store.data.editBoard.active = false;
window.location.hash = "";
let res = await fetch(`/api/boards/${boardId}`, {
method: 'DELETE'
});
if ( res.status == 200 ){
console.log(`deleted board#${boardId}`);
} else {
console.log(`error deleting board#${boardId}`);
}
store.data.loading--;
},
pinZoomShowFullDescription: () => {
store.data.pinZoom.fullDescriptionOpen = true;
},
pinZoomHideFullDescription: () => {
store.data.pinZoom.fullDescriptionOpen = false;
}
}
// }
const app = new Reef("#app", {
store: store,
@ -340,388 +169,28 @@ const app = new Reef("#app", {
</div>
</section>
<footer class="footer" id="footer">
<div class="content">
<div class="level">
<div class="level-left">
</div>
<div class="level-right">
<a data-onclick="showAboutModal">about tinypin</a>
</div>
</div>
<div class="content has-text-right">
<a data-onclick="aboutModal.open">about tinypin</a>
</div>
</footer>
<div id="add-pin-modal"></div>
<div id="pin-zoom-modal"></div>
<div id="edit-board-modal"></div>
<div id="about-modal"></div>
<div id="aboutModal"></div>
`
//<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
}
});
const navbar = new Reef("#navbar", {
store: store,
template: (data) => {
let boardName = "";
if ( data.board ){
boardName = /*html*/`
<span class="navbar-item">${data.board.name} &nbsp;
<a data-onclick="openEditBoardModal" style="padding-top: 3px;"><img alt="edit" width="16" height="16" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHg9IjBweCIgeT0iMHB4Ij48dGl0bGU+NTE8L3RpdGxlPjxwYXRoIGQ9Ik04NC44NTAxMiw1MFY4MS43NDUxMkExMy4yNzAxMiwxMy4yNzAxMiwwLDAsMSw3MS41OTUyNCw5NUgxOC4yNTQ5MUExMy4yNzAxMiwxMy4yNzAxMiwwLDAsMSw1LDgxLjc0NTEyVjI4LjQwNTI4QTEzLjI3MDEyLDEzLjI3MDEyLDAsMCwxLDE4LjI1NDkxLDE1LjE1MDRINTBhMi41LDIuNSwwLDAsMSwwLDVIMTguMjU0OTFBOC4yNjQyMyw4LjI2NDIzLDAsMCwwLDEwLDI4LjQwNTI4VjgxLjc0NTEyQTguMjY0MjQsOC4yNjQyNCwwLDAsMCwxOC4yNTQ5MSw5MEg3MS41OTUyNGE4LjI2NDIzLDguMjY0MjMsMCwwLDAsOC4yNTQ4OC04LjI1NDg5VjUwYTIuNSwyLjUsMCwwLDEsNSwwWk04OS4xNDg0Niw2LjIzNzkyYTQuMjI2NjEsNC4yMjY2MSwwLDAsMC01Ljk3NzI5LDBsLTMzLjk2MjksMzMuOTYzTDU5Ljc5OTE2LDUwLjc5MTc2bDMzLjk2Mjg5LTMzLjk2M2E0LjIyNjUzLDQuMjI2NTMsMCwwLDAsMC01Ljk3NzIzWk00My42MjM4LDU4LjMxMjg3bDEzLjAwOTQtNC4zNTUxNkw0Ni4wNDIyNiw0My4zNjY4M2wtNC4zNTUxLDEzLjAwOTRBMS41MzAwNSwxLjUzMDA1LDAsMCwwLDQzLjYyMzgsNTguMzEyODdaIj48L3BhdGg+PC9zdmc+" /></a>
</span>`;
} else if ( !data.hash.board ) {
boardName = /*html*/`<span class="navbar-item">Boards</span>`;
}
return /*html*/`
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="#">
<img alt="boards" width="32" height="32" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2OCA0OCIgeD0iMHB4IiB5PSIwcHgiPjxwYXRoIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTcyLDgwNiBMMTA3LDgwNiBDMTA4LjEwNDU2OSw4MDYgMTA5LDgwNi44OTU0MzEgMTA5LDgwOCBMMTA5LDgzMiBDMTA5LDgzMy4xMDQ1NjkgMTA4LjEwNDU2OSw4MzQgMTA3LDgzNCBMNzIsODM0IEM3MC44OTU0MzA1LDgzNCA3MCw4MzMuMTA0NTY5IDcwLDgzMiBMNzAsODA4IEM3MCw4MDYuODk1NDMxIDcwLjg5NTQzMDUsODA2IDcyLDgwNiBaIE0xMTIsODIyIEwxMTIsODIyIEwxMTIsODA4IEMxMTIsODA1LjIzODU3NiAxMDkuNzYxNDI0LDgwMyAxMDcsODAzIEw5Niw4MDMgTDk2LDc4OCBDOTYsNzg2Ljg5NTQzMSA5Ni44OTU0MzA1LDc4NiA5OCw3ODYgTDEyMiw3ODYgQzEyMy4xMDQ1NjksNzg2IDEyNCw3ODYuODk1NDMxIDEyNCw3ODggTDEyNCw4MjAgQzEyNCw4MjEuMTA0NTY5IDEyMy4xMDQ1NjksODIyIDEyMiw4MjIgTDExMiw4MjIgWiBNODQsODAzIEw3Miw4MDMgQzY5LjIzODU3NjMsODAzIDY3LDgwNS4yMzg1NzYgNjcsODA4IEw2Nyw4MTcgTDU4LDgxNyBDNTYuODk1NDMwNSw4MTcgNTYsODE2LjEwNDU2OSA1Niw4MTUgTDU2LDc5MSBDNTYsNzg5Ljg5NTQzMSA1Ni44OTU0MzA1LDc4OSA1OCw3ODkgTDgyLDc4OSBDODMuMTA0NTY5NSw3ODkgODQsNzg5Ljg5NTQzMSA4NCw3OTEgTDg0LDgwMyBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNTYgLTc4NikiPjwvcGF0aD48L3N2Zz4=" />
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
${boardName}
</div>
<div class="navbar-end">
<span class="navbar-item">
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
</span>
<a class="navbar-item" data-onclick="openAddPinModal">
<img alt="add pin" width="32" height="32" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' data-name='Layer 1' viewBox='0 0 100 125' x='0px' y='0px'%3E%3Ctitle%3EArtboard 164%3C/title%3E%3Cpath d='M56.77,3.11a4,4,0,1,0-5.66,5.66l5.17,5.17L37.23,33A23.32,23.32,0,0,0,9.42,36.8L7.11,39.11a4,4,0,0,0,0,5.66l21.3,21.29L3.23,91.23a4,4,0,0,0,5.66,5.66L34.06,71.72l21,21a4,4,0,0,0,5.66,0l2.31-2.31a23.34,23.34,0,0,0,3.81-27.82l19-19,5.17,5.18a4,4,0,0,0,5.66-5.66Zm1.16,81.16L15.61,42a15.37,15.37,0,0,1,21.19.51L57.42,63.08A15.39,15.39,0,0,1,57.93,84.27Zm4-28L43.59,37.94,61.94,19.59,80.28,37.94Z'/%3E%3C/svg%3E"/>
</a>
</div>
</div>
</nav>
`;
},
attachTo: app
});
const brickwall = new Reef('#brick-wall', {
store: store,
template: (data, el) => {
// if the hash says we are supposed to be drawing a board, but it hasn't loaded yet... draw an empty div.
if ( data.hash.board && !data.board ){
return '<div></div>';
}
let numberOfColumns = 1;
let width = el.offsetWidth;
// matching bulma breakpoints - https://bulma.io/documentation/overview/responsiveness/
if( width >= 1216 ){
numberOfColumns = 5;
} else if ( width >= 1024 ){
numberOfColumns = 4;
} else if ( width >= 769 ){
numberOfColumns = 3;
} else if ( width > 320 ){
numberOfColumns = 2;
}
function createBrickForBoard(board){
return /*html*/`
<div class="brick board-brick">
<a href="#board=${board.id}">
<img src="${board.titlePinId > 0 ? getThumbnailImagePath(board.titlePinId) : ''}" />
<div class="board-brick-name">${board.name}</div>
</a>
</div>
`;
}
function createBrickForPin(board, pin){
return /*html*/`
<div class="brick" >
<a data-pinid="${pin.id}" data-onclick="openPinZoomModal">
<img src="${getThumbnailImagePath(pin.id)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}" />
</a>
</div>
`;
}
// create the brick elements
let bricks = [];
if ( data.board ){
for ( let i = 0; i < data.board.pins.length; ++i ){
bricks.push(createBrickForPin(data.board, data.board.pins[i]));
}
} else {
for ( let i = 0; i < data.boards.length; ++i ){
bricks.push(createBrickForBoard(data.boards[i]));
}
}
// create column objects
let columns = [];
for ( let i = 0; i < numberOfColumns; ++i ){
columns[i] = {
height: 0,
bricks: []
}
}
// sort bricks into columns
for ( let i = 0; i < bricks.length; ++i ){
columns[i % columns.length].bricks.push(bricks[i]);
}
// write out the bricks
let result = "";
for ( let col = 0; col < columns.length; ++col ){
result += '<div class="brick-wall-column">';
for ( let i = 0; i < columns[col].bricks.length; ++i ){
result += columns[col].bricks[i];
}
result += '</div>';
}
return result;
},
attachTo: app
});
const addPinModal = new Reef("#add-pin-modal", {
store: store,
template: (data) => {
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.addPin.boardId == "new" ){
newBoardField = /*html*/`
<div class="field">
<label class="label">Board Name</label>
<div class="control">
<input class="input" type="text" data-bind="addPin.newBoardName" />
</div>
</div>
`;
}
return /*html*/`
<div class="modal ${data.addPin.active ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Pin</p>
<button class="delete" aria-label="close" data-onclick="closeAddPinModal"></button>
</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.addPin.previewImageUrl ? data.addPin.previewImageUrl : imagePlaceholder}" />
</div>
<div class="add-pin-flex-right">
<form>
<div class="field">
<label class="label">Board</label>
<div class="select">
<select data-bind="addPin.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="addPin.imageUrl" data-onblur="updateAddPinPreview"/>
</div>
</div>
<div class="field">
<label class="label">Website Url</label>
<div class="control">
<input class="input" type="text" data-bind="addPin.siteUrl" />
</div>
</div>
<div class="field">
<label class="label">Description</label>
<div class="control">
<textarea class="textarea" data-bind="addPin.description"></textarea>
</div>
</div>
</form>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success ${data.addPin.saveInProgress ? 'is-loading' : ''}" ${!store.get('isAddPinValid') || data.addPin.saveInProgress ? 'disabled' : ''} data-onclick="saveAddPin">Add Pin</button>
</footer>
</div>
</div>
`;
},
attachTo: app
});
const editBoardModal = new Reef("#edit-board-modal", {
store: store,
template: (data) => {
return /*html*/`
<div class="modal ${data.editBoard.active ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Edit Board</p>
<button class="delete" aria-label="close" data-onclick="closeEditBoardModal"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input class="input" type="text" data-bind="editBoard.name" />
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success ${data.editBoard.saveInProgress ? 'is-loading' : '' }" ${!store.get('isEditBoardValid') || data.editBoard.saveInProgress ? 'disabled' : ''} data-onclick="saveEditBoard">Save</button>
<button class="button is-danger" data-onclick="editBoardDelete">Delete</button>
</footer>
</div>
</div>
`;
},
attachTo: app
});
const pinZoomModal = new Reef("#pin-zoom-modal", {
store: store,
template: (data) => {
let siteLink = '';
if ( data.pinZoom.pin && data.pinZoom.pin.siteUrl ){
siteLink = `<a class="pin-zoom-modal-site-link" href="${data.pinZoom.pin.siteUrl}"></a>`;
}
return /*html*/`
<div class="modal ${data.pinZoom.active ? 'is-active' : ''}" id="pin-zoom-modal" >
<div class="modal-background" data-onclick="closePinZoomModal"></div>
<div class="modal-content">
<p>
<img src="${data.pinZoom.active ? getOriginalImagePath(data.pinZoom.pin.id) : ''}" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" data-onclick="closePinZoomModal"></button>
${siteLink}
<a class="pin-zoom-modal-edit" data-onclick="editPin"></a>
<a class="pin-zoom-modal-delete" data-onclick="deletePin"></a>
<div class="pin-zoom-modal-description" data-onclick="pinZoomShowFullDescription">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
<div class="pin-zoom-modal-full-description ${data.pinZoom.fullDescriptionOpen ? 'pin-zoom-modal-full-description-open' : ''}">
<div>
<a class="pin-zoom-modal-hide-full-description" data-onclick="pinZoomHideFullDescription">&nbsp;</a>
</div>
<div class="content">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
</div>
</div>
`;
},
attachTo: app
});
const aboutModal = new Reef("#about-modal", {
store: store,
template: (data) => {
return /*html*/`
<div class="modal ${data.about.active ? 'is-active' : ''}">
<div class="modal-background" data-onclick="closeAboutModal"></div>
<div class="modal-content">
<div class="box" style="font-family: monospace;">
<h1><strong>tinypin</strong></h1>
<div>
<a href="https://www.github.com">github.com/slynn1324/tinypin</a>
<br />
&nbsp;
</div>
<div>
<h2><strong>credits</strong></h2>
client
<br />
&nbsp;css framework &raquo; <a href="https://www.bulma.io">bulma.io</a>
<br />
&nbsp;ui framework &raquo; <a href="https://reefjs.com">reef</a>
<br />
&nbsp;boards icon &raquo; <a href="https://thenounproject.com/term/squares/1160031/">squares by Andrejs Kirma from the Noun Project</a>
<br />
&nbsp;pin icon &raquo; <a href="https://thenounproject.com/term/pinned/1560993/">pinned by Gregor Cresnar from the Noun Project</a>
<br />
&nbsp;trash icon &raquo; <a href="https://thenounproject.com/term/trash/2449397/">Trash by ICONZ from the Noun Project</a>
<br />
server
<br />
&nbsp;language &amp; runtime &raquo; <a href="https://nodejs.org/en/">node.js</a>
<br />
&nbsp;database &raquo; <a href="https://www.sqlite.org/index.html">sqlite</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/better-sqlite3">better-sqlite3</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/express">express</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/body-parser">body-parser</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/node-fetch">node-fetch</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/sharp">sharp</a>
</div>
</div>
</div>
<button class="modal-close is-large" aria-label="close" data-onclick="closeAboutModal"></button>
</div>
`;
},
attachTo: app
});
document.addEventListener('click', (el) => {
let target = el.target.closest('[data-onclick]');
if (target) {
let action = target.getAttribute('data-onclick');
if (action) {
if ( !actions[action] ){
console.error(`No action named ${action}`);
} else {
actions[action](target);
}
actions.do(action, target);
}
}
});
@ -731,8 +200,8 @@ document.addEventListener('focusout', (el) => {
let target = el.target.closest('[data-onblur]');
if ( target ){
let method = target.getAttribute('data-onblur');
if ( method && typeof(actions[method]) === 'function') {
actions[method](target);
if ( method ) {
actions.do(method, target);
}
}
});
@ -755,7 +224,7 @@ document.addEventListener('keyup', (el) => {
}
}
if ( store.data.about.active ){
if ( store.data.aboutModal.active ){
if ( el.key == "Escape" ){
actions.closeAboutModal();
}
@ -764,7 +233,6 @@ document.addEventListener('keyup', (el) => {
});
window.addEventListener('hashchange', (evt) => {
console.log("hash change");
handleHash();
});
@ -793,7 +261,7 @@ function handleHash(){
store.data.pinZoom.active = false;
store.data.addPin.active = false;
store.data.about.active = false;
store.data.aboutModal.active = false;
}
}
@ -827,25 +295,3 @@ function parseQueryString(qs){
return obj;
}
// image urls
function padId(id){
let result = id.toString();
while ( result.length < 12 ) {
result = '0' + result;
}
return result;
}
function getOriginalImagePath(pinId){
let paddedId = padId(pinId);
let dir = `originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return file;
}
function getThumbnailImagePath(pinId){
let paddedId = padId(pinId);
let dir = `thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return file;
}

View file

@ -0,0 +1,65 @@
app.addSetter('aboutModal.open', (data) => {
data.aboutModal.active = true;
});
app.addSetter('aboutModal.close', (data) => {
data.aboutModal.active = false;
});
app.addComponent('aboutModal', (store) => { return new Reef("#aboutModal", {
store: store,
template: (data) => {
return /*html*/`
<div class="modal ${data.aboutModal.active ? 'is-active' : ''}">
<div class="modal-background" data-onclick="aboutModal.close"></div>
<div class="modal-content">
<div class="box" style="font-family: monospace;">
<h1><strong>tinypin</strong></h1>
<div>
<a href="https://www.github.com">github.com/slynn1324/tinypin</a>
<br />
&nbsp;
</div>
<div>
<h2><strong>credits</strong></h2>
client
<br />
&nbsp;css framework &raquo; <a href="https://www.bulma.io">bulma.io</a>
<br />
&nbsp;ui framework &raquo; <a href="https://reefjs.com">reef</a>
<br />
&nbsp;boards icon &raquo; <a href="https://thenounproject.com/term/squares/1160031/">squares by Andrejs Kirma from the Noun Project</a>
<br />
&nbsp;pin icon &raquo; <a href="https://thenounproject.com/term/pinned/1560993/">pinned by Gregor Cresnar from the Noun Project</a>
<br />
&nbsp;web icon &raquo; <a href="https://thenounproject.com/term/website/2868662/">website by Supriadi Sihotang from the Noun Project</a>
<br />
&nbsp;edit icon &raquo; <a href="https://thenounproject.com/term/edit/3122457/">edit by TTHNga from the Noun Project</a>
<br />
&nbsp;logout icon &raquo; <a href="https://thenounproject.com/term/log-out/3556472">Log Out by Gregor Cresnar from the Noun Project</a>
<br />
&nbsp;trash icon &raquo; <a href="https://thenounproject.com/term/trash/2449397/">Trash by ICONZ from the Noun Project</a>
<br />
server
<br />
&nbsp;language &amp; runtime &raquo; <a href="https://nodejs.org/en/">node.js</a>
<br />
&nbsp;database &raquo; <a href="https://www.sqlite.org/index.html">sqlite</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/better-sqlite3">better-sqlite3</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/express">express</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/body-parser">body-parser</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/node-fetch">node-fetch</a>
<br />
&nbsp;library &raquo; <a href="https://www.npmjs.com/package/sharp">sharp</a>
</div>
</div>
</div>
<button class="modal-close is-large" aria-label="close" data-onclick="aboutModal.close"></button>
</div>
`;
}
}); });

203
static/components/addpin.js Normal file
View file

@ -0,0 +1,203 @@
app.addSetter('addPinModal.open', (data) => {
if ( data.board ){
data.addPinModal.boardId = data.board.id;
} else if ( data.boards && data.boards.length > 0 ){
data.addPinModal.boardId = data.boards[0].id;
} else {
data.addPinModal.boardId = "new";
}
data.addPinModal.active = true;
});
app.addSetter('addPinModal.close', (data) => {
data.addPinModal.active = false;
data.addPinModal.imageUrl = "";
data.addPinModal.previewImageUrl = "";
data.addPinModal.siteUrl = "";
data.addPinModal.description = "";
data.addPinModal.newBoardName = "";
data.addPinModal.saveInProgress = false;
});
app.addSetter('addPinModal.updatePreview', (data) => {
console.log("update preview");
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.addSetter('addPinModal.save', async (data) => {
store.do("loader.show");
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 ){
let body = await res.json();
if ( data.board && data.board.id == boardId ){
data.board.pins.push(body);
}
if ( newBoard ){
newBoard.titlePinId = body.id;
}
store.do("addPinModal.close");
}
store.do("loader.hide");
});
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.addComponent('addPinModal', (store) => { return new Reef("#addPinModal", {
store: store,
template: (data) => {
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 class="modal ${data.addPinModal.active ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Pin</p>
<button class="delete" aria-label="close" data-onclick="addPinModal.close"></button>
</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"/>
</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>
</footer>
</div>
</div>
`;
}
}); });

View file

@ -0,0 +1,90 @@
app.addComponent('brickwall', (store) => { return new Reef('#brickwall', {
store: store,
template: (data, el) => {
// if the hash says we are supposed to be drawing a board, but it hasn't loaded yet... draw an empty div.
if ( data.hash.board && !data.board ){
return '<div></div>';
}
let numberOfColumns = 1;
let width = el.offsetWidth;
// matching bulma breakpoints - https://bulma.io/documentation/overview/responsiveness/
if( width >= 1216 ){
numberOfColumns = 5;
} else if ( width >= 1024 ){
numberOfColumns = 4;
} else if ( width >= 769 ){
numberOfColumns = 3;
} else if ( width > 320 ){
numberOfColumns = 2;
}
function createBrickForBoard(board){
return /*html*/`
<div class="brick board-brick">
<a href="#board=${board.id}">
<img src="${board.titlePinId > 0 ? getThumbnailImagePath(board.titlePinId) : ''}" />
<div class="board-brick-name">${board.name}</div>
</a>
</div>
`;
}
function createBrickForPin(board, pin){
return /*html*/`
<div class="brick" >
<a data-pinid="${pin.id}" data-onclick="pinZoomModal.open">
<img src="${getThumbnailImagePath(pin.id)}" width="${pin.thumbnailWidth}" height="${pin.thumbnailHeight}" />
</a>
</div>
`;
}
// create the brick elements
let bricks = [];
if ( data.board ){
for ( let i = 0; i < data.board.pins.length; ++i ){
bricks.push(createBrickForPin(data.board, data.board.pins[i]));
}
} else {
for ( let i = 0; i < data.boards.length; ++i ){
bricks.push(createBrickForBoard(data.boards[i]));
}
}
// create column objects
let columns = [];
for ( let i = 0; i < numberOfColumns; ++i ){
columns[i] = {
height: 0,
bricks: []
}
}
// sort bricks into columns
for ( let i = 0; i < bricks.length; ++i ){
columns[i % columns.length].bricks.push(bricks[i]);
}
// write out the bricks
let result = "";
for ( let col = 0; col < columns.length; ++col ){
result += '<div class="brickwall-column">';
for ( let i = 0; i < columns[col].bricks.length; ++i ){
result += columns[col].bricks[i];
}
result += '</div>';
}
return result;
}
}); });

View file

@ -0,0 +1,119 @@
app.addSetter('editBoardModal.open', (data) => {
data.editBoardModal.name = data.board.name;
data.editBoardModal.active = true;
});
app.addSetter('editBoardModal.close', (data) => {
data.editBoardModal.name = "";
data.editBoardModal.active = false;
});
app.addSetter('editBoardModal.save', async (data) => {
store.do("loader.show");
let boardId = data.board.id;
let name = data.editBoardModal.name;
let idx = getBoardIndexById(boardId);
if ( idx >= 0 ){
data.boards[idx].name = name;
data.board.name = name;
}
let res = await fetch(`/api/boards/${boardId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name
})
});
if ( res.status == 200 ){
console.log(`updated board#${boardId}`);
data.editBoardModal.active = false;
} else {
console.error(`error updating board#${boardId}`);
}
store.do("loader.hide");
});
app.addSetter('editBoardModal.delete', async (data) => {
if ( !confirm("Are you sure you want to delete this board and all pins on it?") ){
return;
}
store.do("loader.show");
let boardId = data.board.id;
let idx = getBoardIndexById(boardId);
console.log(idx);
if ( idx >= 0 ){
data.boards.splice(idx, 1);
}
data.editBoardModal.active = false;
window.location.hash = "";
let res = await fetch(`/api/boards/${boardId}`, {
method: 'DELETE'
});
if ( res.status == 200 ){
console.log(`deleted board#${boardId}`);
} else {
console.log(`error deleting board#${boardId}`);
}
store.do("loader.hide");
});
app.addGetter('editBoardModal.isValid', (data) => {
if (!data.editBoardModal.name){
return false;
}
if ( data.editBoardModal.name.trim().length < 1 ){
return false;
}
return true;
});
app.addComponent('editBoardModal', (store) => { return new Reef("#editBoardModal", {
store: store,
template: (data) => {
return /*html*/`
<div class="modal ${data.editBoardModal.active ? 'is-active' : ''}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Edit Board</p>
<button class="delete" aria-label="close" data-onclick="editBoardModal.close"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Name</label>
<div class="control">
<input class="input" type="text" data-bind="editBoardModal.name" />
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success ${data.editBoardModal.saveInProgress ? 'is-loading' : '' }" ${!store.get('editBoardModal.isValid') || data.editBoardModal.saveInProgress ? 'disabled' : ''} data-onclick="editBoardModal.save">Save</button>
<button class="button is-danger" data-onclick="editBoardModal.delete">Delete</button>
</footer>
</div>
</div>
`;
}
}); });

View file

@ -0,0 +1,59 @@
app.addComponent('navbar', (store) => { return new Reef("#navbar", {
store: store,
template: (data) => {
let boardName = "";
if ( data.board ){
boardName = /*html*/`
<span class="navbar-item">${data.board.name} &nbsp;
<a data-onclick="editBoardModal.open" style="padding-top: 3px;"><img alt="edit" width="16" height="16" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHg9IjBweCIgeT0iMHB4Ij48dGl0bGU+NTE8L3RpdGxlPjxwYXRoIGQ9Ik04NC44NTAxMiw1MFY4MS43NDUxMkExMy4yNzAxMiwxMy4yNzAxMiwwLDAsMSw3MS41OTUyNCw5NUgxOC4yNTQ5MUExMy4yNzAxMiwxMy4yNzAxMiwwLDAsMSw1LDgxLjc0NTEyVjI4LjQwNTI4QTEzLjI3MDEyLDEzLjI3MDEyLDAsMCwxLDE4LjI1NDkxLDE1LjE1MDRINTBhMi41LDIuNSwwLDAsMSwwLDVIMTguMjU0OTFBOC4yNjQyMyw4LjI2NDIzLDAsMCwwLDEwLDI4LjQwNTI4VjgxLjc0NTEyQTguMjY0MjQsOC4yNjQyNCwwLDAsMCwxOC4yNTQ5MSw5MEg3MS41OTUyNGE4LjI2NDIzLDguMjY0MjMsMCwwLDAsOC4yNTQ4OC04LjI1NDg5VjUwYTIuNSwyLjUsMCwwLDEsNSwwWk04OS4xNDg0Niw2LjIzNzkyYTQuMjI2NjEsNC4yMjY2MSwwLDAsMC01Ljk3NzI5LDBsLTMzLjk2MjksMzMuOTYzTDU5Ljc5OTE2LDUwLjc5MTc2bDMzLjk2Mjg5LTMzLjk2M2E0LjIyNjUzLDQuMjI2NTMsMCwwLDAsMC01Ljk3NzIzWk00My42MjM4LDU4LjMxMjg3bDEzLjAwOTQtNC4zNTUxNkw0Ni4wNDIyNiw0My4zNjY4M2wtNC4zNTUxLDEzLjAwOTRBMS41MzAwNSwxLjUzMDA1LDAsMCwwLDQzLjYyMzgsNTguMzEyODdaIj48L3BhdGg+PC9zdmc+" /></a>
</span>`;
} else if ( !data.hash.board ) {
boardName = /*html*/`<span class="navbar-item">Boards</span>`;
}
return /*html*/`
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="#">
<img alt="boards" style="width:24px; height: 24px;" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2OCA0OCIgeD0iMHB4IiB5PSIwcHgiPjxwYXRoIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTcyLDgwNiBMMTA3LDgwNiBDMTA4LjEwNDU2OSw4MDYgMTA5LDgwNi44OTU0MzEgMTA5LDgwOCBMMTA5LDgzMiBDMTA5LDgzMy4xMDQ1NjkgMTA4LjEwNDU2OSw4MzQgMTA3LDgzNCBMNzIsODM0IEM3MC44OTU0MzA1LDgzNCA3MCw4MzMuMTA0NTY5IDcwLDgzMiBMNzAsODA4IEM3MCw4MDYuODk1NDMxIDcwLjg5NTQzMDUsODA2IDcyLDgwNiBaIE0xMTIsODIyIEwxMTIsODIyIEwxMTIsODA4IEMxMTIsODA1LjIzODU3NiAxMDkuNzYxNDI0LDgwMyAxMDcsODAzIEw5Niw4MDMgTDk2LDc4OCBDOTYsNzg2Ljg5NTQzMSA5Ni44OTU0MzA1LDc4NiA5OCw3ODYgTDEyMiw3ODYgQzEyMy4xMDQ1NjksNzg2IDEyNCw3ODYuODk1NDMxIDEyNCw3ODggTDEyNCw4MjAgQzEyNCw4MjEuMTA0NTY5IDEyMy4xMDQ1NjksODIyIDEyMiw4MjIgTDExMiw4MjIgWiBNODQsODAzIEw3Miw4MDMgQzY5LjIzODU3NjMsODAzIDY3LDgwNS4yMzg1NzYgNjcsODA4IEw2Nyw4MTcgTDU4LDgxNyBDNTYuODk1NDMwNSw4MTcgNTYsODE2LjEwNDU2OSA1Niw4MTUgTDU2LDc5MSBDNTYsNzg5Ljg5NTQzMSA1Ni44OTU0MzA1LDc4OSA1OCw3ODkgTDgyLDc4OSBDODMuMTA0NTY5NSw3ODkgODQsNzg5Ljg5NTQzMSA4NCw3OTEgTDg0LDgwMyBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNTYgLTc4NikiPjwvcGF0aD48L3N2Zz4=" />
</a>
${boardName}
<span id="loader-mobile" class="navbar-item" style="position: relative; margin-left: auto;">
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
</span>
<a id="burger-mobile" role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu is-active">
<!--<div class="navbar-start">
${boardName}
</div>-->
<div class="navbar-end">
<span id="loader-desktop" class="navbar-item">
<div id="loader" class="button is-text ${data.loading ? 'is-loading' : ''}"></div>
</span>
<a class="navbar-item has-text-right" data-onclick="addPinModal.open">
<span class="is-hidden-desktop">Add Pin</span>
<img alt="add pin" style="width:24px;height:24px; margin-top:4px;" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' data-name='Layer 1' viewBox='0 0 100 125' x='0px' y='0px'%3E%3Ctitle%3EArtboard 164%3C/title%3E%3Cpath d='M56.77,3.11a4,4,0,1,0-5.66,5.66l5.17,5.17L37.23,33A23.32,23.32,0,0,0,9.42,36.8L7.11,39.11a4,4,0,0,0,0,5.66l21.3,21.29L3.23,91.23a4,4,0,0,0,5.66,5.66L34.06,71.72l21,21a4,4,0,0,0,5.66,0l2.31-2.31a23.34,23.34,0,0,0,3.81-27.82l19-19,5.17,5.18a4,4,0,0,0,5.66-5.66Zm1.16,81.16L15.61,42a15.37,15.37,0,0,1,21.19.51L57.42,63.08A15.39,15.39,0,0,1,57.93,84.27Zm4-28L43.59,37.94,61.94,19.59,80.28,37.94Z'/%3E%3C/svg%3E"/>
</a>
<a class="navbar-item has-text-right">
<span class="is-hidden-desktop">Sign Out</span>
<img alt="sign out" width="32" height="32" src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHg9IjBweCIgeT0iMHB4Ij48dGl0bGU+QXJ0Ym9hcmQgODQ8L3RpdGxlPjxnPjxwYXRoIGQ9Ik0yOCw4MkgzOFY3NEgyOGEyLDIsMCwwLDEtMi0yVjI4YTIsMiwwLDAsMSwyLTJIMzhWMThIMjhBMTAsMTAsMCwwLDAsMTgsMjhWNzJBMTAsMTAsMCwwLDAsMjgsODJaIj48L3BhdGg+PHBhdGggZD0iTTY2LDMyLjM0LDYwLjM0LDM4bDgsOEgzNHY4SDY4LjM0bC04LDhMNjYsNjcuNjYsODAuODMsNTIuODNhNCw0LDAsMCwwLDAtNS42NloiPjwvcGF0aD48L2c+PC9zdmc+" />
<a>
</div>
</div>
</nav>
`;
}
}); });

View file

@ -0,0 +1,141 @@
function getBoardIndexById(id){
let idx = -1;
for ( let i = 0; i < store.data.boards.length; ++i ){
if ( store.data.boards[i].id == id ){
idx = i;
}
}
return idx;
}
function getBoardById(id){
return store.data.boards[getBoardIndexById(id)];
}
function getPinIndexById(id){
let idx = -1;
for ( let i = 0; i < store.data.board.pins.length; ++i ){
if ( store.data.board.pins[i].id == id ){
idx = i;
}
}
return idx;
}
function getPinById(id){
return store.data.board.pins[getPinIndexById(id)];
}
app.addSetter('pinZoomModal.open', (data, el) => {
let pinId = el.getAttribute("data-pinid");
if( pinId ){
data.pinZoomModal.pin = getPinById(pinId);
data.pinZoomModal.active = true;
}
});
app.addSetter('pinZoomModal.close', (data) => {
data.pinZoomModal.active = false;
data.pinZoomModal.pinId = null;
data.pinZoomModal.fullDescriptionOpen = false;
});
app.addSetter('pinZoomModal.moveLeft', (data) => {
let idx = getPinIndexById(data.pinZoomModal.pin.id);
if ( idx > 0 ){
data.pinZoomModal.pin = data.board.pins[idx-1];
}
});
app.addSetter('pinZoomModal.moveRight', (data) => {
let idx = getPinIndexById(data.pinZoomModal.pin.id);
if ( idx >= 0 && (idx < data.board.pins.length-1) ){
data.pinZoomModal.pin = data.board.pins[idx+1];
}
});
app.addSetter('pinZoomModal.showFullDescription', (data) => {
data.pinZoomModal.fullDescriptionOpen = true;
});
app.addSetter('pinZoomModal.hideFullDescription', (data) => {
data.pinZoomModal.fullDescriptionOpen = false;
});
app.addSetter('pinZoomModal.deletePin', async (data) => {
if ( !confirm("Are you sure you want to delete this pin?" ) ){
return;
}
store.do('loader.show');
let pinId = data.pinZoomModal.pin.id;
let idx = getPinIndexById(pinId);
if ( idx >= 0 ){
data.board.pins.splice(idx,1);
}
store.do("pinZoomModal.close");
let res = await fetch(`api/pins/${pinId}`, {
method: 'DELETE'
});
if ( res.status == 200 ){
console.log(`deleted pin#${pinId}`);
} else {
console.error(`error deleting pin#${pinId}`);
}
store.do('loader.hide');
});
app.addComponent('pinZoomModal', (store) => { return new Reef("#pinZoomModal", {
store: store,
template: (data) => {
let siteLink = '';
if ( data.pinZoomModal.pin && data.pinZoomModal.pin.siteUrl ){
siteLink = `<a class="pinZoomModal-site-link" href="${data.pinZoomModal.pin.siteUrl}"></a>`;
}
return /*html*/`
<div class="modal ${data.pinZoomModal.active ? 'is-active' : ''}">
<div class="modal-background" data-onclick="pinZoomModal.close"></div>
<div class="modal-content">
<p>
<img src="${data.pinZoomModal.active ? getOriginalImagePath(data.pinZoomModal.pin.id) : ''}" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" data-onclick="pinZoomModal.close"></button>
${siteLink}
<a class="pinZoomModal-edit" data-onclick="pinZoomModal.editPin"></a>
<a class="pinZoomModal-delete" data-onclick="pinZoomModal.deletePin"></a>
<div class="pinZoomModal-description" data-onclick="pinZoomModal.showFullDescription">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
<div class="pinZoomModal-full-description ${data.pinZoomModal.fullDescriptionOpen ? 'pinZoomModal-full-description-open' : ''}">
<div>
<a class="pinZoomModal-hide-full-description" data-onclick="pinZoomModal.hideFullDescription">&nbsp;</a>
</div>
<div class="content">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
</div>
</div>
`;
}
}); });

View file

@ -10,8 +10,25 @@
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/reefjs/dist/reef.min.js"></script>
<script src="reef-bootstrap.js"></script>
<script src="reef-databind.js"></script>
<script src="client.js"></script>
<script src="utils.js"></script>
<!-- <script src="client.js"></script> -->
<script src="components/navbar.js"></script>
<script src="components/brickwall.js"></script>
<script src="components/pinzoom.js"></script>
<script src="components/addpin.js"></script>
<script src="components/editboard.js"></script>
<script src="components/about.js"></script>
<script src="app.js"></script>
</body>
</html>

58
static/reef-bootstrap.js Normal file
View file

@ -0,0 +1,58 @@
const app = new Proxy(new function(){
const _setters = {};
const _getters = {};
const _components = {};
let frozen = false;
this.addSetter = (name, f) => {
if ( frozen ){
console.error(`actions are frozen, add setter '${name}' before init.`);
}
if ( _setters[name] ){
console.error(`setter ${name} is already defined.`);
} else {
_setters[name] = f;
}
}
this.addGetter = (name, f) => {
if ( frozen ){
console.error(`actions are frozen, add getter '${name}' before init.`);
}
if (_getters[name] ){
console.error(`getter ${name} is already defined.`);
} else {
_getters[name] = f;
}
}
this.addComponent = (name, f) => {
if ( frozen ){
throw(new Error(`context is frozen, add component '${name}' components before init`));
}
_components[name] = f;
}
this.getSetters = () => {
return _setters;
}
this.getGetters = () => {
return _getters;
}
this.getComponents = () => {
return _components;
}
this.freeze = () => {
frozen = true;
}
}, {
get(target, name, receiver){
return Reflect.get(target, name, receiver);
},
set(target, name, receiver){
console.error("Direct modification of setters is not allowed. Use setters.do(name, function) instead.");
}
});

View file

@ -1,5 +1,105 @@
// this currently will bind all fields with 'data-bind' attributes
// to the 'store'.
app.addSetter('databind.onInput', (data, bindPath, value) => {
console.log(`binding ${bindPath} to ${value}`);
put(data, bindPath, value);
/*!
* Add items to an object at a specific path
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {Object} obj The object
* @param {String|Array} path The path to assign the value to
* @param {*} val The value to assign
*/
function put(obj, path, val) {
/**
* If the path is a string, convert it to an array
* @param {String|Array} path The path
* @return {Array} The path array
*/
var stringToPath = function (path) {
// If the path isn't a string, return it
if (typeof path !== 'string') return path;
// Create new array
var output = [];
// Split to an array with dot notation
path.split('.').forEach(function (item, index) {
// Split to an array with bracket notation
item.split(/\[([^}]+)\]/g).forEach(function (key) {
// Push to the new array
if (key.length > 0) {
output.push(key);
}
});
});
return output;
};
// Convert the path to an array if not already
path = stringToPath(path);
// Cache the path length and current spot in the object
var length = path.length;
var current = obj;
// Loop through the path
path.forEach(function (key, index) {
// Check if the assigned key shoul be an array
var isArray = key.slice(-2) === '[]';
// If so, get the true key name by removing the trailing []
key = isArray ? key.slice(0, -2) : key;
// If the key should be an array and isn't, create an array
if (isArray && Object.prototype.toString.call(current[key]) !== '[object Array]') {
current[key] = [];
}
// If this is the last item in the loop, assign the value
if (index === length - 1) {
// If it's an array, push the value
// Otherwise, assign it
if (isArray) {
current[key].push(val);
} else {
current[key] = val;
}
}
// Otherwise, update the current place in the object
else {
// If the key doesn't exist, create it
if (!current[key]) {
current[key] = {};
}
// Update the current place in the object
current = current[key];
}
});
};
});
Reef.databind = function(reef){
let el = document.querySelector(reef.elem);
@ -12,6 +112,7 @@ Reef.databind = function(reef){
// bind all elements on the page that have a data-bind item
const bindData = debounce(() => {
console.log("bindData");
let elems = el.querySelectorAll("[data-bind]");
for ( let i = 0; i < elems.length; ++i ){
@ -54,6 +155,8 @@ Reef.databind = function(reef){
}
});
el.addEventListener('input', (evt) => {
let target = evt.target;
@ -86,7 +189,8 @@ Reef.databind = function(reef){
val = parseString(val);
}
put(store.data, bindPath, val);
store.do('databind.onInput', bindPath, val);
//put(store.data, bindPath, val);
}
});
@ -201,98 +305,4 @@ Reef.databind = function(reef){
};
/*!
* Add items to an object at a specific path
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {Object} obj The object
* @param {String|Array} path The path to assign the value to
* @param {*} val The value to assign
*/
function put(obj, path, val) {
/**
* If the path is a string, convert it to an array
* @param {String|Array} path The path
* @return {Array} The path array
*/
var stringToPath = function (path) {
// If the path isn't a string, return it
if (typeof path !== 'string') return path;
// Create new array
var output = [];
// Split to an array with dot notation
path.split('.').forEach(function (item, index) {
// Split to an array with bracket notation
item.split(/\[([^}]+)\]/g).forEach(function (key) {
// Push to the new array
if (key.length > 0) {
output.push(key);
}
});
});
return output;
};
// Convert the path to an array if not already
path = stringToPath(path);
// Cache the path length and current spot in the object
var length = path.length;
var current = obj;
// Loop through the path
path.forEach(function (key, index) {
// Check if the assigned key shoul be an array
var isArray = key.slice(-2) === '[]';
// If so, get the true key name by removing the trailing []
key = isArray ? key.slice(0, -2) : key;
// If the key should be an array and isn't, create an array
if (isArray && Object.prototype.toString.call(current[key]) !== '[object Array]') {
current[key] = [];
}
// If this is the last item in the loop, assign the value
if (index === length - 1) {
// If it's an array, push the value
// Otherwise, assign it
if (isArray) {
current[key].push(val);
} else {
current[key] = val;
}
}
// Otherwise, update the current place in the object
else {
// If the key doesn't exist, create it
if (!current[key]) {
current[key] = {};
}
// Update the current place in the object
current = current[key];
}
});
};
}

33
static/utils.js Normal file
View file

@ -0,0 +1,33 @@
function getOriginalImagePath(pinId){
if ( !pinId ){
return "";
}
let paddedId = pinId.toString().padStart(12, '0');
let dir = `originals/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return file;
}
function getThumbnailImagePath(pinId){
if ( !pinId ){
return "";
}
let paddedId = pinId.toString().padStart(12, '0');
let dir = `thumbnails/${paddedId[11]}/${paddedId[10]}/${paddedId[9]}/${paddedId[8]}`;
let file = `${dir}/${paddedId}.jpg`;
return file;
}
function parseQueryString(qs){
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;
}
}
return obj;
}