From 94a562fdf503a28740fac150a102c86d9bcbda63 Mon Sep 17 00:00:00 2001 From: slynn1324 Date: Fri, 22 Jan 2021 15:38:06 -0600 Subject: [PATCH] breakout into components, bootstrapping --- server.js | 3 +- static/app.js | 192 +++++++++ static/client.css | 117 +++--- static/client.js | 714 ++++----------------------------- static/components/about.js | 65 +++ static/components/addpin.js | 203 ++++++++++ static/components/brickwall.js | 90 +++++ static/components/editboard.js | 119 ++++++ static/components/navbar.js | 59 +++ static/components/pinzoom.js | 141 +++++++ static/index.html | 19 +- static/reef-bootstrap.js | 58 +++ static/reef-databind.js | 200 ++++----- static/utils.js | 33 ++ 14 files changed, 1214 insertions(+), 799 deletions(-) create mode 100644 static/app.js create mode 100644 static/components/about.js create mode 100644 static/components/addpin.js create mode 100644 static/components/brickwall.js create mode 100644 static/components/editboard.js create mode 100644 static/components/navbar.js create mode 100644 static/components/pinzoom.js create mode 100644 static/reef-bootstrap.js create mode 100644 static/utils.js diff --git a/server.js b/server.js index 766deb6..aaa8da5 100644 --- a/server.js +++ b/server.js @@ -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 ){ diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..a2b9f1d --- /dev/null +++ b/static/app.js @@ -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*/` + +
+
+
+
+
+ + +
+
+
+
+ ` + //
+ } +}); + +// 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(); \ No newline at end of file diff --git a/static/client.css b/static/client.css index df18c1d..fe0722e 100644 --- a/static/client.css +++ b/static/client.css @@ -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; + } } \ No newline at end of file diff --git a/static/client.js b/static/client.js index 7f70a11..d5e9e79 100644 --- a/static/client.js +++ b/static/client.js @@ -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", { + +
-
+
` + //
} }); -const navbar = new Reef("#navbar", { - store: store, - template: (data) => { - - let boardName = ""; - - if ( data.board ){ - boardName = /*html*/` - ${data.board.name}   - edit - `; - } else if ( !data.hash.board ) { - boardName = /*html*/`Boards`; - } - - return /*html*/` - - `; - - }, - 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 '
'; - } - - 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*/` -
- - -
${board.name}
-
-
- `; - } - - function createBrickForPin(board, pin){ - return /*html*/` -
- - - -
- `; - } - - // 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 += '
'; - - for ( let i = 0; i < columns[col].bricks.length; ++i ){ - result += columns[col].bricks[i]; - } - - result += '
'; - } - - 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 += ``; - } - - let newBoardField = ''; - if ( data.addPin.boardId == "new" ){ - newBoardField = /*html*/` -
- -
- -
-
- `; - } - - return /*html*/` - - `; - }, - attachTo: app -}); - -const editBoardModal = new Reef("#edit-board-modal", { - store: store, - template: (data) => { - return /*html*/` - - `; - }, - attachTo: app -}); - -const pinZoomModal = new Reef("#pin-zoom-modal", { - store: store, - template: (data) => { - - let siteLink = ''; - if ( data.pinZoom.pin && data.pinZoom.pin.siteUrl ){ - siteLink = ``; - } - - return /*html*/` - - `; - }, - attachTo: app -}); - -const aboutModal = new Reef("#about-modal", { - store: store, - template: (data) => { - return /*html*/` - - `; - }, - 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; -} \ No newline at end of file diff --git a/static/components/about.js b/static/components/about.js new file mode 100644 index 0000000..79383ae --- /dev/null +++ b/static/components/about.js @@ -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*/` + + `; + } +}); }); diff --git a/static/components/addpin.js b/static/components/addpin.js new file mode 100644 index 0000000..7122ad2 --- /dev/null +++ b/static/components/addpin.js @@ -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 += ``; + } + + let newBoardField = ''; + if ( data.addPinModal.boardId == "new" ){ + newBoardField = /*html*/` +
+ +
+ +
+
+ `; + } + + return /*html*/` + + `; + } + +}); }); \ No newline at end of file diff --git a/static/components/brickwall.js b/static/components/brickwall.js new file mode 100644 index 0000000..50fd60f --- /dev/null +++ b/static/components/brickwall.js @@ -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 '
'; + } + + 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*/` +
+ + +
${board.name}
+
+
+ `; + } + + function createBrickForPin(board, pin){ + return /*html*/` +
+ + + +
+ `; + } + + // 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 += '
'; + + for ( let i = 0; i < columns[col].bricks.length; ++i ){ + result += columns[col].bricks[i]; + } + + result += '
'; + } + + return result; + } + +}); }); + diff --git a/static/components/editboard.js b/static/components/editboard.js new file mode 100644 index 0000000..24a6495 --- /dev/null +++ b/static/components/editboard.js @@ -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*/` + + `; + } + +}); }); \ No newline at end of file diff --git a/static/components/navbar.js b/static/components/navbar.js new file mode 100644 index 0000000..2cd2dbf --- /dev/null +++ b/static/components/navbar.js @@ -0,0 +1,59 @@ +app.addComponent('navbar', (store) => { return new Reef("#navbar", { + store: store, + template: (data) => { + + let boardName = ""; + + if ( data.board ){ + boardName = /*html*/` + ${data.board.name}   + edit + `; + } else if ( !data.hash.board ) { + boardName = /*html*/`Boards`; + } + + return /*html*/` + + `; + + } + +}); }); \ No newline at end of file diff --git a/static/components/pinzoom.js b/static/components/pinzoom.js new file mode 100644 index 0000000..9b92708 --- /dev/null +++ b/static/components/pinzoom.js @@ -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 = ``; + } + + return /*html*/` + + `; + } + +}); }); \ No newline at end of file diff --git a/static/index.html b/static/index.html index f4952aa..aa8b47a 100644 --- a/static/index.html +++ b/static/index.html @@ -10,8 +10,25 @@
+ + + + - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/reef-bootstrap.js b/static/reef-bootstrap.js new file mode 100644 index 0000000..da07a9f --- /dev/null +++ b/static/reef-bootstrap.js @@ -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."); + } +}); \ No newline at end of file diff --git a/static/reef-databind.js b/static/reef-databind.js index e41d078..c0b7e69 100644 --- a/static/reef-databind.js +++ b/static/reef-databind.js @@ -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]; - - } - - }); - - }; - } - - diff --git a/static/utils.js b/static/utils.js new file mode 100644 index 0000000..f6f8f95 --- /dev/null +++ b/static/utils.js @@ -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; +} \ No newline at end of file