mirror of
https://github.com/slynn1324/tinypin
synced 2026-05-04 01:40:43 +00:00
851 lines
31 KiB
JavaScript
851 lines
31 KiB
JavaScript
Reef.debug(true);
|
|
|
|
const store = new Reef.Store({
|
|
data: {
|
|
hash: {
|
|
board: null
|
|
},
|
|
loading: false,
|
|
boards: [],
|
|
board: null,
|
|
addPin: {
|
|
active: false,
|
|
boardId: "",
|
|
newBoardName: null,
|
|
imageUrl: "",
|
|
previewReady: false,
|
|
previewImageUrl: null,
|
|
siteUrl: "",
|
|
description: "",
|
|
saveInProgress: false
|
|
},
|
|
pinZoom: {
|
|
active: false,
|
|
pin: null,
|
|
fullDescriptionOpen: false
|
|
},
|
|
about: {
|
|
active: false
|
|
},
|
|
editBoard: {
|
|
active: false,
|
|
name: ""
|
|
}
|
|
},
|
|
getters: {
|
|
isAddPinValid: (data) => {
|
|
|
|
if ( data.addPin.boardId == "new"){
|
|
if ( !data.addPin.newBoardName ){
|
|
return false;
|
|
} else if ( data.addPin.newBoardName.trim().length < 1 ){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ( !data.addPin.previewImageUrl ){
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
isEditBoardValid: (data) => {
|
|
if (!data.editBoard.name){
|
|
return false;
|
|
}
|
|
|
|
if ( data.editBoard.name.trim().length < 1 ){
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
|
|
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)];
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
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.addPin.saveInProgress = true;
|
|
|
|
let boardId = store.data.addPin.boardId;
|
|
|
|
let newBoard = null;
|
|
|
|
if ( boardId == "new" ){
|
|
let res = await fetch('api/boards', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
"name": store.data.addPin.newBoardName
|
|
})
|
|
});
|
|
|
|
if ( res.status == 200 ){
|
|
newBoard = await res.json();
|
|
boardId = newBoard.id;
|
|
store.data.boards.push(newBoard);
|
|
}
|
|
}
|
|
|
|
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--;
|
|
|
|
}
|
|
},
|
|
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,
|
|
template: (data) => {
|
|
return /*html*/`
|
|
<div id="navbar"></div>
|
|
<section class="section">
|
|
<div class="container" id="brick-wall-container">
|
|
<div id="brick-wall" class="brick-wall"></div>
|
|
</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>
|
|
</footer>
|
|
|
|
<div id="add-pin-modal"></div>
|
|
<div id="pin-zoom-modal"></div>
|
|
<div id="edit-board-modal"></div>
|
|
<div id="about-modal"></div>
|
|
`
|
|
}
|
|
});
|
|
|
|
const navbar = new Reef("#navbar", {
|
|
store: store,
|
|
template: (data) => {
|
|
|
|
let boardName = "";
|
|
|
|
if ( data.board ){
|
|
boardName = /*html*/`
|
|
<span class="navbar-item">${data.board.name}
|
|
<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"> </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 />
|
|
|
|
</div>
|
|
<div>
|
|
<h2><strong>credits</strong></h2>
|
|
client
|
|
<br />
|
|
css framework » <a href="https://www.bulma.io">bulma.io</a>
|
|
<br />
|
|
ui framework » <a href="https://reefjs.com">reef</a>
|
|
<br />
|
|
boards icon » <a href="https://thenounproject.com/term/squares/1160031/">squares by Andrejs Kirma from the Noun Project</a>
|
|
<br />
|
|
pin icon » <a href="https://thenounproject.com/term/pinned/1560993/">pinned by Gregor Cresnar from the Noun Project</a>
|
|
<br />
|
|
trash icon » <a href="https://thenounproject.com/term/trash/2449397/">Trash by ICONZ from the Noun Project</a>
|
|
<br />
|
|
server
|
|
<br />
|
|
language & runtime » <a href="https://nodejs.org/en/">node.js</a>
|
|
<br />
|
|
database » <a href="https://www.sqlite.org/index.html">sqlite</a>
|
|
<br />
|
|
library » <a href="https://www.npmjs.com/package/better-sqlite3">better-sqlite3</a>
|
|
<br />
|
|
library » <a href="https://www.npmjs.com/package/express">express</a>
|
|
<br />
|
|
library » <a href="https://www.npmjs.com/package/body-parser">body-parser</a>
|
|
<br />
|
|
library » <a href="https://www.npmjs.com/package/node-fetch">node-fetch</a>
|
|
<br />
|
|
library » <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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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 && typeof(actions[method]) === 'function') {
|
|
actions[method](target);
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', (el) => {
|
|
|
|
if ( store.data.pinZoom.active ){
|
|
if ( el.key == "Escape" ){
|
|
actions.closePinZoomModal();
|
|
} else if ( el.key == "ArrowLeft" ){
|
|
actions.movePinZoomModalLeft();
|
|
} else if ( el.key == "ArrowRight" ){
|
|
actions.movePinZoomModalRight();
|
|
}
|
|
}
|
|
|
|
if ( store.data.addPin.active ){
|
|
if ( el.key == "Escape" ){
|
|
actions.closeAddPinModal();
|
|
}
|
|
}
|
|
|
|
if ( store.data.about.active ){
|
|
if ( el.key == "Escape" ){
|
|
actions.closeAboutModal();
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
window.addEventListener('hashchange', (evt) => {
|
|
console.log("hash change");
|
|
handleHash();
|
|
});
|
|
|
|
window.addEventListener('resize', (evt) => {
|
|
app.render();
|
|
});
|
|
|
|
Reef.databind(app);
|
|
app.render();
|
|
|
|
handleHash();
|
|
loadBoards();
|
|
|
|
|
|
function handleHash(){
|
|
|
|
let hash = parseQueryString(window.location.hash.substr(1));
|
|
store.data.hash = hash;
|
|
|
|
if ( hash.board ){
|
|
if ( !store.board || store.board.id != hash.board ){
|
|
loadBoard(hash.board);
|
|
}
|
|
} else {
|
|
store.data.board = null;
|
|
|
|
store.data.pinZoom.active = false;
|
|
store.data.addPin.active = false;
|
|
store.data.about.active = false;
|
|
}
|
|
|
|
}
|
|
|
|
async function loadBoard(id){
|
|
store.data.loading++
|
|
let res = await fetch("/api/boards/" + id);
|
|
store.data.board = await res.json();
|
|
store.data.loading--;
|
|
}
|
|
|
|
async function loadBoards(){
|
|
store.data.loading++;
|
|
let res = await fetch("/api/boards");
|
|
store.data.boards = await res.json();
|
|
store.data.loading--;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
} |